티스토리 뷰

개요

  • RDBMS는 엄격한 스키마 정합성을 기반으로 오랫동안 업계에서 사랑 받아 왔다. 하지만 리얼 월드에서는 불가피하게 가변적인 구조의 데이터를 다뤄야 하는 상황을 항상 경험하게 된다. (내 최근 경험으로는 브라우저에 렌더링된 가변적인 HTML5 Canvas 태그 정보를 저장해야 하는 상황을 겪었다.)
  • 일반적으로 RDBMS에서 가변 데이터를 다뤄야할 때 EAV 패턴을 사용하거나, VARCHAR 컬럼에 JSON 문자열을 그대로 저장하는 방법을 사용하는데 2가지 방법 모두 최선책은 아니다. 최근의 RDBMSNoSQL에 대응하여 네이티브 JSON 데이터 타입과 연관된 쿼리 함수를 제공하고 있고, 이 방법은 RDBMS에서 가변 구조의 데이터를 저장하는데 있어 최선책이라고 말할 수 있다.
  • 이번 글에서는 Spring Data JPA 프로젝트 환경에서 데이터베이스 벤더마다 특화된 JSON 컬럼 타입을 JPA 레벨에서 Map<String, Any?> 타입으로 자동 맵핑하는 방법을 소개하고자 한다.

MySQL과 JSON 데이터 타입

  • MySQL의 경우 5.7.8부터 JSON 데이터 타입을 네이티브 지원한다. 이를 통해 여러 네이티브 함수의 도움을 받아 단순히 VARCHAR 데이터 타입 사용 대비 NoSQL 스타일의 유연한 CRUD가 가능해진다.
  • 만약 기존에 VARCHAR 데이터 타입으로 JSON 문자열을 저장하고 있다면 아래와 같이 데이터 유실 없이 JSON 데이터 타입 변경이 가능하다.
-- foo.bar 컬럼을 JSON 타입으로 변경, INPLACE 알고리즘 사용은 불가능
ALTER TABLE foo MODIFY COLUMN bar JSON NULL DEFAULT NULL, ALGORITHM=COPY;

build.gradle.kts

  • High-Performance Java Persistence의 저자인 Vlad Mihalcea가 제작한 Hibernate 데이터 타입 확장 라이브러리의 도움을 받을 것이다. 프로젝트 루트의 build.gradle.kts에 아래 내용을 추가한다.
dependencies {

    // spring-boot-starter-data-jpa 3.X.X 버전 사용시
    implementation("com.vladmihalcea:hibernate-types-60:2.20.0")
    
    // spring-boot-starter-data-jpa 2.X.X 버전 사용시
    implementation("com.vladmihalcea:hibernate-types-55:2.20.0")
}

BaseEntity 작성

  • String Boot 2.X.X에만 해당하는 내용으로, BaseEntity에 아래 내용을 추가한다. (String Boot 3.0.0 이상에서는 작성하지 않는다.)
import com.vladmihalcea.hibernate.type.json.JsonType
import jakarta.persistence.*
import org.hibernate.annotations.*

// spring-boot-starter-data-jpa 3.X.X 버전 사용시 생략
// spring-boot-starter-data-jpa 2.X.X 버전 사용시 작성 필수
@TypeDefs(TypeDef(name = "json", typeClass = JsonType::class))
@MappedSuperclass
class BaseEntity {
    ...
}

엔티티에 JSON 타입 필드 작성

  • 임의의 엔티티에 JSON 타입 필드를 아래와 같이 추가한다. 이 것으로 끝이다.
import com.vladmihalcea.hibernate.type.json.JsonType
import jakarta.persistence.*
import org.hibernate.Hibernate
import org.hibernate.annotations.Type
import java.io.Serializable

@Entity
@Table(name = "foo")
class Foo : BaseEntity(), Serializable {

    ...

    // spring-boot-starter-data-jpa 3.X.X 버전 사용시
    @Type(JsonType::class)
    @Column(columnDefinition = "json")
    var bar: Map<String, Any?>? = null

    // spring-boot-starter-data-jpa 2.X.X 버전 사용시
    @Type(type = "json")
    @Column(columnDefinition = "json")
    var bar: Map<String, Any?>? = null
}

다른 대안: 커스텀 AttributeConverter 작성

  • 위 소개한 hibernate-types 라이브러리를 사용하지 않고도 아래와 같이 커스텀 AttributeConverter을 제작해도 동일하게 작동한다.
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import jakarta.persistence.AttributeConverter
import jakarta.persistence.Converter

@Converter(autoApply = false)
class MapToJsonConverter : AttributeConverter<Map<String, Any?>, String?> {

    override fun convertToEntityAttribute(dbData: String?): Map<String, Any?>? {

        return try {
            ObjectMapper().readValue<HashMap<String, Any?>>(dbData ?: "")
        } catch (ex: Exception) {
            return null
        }
    }

    override fun convertToDatabaseColumn(attribute: Map<String, Any?>?): String? {

        return try {
            attribute ?: return null
            if (attribute.isEmpty()) return null
            ObjectMapper().writeValueAsString(attribute)
        } catch (ex: Exception) {
            return null
        }
    }
}
  • 이제 대상 엔티티 필드 레벨에 @Type 대신 @Convert을 명시하면 적용이 완료된다.
@Entity
@Table(name = "foo")
class Foo : BaseEntity(), Serializable {

    ...

    @Convert(converter = MapToJsonConverter::class)
    @Column(columnDefinition = "json")
    var bar: Map<String, Any?>? = null
}

참고 글

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함