|

Room TypeConverter 정리

Room TypeConverter 정리 대표 이미지
객체 표현과 DB 컬럼 표현이 다를 때 TypeConverter 판단이 필요하다

Room TypeConverter는 단순 문법이 아니라, 앱 안의 객체 표현과 SQLite 컬럼 표현 사이를 번역하는 장치입니다. 결론부터 말하면 컬럼 하나로 납작하게 저장해도 되는 값인지 먼저 판단한 뒤 converter를 붙여야 Room 설계가 오래 버팁니다.

이 글에서는 왜 Room이 객체를 그대로 저장하지 못하는지, enum과 LocalDateTime은 왜 converter가 잘 맞는지, List나 복합 객체는 언제 테이블 분리가 더 나은지까지 실무 기준으로 정리하겠습니다.


왜 그대로 못 넣을까

Room은 Kotlin 객체 저장소가 아니라 SQLite 위에서 동작하는 ORM입니다. 그래서 각 필드를 결국 SQLite가 이해하는 컬럼 값으로 바꿔야 합니다. Int, Long, String처럼 규칙이 분명한 값은 괜찮지만 enum, LocalDateTime, List 같은 값은 어떤 저장 규칙을 써야 할지 개발자가 명시해야 합니다.

객체 값을 Room DB에 저장할 때 TypeConverter 판단 흐름도
핵심은 객체냐 아니냐보다 컬럼 하나로 표현해도 되는 값인지 먼저 보는 것이다

한 줄로 보면

TypeConverter는 앱 타입을 persistence 가능한 DB 타입으로 바꾸고, 다시 읽을 때 원래 앱 타입으로 복원하는 양방향 규칙입니다. 즉 저장 시점과 읽기 시점의 번역 메서드를 Room에 등록하는 방식입니다.

Room TypeConverter 핵심 판단 카드
작은 값의 표현 문제인지, 관계 설계 문제인지 먼저 나눠야 한다

예시 1: enum

enum은 값 하나를 의미 있는 코드로 표현할 때가 많아서 converter와 궁합이 좋습니다. 컬럼 하나로 충분하고, 문자열로 저장하면 디버깅도 쉽습니다.

enum class Priority {
    LOW,
    MEDIUM,
    HIGH,
}

@Entity
data class TaskEntity(
    @PrimaryKey val id: Long,
    val title: String,
    val priority: Priority,
)

class TaskConverters {
    @TypeConverter
    fun fromPriority(value: Priority): String = value.name

    @TypeConverter
    fun toPriority(value: String): Priority = Priority.valueOf(value)
}

여기서도 ordinal보다는 의미가 남는 문자열 저장이 더 안전한 경우가 많습니다. enum 순서가 바뀌면 ordinal은 migration과 디버깅을 더 거칠게 만들 수 있기 때문입니다.


예시 2: LocalDateTime

LocalDateTime은 객체 하나처럼 보이지만 저장 방식은 하나가 아닙니다. 문자열로 저장할지, epoch 숫자로 저장할지, timezone 해석을 어떻게 둘지 먼저 정해야 합니다. 즉 converter는 문법이 아니라 저장 정책에 가깝습니다.

class DateTimeConverters {
    @TypeConverter
    fun fromLocalDateTime(value: LocalDateTime?): String? {
        return value?.toString()
    }

    @TypeConverter
    fun toLocalDateTime(value: String?): LocalDateTime? {
        return value?.let(LocalDateTime::parse)
    }
}

DB를 사람이 직접 읽어야 하는지, 정렬과 비교를 숫자 기준으로 빠르게 하고 싶은지, 서버와 교환 규칙이 무엇인지에 따라 더 자연스러운 포맷이 달라집니다.


예시 3: List 저장

List 같은 값은 문자열 하나로 직렬화해서 저장할 수는 있습니다. 하지만 특정 태그 검색, 집계, 정렬, 수정 요구가 붙기 시작하면 문자열 덩어리 안에 정보를 숨기게 됩니다. 이때는 converter보다 별도 테이블 설계가 더 자연스러울 가능성이 큽니다.

  • 저장만 되면 되는 작은 부가 표현인가
  • 값 내부를 SQL에서 검색할 일은 없는가
  • 정렬, 집계, 조인 요구가 붙을 가능성은 낮은가
  • 변환 규칙이 나중에도 안정적으로 유지될 수 있는가

즉 객체냐 원시값이냐보다 그 값이 DB 안에서 독립적으로 질의될 정보인가를 먼저 봐야 합니다.


남용하면 생기는 문제

Room TypeConverter 남용 신호 카드
저장은 쉬워져도 쿼리성, migration, 디버깅은 같이 어려워질 수 있다
  1. DB에서는 단순해 보이지만 쿼리는 오히려 불편해진다
  2. 변환 규칙이 바뀌면 schema 변경이 없어 보여도 데이터 의미가 달라질 수 있다
  3. DB Inspector에서 값이 난독화된 문자열처럼 보여 디버깅이 느려진다
  4. SQLite의 조회·정렬·관계 표현 장점을 스스로 줄이게 된다

ProvidedTypeConverter는 언제

어떤 converter는 공통 serializer 설정이나 의존성이 필요할 수 있습니다. 이럴 때 Room의 ProvidedTypeConverter를 검토할 수 있습니다. 핵심은 converter가 필요하지만 static한 유틸 함수처럼 두고 싶지 않을 때, database builder 쪽에서 명시적으로 제공한다는 점입니다.


빠르게 고르는 기준

  1. 컬럼 하나로 표현해도 되는 값인가
  2. 값 내부를 SQL에서 검색하지 않아도 되는가
  3. 사람이 DB 값을 봐도 의미를 이해할 수 있는가
  4. 변환 규칙이 바뀔 때 migration까지 함께 설명할 수 있는가

대체로 “예”가 많으면 converter가 잘 맞고, “아니오”가 많으면 컬럼 분리나 관계 설계를 먼저 보는 편이 안전합니다.



컬럼 하나로 충분한지가 먼저다

TypeConverter를 쓸지 말지는 객체가 복잡하냐가 아니라, 그 값을 컬럼 하나로 납작하게 저장해도 나중 질의와 해석이 안 꼬이는지부터 보는 편이 좋습니다.

검색, 집계, 정렬, 관계가 중요해지는 값이라면 converter보다 테이블 분리가 더 자연스러울 수 있습니다.

마무리

Room TypeConverter는 Room의 부족함을 억지로 메우는 해킹이 아니라, 앱 객체와 DB 표현의 경계를 개발자가 명시하는 공식 통로에 가깝습니다. enum, 날짜/시간, 작은 value object에는 매우 유용하지만, 검색성과 관계가 중요한 구조까지 전부 문자열 하나로 감추기 시작하면 쿼리와 migration이 같이 어려워질 수 있습니다.

함께 보면 좋은 글로는 Room migration 쉽게 이해하기, 화면 상태는 어디에 두는 게 맞을까, 안드로이드 single activity 구조는 왜 많아졌을까가 있습니다. 공식 기준은 Referencing complex data using Room, TypeConverter API reference, TypeConverters API reference, ProvidedTypeConverter API reference를 같이 보면 더 선명해집니다.

함께보면 좋은 글