[Android] Room에서 Entity 간의 관계 정의하기

NoFlue·2023년 2월 4일
0

이 글을 쓰는 지금, 첫 토이 프로젝트로 여행 플래너를 개발하는 중이다. 플래너를 만들기 위해 Database를 다음과 같이 설계해보았다.

처음 DB를 설계해봤기에 엉망인 점이 많겠지만 원하는 대로 설계가 되었다. 이제 설계한 DB를 Room으로 구현을 해야한다. 각각의 테이블의 엔티티를 정의하고, 엔티티 간의 관계를 정의해서 데이터의 목록을 가져와야 하는데 Room에서는 어떤 방법을 사용하여 가져와야 하는지 여러 방법을 찾아보았다.

Entity 관계 정의하는 방법

설계한 DB를 엔티티로 간단하게 정의 해보았다.

// 해시태그 테이블을 엔티티로 정의
@Entity(tableName = "hashtag")
data class Hashtag(
    ...
)

// 플래너 테이블을 엔티티로 정의
@Entity(tableName = "planner")
data class Planner(
    ...
)

// 데일리 플랜 테이블을 엔티티로 정의
@Entity(tableName = "daily_plan")
data class DailyPlan(
    ...
)

// 일정 테이블을 엔티티로 정의
@Entity(tableName = "plan")
data class Plan(
    ...
)

이제 정의한 엔티티들 간의 관계를 정의해야한다. 이때까지 찾아본 방법은 3가지가 있었다.

  • TypeConverter 를 이용하여 ListJson 형태로 변환
  • @Relation 을 사용하여 엔티티간 관계를 모델링하는 클래스 정의
  • 멀티매핑 반환 ( Multimap )

각 방법의 사용법과 장단점을 알아보자.

TypeConverter 를 이용한 List를 Json 형태로 변환

@TypeConverter 어노테이션은 SQLite에서 지원하지 않는 참조형 타입을 지원하는 타입으로 변환해준다.
이 방법을 사용하기 위해선 엔티티 데이터 클래스 내에 다른 엔티티 목록을 선언 해주어야 한다.

// 데일리 플랜 엔티티 내부에 일정 리스트를 선언했다.
@Entity
data class DailyPlan(
	...,
    var planList: List<Plan>? = null
)

List 타입을 DB에 저장하기 위해선 @TypeConverter 를 통해 String 타입인 Json 형태 로 바꿔주어야 한다. 만약 변환하지 않고 그대로 실행하면 해당 컴파일 에러가 뜨게 된다.

error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.

class ListConverter {

	@TypeConverter
    fun listToJson(list: List<Plan>?) = Gson.toJson(list)
    
    @TypeConverter
    fun jsonToList(value: String) = Gson().fromJson(value, Array<Plan>::class.java).toList()
}


// TypeConverter 를 정의한 클래스를 데이터베이스에 적용하기
@Database(
	entities = [Hashtag::class, Planner::class, DailyPlan::class, Plan::class],
    version = 1
)
@TypeConverters(ListConverter::class)
abstract class PlannerDatabase: RoomDatabase() {
	...
}

Gson 라이브러리를 사용해 List -> Json, Json -> List 변환을 할 수 있게 되었다.

장점

  • 우리가 일반적으로 클래스를 설계하는 방식처럼 필드에 List를 선언할 수 있어 익숙하고 쉽다.

단점

  • Json 라이브러리를 사용할 경우 런타임 때 객체를 인스턴스화 하기 위해 리플렉션을 사용한다.
  • 해당 엔티티의 쿼리문을 작성하기가 힘들다.

이러한 장단점이 있긴 하지만 이 방법은 사용하지 않는 것이 좋다고 생각한다. Room 에서는 객체 간 직접적인 참조를 금하기 때문이다.

대부분의 객체 관계 매핑(ORM) 라이브러리에서는 항목 객체가 서로를 참조할 수 있지만, Room은 이러한 상호 참조를 명시적으로 금지합니다.

그렇기 때문에 나머지 두 방법을 사용하는 것이 좋을 것 같다.

@Relation 어노테이션 사용

@Relation 어노테이션은 두 엔티티 간의 관계를 모델링하는 클래스를 정의하는 기능을 제공해준다.
아래의 코드는 1 : N 관계를 모델링한 클래스다.

data class DailyWithPlanList(
	@Embedded val dailyPlan: DailyPlan,
    @Relation(
    	parentColumn = "dailyId", // 상위 엔티티의 PK
        entityColumn = "dailyId" // 상위 엔티티를 기본키를 참조하는 하위 엔티티 PK
    )
    val planList: List<Plan>
)

관계를 모델링한 클래스를 미리 정의해뒀기 때문에 테이블 끼리 JOIN 할 필요없이 반환 타입만 모델링한 클래스로 두고, 상위 엔티티를 검색하는 쿼리문을 작성하면 된다.

@Transction
@Query("SELECT * FROM daily_plan WHERE dailyId = :id AND day = :day")
fun getDailyWithPlanList(id: Long, day: Int): DailyWithPlanList

장점

  • 쿼리문을 간단하게 작성할 수 있다.

단점

  • 추가적인 데이터 클래스를 정의해야 하므로 코드가 복잡해질 수 있다.

멀티매핑 반환

MultimapMap 과 유사하지만 키가 중복될 수 있다는 차이점이 있다. 멀티매핑 반환은 멀티맵을 반환하는 방법이며, 추가로 데이터 클래스를 정의할 필요가 없다.

@Query(
	"SELECT * FROM daily_plan AS daily" +
	"INNER JOIN plan ON daily.dailyId = plan.dailyId"
)
fun getDailyAndPlanList(): Map<DailyPlan, List<Plan>>

위 코드에서 DailyPlan 필요없이 특정 컬럼인 day 를 가져오고 싶다. 그러기 위해선 @MapInfo 어노테이션을 사용하면 된다.

@MapInfo(keyColumn = "day")
@Query(
	"SELECT * FROM daily_plan AS daily" +
	"INNER JOIN plan ON daily.dailyId = plan.dailyId"
)
fun getSpecificDayAndPlanList(): Map<Int, List<Plan>>

멀티매핑 반환 방법은 Room 2.4.0 부터 지원한다.

장점

  • 추가적인 데이터 클래스를 정의할 필요가 없다.

단점

  • 쿼리문이 상대적으로 복잡하다. (SQL을 배운다면 상쇄될 수 있는 단점이다)

사용 용도에 따라 @Relation, 멀티매핑 반환 을 선택해서 사용하면 된다.
@Relation 어노테이션을 사용할 때 1 : N 관계 말고도 다른 관계에 대한 내용은 Room Database 문서 에서 공부할 수 있다.

profile
앱 개발에 호기심이 많은 대학생 개발자 :3

0개의 댓글