[Android][Room] Room Entity 다중 관계 사용해보기

윤찬·2025년 9월 24일

Android

목록 보기
25/37

안드로이드를 사용하면서 내부 데이터를 저장할 때 Room을 많이 사용해봤을 것이다.

보통 서버에 데이터가 왔을 때 저장하는 Paing3의 데이터를 Room에 저장할 때나 주로 사용한 것 같은데, Room Entity끼리 연관 관계를 사용해본 적은 없는 것 같다.

뭐.. 실제로도 서버에 있는 데이터가 알아서 연관 관계에 맞게 데이터를 주기 때문에 잘 사용하지 않기도 하는 것 같지만.. (제 생각)

이번 강의에서 Mult-table Database에 대한 강의가 있어 기록할 겸 작성하게 되었다.

참고로 공식 문서에서 객체간 관계 데이터를 사용하는 방법이 자세히 나와있으니 한 번 참고해보는 것도 좋을 것 같다.

관계 데이터를 사용하는 이유

먼저 해당 프로젝트의 모델을 보자
EchoUi라는 presentation쪽에 사용하는 모델이다.

data class EchoUi(
    val id: Int,
    val title: String,
    val mood: MoodUI,
    val recordedAt: Instant,
    val note: String?,
    val topics: List<String>,
    val amplitudes: List<Float>,
    val audioFilePath: String,
    val playbackTotalDuration: Duration,
    val playbackCurrentDuration: Duration = Duration.ZERO,
    val playbackState: PlaybackState = PlaybackState.STOPPED
) {
    val formattedRecordedAt = recordedAt.toReadableTime()
    val playbackRatio = (playbackCurrentDuration / playbackTotalDuration).toFloat()
}

여기에 topics라는 List<String>을 사용하는데 이거는 해시태그라고 생각하면 좋을 것 같다.

이 데이터 클래스는 아래 이미지의 하나의 아이템을 표시한다

그런데 위에 두 개의 필터가 있다. 하나는 감정에 따른 필터, 다른 하나는 Topic에 따른 필터이다. 여기서 Topic은 이미지에 나와 있는 "work"와 "테스트"다.

Topic 필터에는 지금까지 사용한 해시태그의 정보가 전부 보여져야 한다. 아래 이미지 처럼 말이다.

그런데 EchoUi의 전체 파라미터를 Entity로 만들 경우 모든 Echo item의 Topic 정보를 가져오는 것은 전체 EchoUi를 확인해보면서 찾아야 한다. 이는 많은 참조가 발생할 수 있다.

물론 서버 API에 요청해서 Topic의 정보만 달라고 요청하는 것도 방법이지만, 이 프로젝트에서는 서버를 사용하지 않고 내부 데이터로만 구현하고 있다.

그래서 다대다 관계인 Topic의 엔티티와 Echo의 엔티티를 이용하면 굳이 Echo의 정보를 다 훑어보지 않아도 Topic에 있는 정보만 가져오면 되기 때문에 많은 참조를 하지 않아도 간단하게 사용할 수 있다. 이럴 때 Multi-table database의 도입을 한 것이다.

Multi-table database 적용

일단 Entity에 사용할 두 모델을 정의했다.

//topics가 제외된 EchoEntity
@Entity
data class EchoEntity(
    @PrimaryKey(autoGenerate = true)
    val echoId: Int = 0,
    val title: String,
    val mood: Mood,
    val recordedAt: Long,
    val note: String?,
    val audioFilePath: String,
    val audioPlaybackLength: Long,
    val audioAmplitudes: List<Float>
)

//topic의 정보를 담는 TopicEntity
@Entity
data class TopicEntity(
    @PrimaryKey(autoGenerate = false)
    val topic: String,
)

일단 두 데이터 클래스 중 Mood와 List<Float>는 알지 못하므로 TypeConverter 작성

class FloatListTypeConverter {
    @TypeConverter
    fun fromList(value: List<Float>): String {
        return value.joinToString(",")
    }

    @TypeConverter
    fun toList(value: String): List<Float> {
        return value.split(",").map { it.toFloat() }
    }
}

class MoodUiTypeConverter {
    @TypeConverter
    fun fromMood(mood: Mood): String {
        return mood.name
    }

    @TypeConverter
    fun toMood(moodName: String): Mood {
        return Mood.valueOf(moodName)
    }
}

이제 EchoEntity와 TopicEntity의 연관 관계를 가지는 EchoTopicCrossRef를 작성하고
echo에 대한 topics의 정보를 담는 EchoWithTopics를 구현했다.

@Entity(
    primaryKeys = ["echoId", "topic"]
)
data class EchoTopicCrossRef(
    val echoId: Int,
    val topic: String
)

data class EchoWithTopics(
    @Embedded val echo: EchoEntity,
    @Relation(
        parentColumn = "echoId",
        entityColumn = "topic",
        associateBy = Junction(EchoTopicCrossRef::class)
    )
    val topics: List<TopicEntity>
)

여기서 주목해야할 것이 @Entity 어노테이션에서 primaryKeys인데 각 엔티티의 프라이머리 키를 연결

EchoWithTopics는 echo에서 가지는 topics의 정보를 가져오는 방법이다. 반대도 가능하다.

data class TopicsWithEcho(
    @Embedded val topic: TopicEntity,
    @Relation(
        parentColumn = "topic",
        entityColumn = "echoId",
        associateBy = Junction(EchoTopicCrossRef::class)
    )
    val echos: List<EchoEntity>
)

근데 이 데이터클래스는 사용하지 않기 때문에 구현하지 않았다. 이제 이 EchoWithTopics의 정보를 토대로 도메인 Echo 데이터클래스가 만들어지고 마지막으로 UI에 사용되는 EchoUi가 되는 것이다.

이제 이를 사용하는 Dao 인터페이스를 작성했다.


@Dao
interface EchoDao {
	//이걸로 원하던(topic의 정보들 + echo의 정보) 정보를 가져올 수 있다.
    @Query("SELECT * FROM echoentity ORDER BY recordedAt DESC")
    fun observeEchos(): Flow<List<EchoWithTopics>>
	
    //이걸로 이제 모든 Topic의 정보들을 가져올 수 있다.
    @Query("SELECT * FROM topicentity ORDER BY topic ASC")
    fun observeTopics(): Flow<List<TopicEntity>>

    @Query("""
        SELECT *
        FROM topicentity
        WHERE topic LIKE '%' || :query || '%'
        ORDER BY topic ASC
    """)
    fun searchTopics(query: String): Flow<List<TopicEntity>>

    @Insert
    suspend fun insertEcho(echo: EchoEntity): Long

    @Upsert
    suspend fun upsertTopic(topicEntity: TopicEntity): Long

    @Insert
    suspend fun insertEchoTopicCrossRef(crossRef: EchoTopicCrossRef)

    @Transaction
    suspend fun insertEchoWithTopics(echoWithTopics: EchoWithTopics) {
        val echoId = insertEcho(echoWithTopics.echo)

        echoWithTopics.topics.forEach { topic ->
            upsertTopic(topic)
            insertEchoTopicCrossRef(
                crossRef = EchoTopicCrossRef(
                    echoId = echoId.toInt(),
                    topic = topic.topic
                )
            )
        }
    }
}

이제 이를 이용해 음성 녹음을 진행하면 아래와 같이 Room의 정보가 쌓이는 것을 볼 수 있다.

참고로 이건 Android Studio에서 App Inspection에서 확인이 가능하다. 두 엔티티가 연관 관계가 제대로 이루어 진 것을 볼 수 있다.

profile
좋은 개발자가 되기까지

0개의 댓글