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

Room은 기본 유형과 박싱된 유형을 변환할 수 있지만 항목 간 객체 참조는 허용하지 않습니다. 유형 변환기를 사용하는 방법 및 Room이 객체 참조를 지원하지 않는 이유를 알아보세요.

유형 변환기 사용

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

Room 데이터베이스에 Date 인스턴스를 유지해야 한다고 가정해 보겠습니다. Room은 기본적으로 Date 객체를 유지할 수 없으므로 유형 변환기를 정의해야 합니다.

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

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

이 예에서는 Date 객체를 Long 객체로 변환하는 유형 변환기 함수와 Long 객체를 다시 Date 객체로 변환하는 유형 변환기 함수라는 두 가지 유형 변환기 함수를 정의합니다. Room은 Long 객체를 유지할 수 있으므로 이러한 변환기를 사용하여 Date 객체를 유지할 수 있습니다.

다음으로 Room에서 정의한 변환기 클래스를 사용할 수 있도록 AppDatabase 클래스에 @ColumnTypeConverters 주석을 추가합니다.

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

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

@Entity
data class User(
    @PrimaryKey val id: Long,
    val name: String,
    val birthday: Date?
)

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

이 예에서는 AppDatabase@ColumnTypeConverters 주석을 달았으므로 Room에서 정의된 유형 변환기를 어디서든 사용할 수 있습니다. 대신 유형 변환기의 범위를 특정 항목이나 DAO로 지정하려면 @Entity 또는 @Dao 클래스에 @ColumnTypeConverters 주석을 답니다.

유형 변환기 초기화 제어

Room은 일반적으로 유형 변환기를 자동으로 인스턴스화합니다. 그러나 추가 종속 항목을 유형 변환기 클래스에 전달해야 하는 경우 앱에서 초기화를 직접 제어해야 합니다. 이 경우 변환기 클래스에 @ProvidedColumnTypeConverter 주석을 답니다.

@ProvidedColumnTypeConverter
class ExampleConverter {
    @ColumnTypeConverter
    fun stringToExample(string: String?): ExampleType? {
        return string?.let { ExampleType() }
    }

    @ColumnTypeConverter
    fun exampleToString(example: ExampleType?): String? {
        return example?.toString()
    }
}

@ColumnTypeConverters에서 변환기 클래스를 선언하는 것 외에도 RoomDatabase.Builder.addColumnTypeConverter 함수를 사용하여 변환기 클래스 인스턴스를 RoomDatabase 빌더에 전달합니다.

val db = Room.databaseBuilder<MyDatabase>(applicationContext, "database-name")
    .addColumnTypeConverter(exampleConverterInstance)
    .build()

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

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

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

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

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

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

Text(text = book.author.name)

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

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

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