Room을 사용하여 복잡한 데이터 참조

Room은 기본 유형과 박싱된 유형 간 변환을 위한 기능을 제공하지만 항목 간 객체 참조는 허용하지 않습니다. 본 문서에서는 유형 변환기를 사용하는 방법 및 Room이 객체 참조를 지원하지 않는 이유를 설명합니다.

유형 변환기 사용

앱에서는 맞춤 데이터 유형을 단일 데이터베이스 열에 저장해야 하는 때도 있습니다. 맞춤 유형을 Room에서 유지할 수 있는 알려진 유형으로 상호 변환하는 방법을 Room에 알려 주는 메서드인 유형 변환기를 제공하여 맞춤 유형을 지원합니다. 유형 변환기는 @TypeConverter 주석을 사용하여 식별합니다.

Room 데이터베이스에 Date 인스턴스를 유지해야 한다고 가정해 보겠습니다. Room에서는 Date 객체를 유지하는 방법을 알 수 없으므로 유형 변환기를 정의해야 합니다.

Kotlin

class Converters {
  @TypeConverter
  fun fromTimestamp(value: Long?): Date? {
    return value?.let { Date(it) }
  }

  @TypeConverter
  fun dateToTimestamp(date: Date?): Long? {
    return date?.time?.toLong()
  }
}

Java

public class Converters {
  @TypeConverter
  public static Date fromTimestamp(Long value) {
    return value == null ? null : new Date(value);
  }

  @TypeConverter
  public static Long dateToTimestamp(Date date) {
    return date == null ? null : date.getTime();
  }
}

이 예에서는 두 가지 유형 변환기 메서드를 정의합니다. 하나는 Date 객체를 Long 객체로 변환하는 메서드이고 다른 하나는 Long에서 Date로 역변환을 실행하는 메서드입니다. Room은 Long 객체를 유지하는 방법을 알고 있으므로 이러한 변환기를 사용하여 Date 객체를 유지할 수 있습니다.

이제 정의한 변환기 클래스를 Room에서 알도록 @TypeConverters 주석을 AppDatabase 클래스에 추가합니다.

Kotlin

@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
  abstract fun userDao(): UserDao
}

Java

@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
  public abstract UserDao userDao();
}

이러한 유형 변환기를 정의하면 프리미티브 유형을 사용하는 것처럼 항목과 DAO에 맞춤 유형을 사용할 수 있습니다.

Kotlin

@Entity
data class User(private val birthday: Date?)

@Dao
interface UserDao {
  @Query("SELECT * FROM user WHERE birthday = :targetDate")
  fun findUsersBornOnDate(targetDate: Date): List<User>
}

Java

@Entity
public class User {
  private Date birthday;
}

@Dao
public interface UserDao {
  @Query("SELECT * FROM user WHERE birthday = :targetDate")
  List<User> findUsersBornOnDate(Date targetDate);
}

이 예에서 Room은 정의된 유형 변환기를 어디서든 사용할 수 있습니다. AppDatabase@TypeConverters 주석을 달았기 때문입니다. 그러나 @Entity 또는 @Dao 클래스에 @TypeConverters 주석을 달아 유형 변환기의 범위를 특정 항목이나 DAO로 지정할 수도 있습니다.

유형 변환기 초기화 제어

Room은 일반적으로 유형 변환기의 인스턴스화를 자동으로 처리합니다. 그러나 때에 따라 추가 종속 항목을 유형 변환기 클래스에 전달해야 할 수도 있습니다. 즉, 앱에서 유형 변환기의 초기화를 직접 제어해야 합니다. 이 경우 변환기 클래스에 @ProvidedTypeConverter 주석을 답니다.

Kotlin

@ProvidedTypeConverter
class ExampleConverter {
  @TypeConverter
  fun StringToExample(string: String?): ExampleType? {
    ...
  }

  @TypeConverter
  fun ExampleToString(example: ExampleType?): String? {
    ...
  }
}

Java

@ProvidedTypeConverter
public class ExampleConverter {
  @TypeConverter
  public Example StringToExample(String string) {
    ...
  }

  @TypeConverter
  public String ExampleToString(Example example) {
    ...
  }
}

그런 다음 @TypeConverters에서 변환기 클래스를 선언하는 것 외에도 RoomDatabase.Builder.addTypeConverter() 메서드를 사용하여 변환기 클래스 인스턴스를 RoomDatabase 빌더에 전달합니다.

Kotlin

val db = Room.databaseBuilder(...)
  .addTypeConverter(exampleConverterInstance)
  .build()

Java

AppDatabase db = Room.databaseBuilder(...)
  .addTypeConverter(exampleConverterInstance)
  .build();

Room에서 객체 참조를 허용하지 않는 이유 이해

주요 요점: Room은 항목 클래스 간의 객체 참조를 허용하지 않습니다. 대신 앱에 필요한 데이터를 명시적으로 요청해야 합니다.

데이터베이스에서 각 객체 모델로 관계를 매핑하는 것은 일반적인 관행이며 이러한 매핑은 서버 측에서 매우 잘 작동합니다. 필드가 액세스될 때 프로그램이 필드를 로드하는 경우에도 서버는 여전히 잘 작동합니다.

그러나 클라이언트 측에서는 이 유형의 지연 로드가 일반적으로 UI 스레드에서 발생하기 때문에 실행 가능하지 않으며 UI 스레드에서 디스크에 관한 정보를 쿼리하면 상당한 성능 문제가 발생합니다. 일반적으로 UI 스레드는 활동의 업데이트된 레이아웃을 계산하고 그리는 데 약 16ms를 소요하므로 쿼리가 5ms밖에 걸리지 않은 경우에도 앱에서 프레임을 그리는 데 여전히 시간이 부족할 가능성이 크며 이에 따라 분명한 시각적 결함이 발생할 수 있습니다. 병렬로 실행 중인 별도의 트랜잭션이 있거나 기기가 다른 디스크 집약적인 작업을 실행 중이면 쿼리가 완료되는 데 훨씬 많은 시간이 걸릴 수 있습니다. 그러나 지연 로드를 사용하지 않으면 앱이 필요한 것보다 더 많은 데이터를 가져오며 이에 따라 메모리 소비 문제가 발생합니다.

객체 관계형 매핑은 일반적으로 개발자가 앱 사용 사례에 가장 적합한 모든 것을 할 수 있도록 이 결정을 개발자에게 맡깁니다. 개발자는 일반적으로 앱과 UI 간에 모델을 공유하려고 합니다. 그러나 이 방법은 확장성이 좋지 않습니다. 시간이 지남에 따라 UI가 변경되므로 공유된 모델이 개발자가 예측 및 디버그하기 어려운 문제를 일으키기 때문입니다.

예를 들어 각 도서에 Author 객체가 있는 Book 객체 목록을 로드하는 UI를 생각해 보세요. 처음에는 지연 로드를 사용하여 Book 인스턴스가 저자를 검색하도록 하는 쿼리를 디자인할 수 있습니다. author 필드의 첫 번째 검색은 데이터베이스를 쿼리합니다. 그리고 얼마 후에 앱 UI에도 저자 이름을 표시해야 한다는 사실을 알게 되었습니다. 다음 코드 스니펫에서와 같이 이 이름에 매우 쉽게 액세스할 수 있습니다.

Kotlin

authorNameTextView.text = book.author.name

Java

authorNameTextView.setText(book.getAuthor().getName());

그러나 외견상 무해한 이 변경사항으로 인해 기본 스레드에서 Author 테이블이 쿼리됩니다.

저자 정보를 미리 쿼리하면 데이터가 더 이상 필요 없는 경우에 데이터가 로드되는 방식을 변경하기가 어려워집니다. 예를 들어 앱의 UI가 더 이상 Author 정보를 표시하지 않아도 되는 경우에도 앱은 더 이상 표시하지 않는 데이터를 사실상 로드하여 소중한 메모리 공간을 낭비합니다. 앱의 효율성은 Author 클래스가 Books와 같은 다른 테이블을 참조하면 훨씬 더 저하됩니다.

Room을 사용하여 여러 항목을 동시에 참조하려면 각 항목이 포함된 POJO를 생성한 후 테이블을 조인하는 쿼리를 작성하세요. Room의 강력한 쿼리 유효성 검사 기능과 결합되어 제대로 구조화된 이 모델을 사용하면 앱이 데이터를 로드할 때 더 적은 리소스를 소비하므로 앱 성능 및 사용자 환경을 향상할 수 있습니다.