Enum은 열거형이라고 불리는 데이터 유형입니다. 고정된 상수 집합을 하나의 클래스처럼 다룰 수 있기에 빈번하게 사용됩니다.
JPA는 enum을 지원하며 데이터베이스에 열거형 상수를 저장하고 조회할 수 있도록 지원합니다.
일반적으로 JPA에서 enum을 매핑하기 위해 @Enumerated 애노테이션을 사용하여 매핑합니다. 혹은 @Converter를 활용합니다.
Java
@Entity
public class Book {
// ...
@Enumerated(EnumType.STRING)
private BookType type;
// ...
}
Kotlin
@Entity
class Book constructor(
...
@Convert(converter = BookTypeConvertor::class)
val type: BookType,
...
)
enum을 매핑하는 방법이 여러 가지가 존재하지만 오늘은 이 두 가지에 대해 정리하려고 합니다.
@Enumerated
@Enumerated 애노테이션은 JPA에서 enum을 매핑하는 가장 기본적인 방법입니다.
이를 사용하면 enum 상수를 데이터베이스에 어떻게 저장할지 지정할 수 있습니다.
이는 EnumType.ORDINAL 혹은 EnumTYPE.STRING을 매개변수로 사용합니다.
순서를 저장할지 상수의 이름을 문자열로 저장할지 선택합니다.
물론 대부분 문자열로 저장합니다. 순서는 변경에 매우 취약하기 때문입니다.
이 매핑 방식은 ENUM의 상수의 변경에 취약합니다. 실제 상수의 값이 변경되면 다양한 문제가 발생할 수 있습니다.
예를 들어 테스트 코드, 비즈니스 로직, 데이터베이스 일관성 등 생각보다 많은 영향을 주게 됩니다.
설계단계에서 절대 변하지 않을 것 같지만 가능성은 존재하고 개발자는 이에 대비해야 합니다.
다음과 같은 Enum이 정의되어 있고, 값이 변경되었습니다.
enum class BookType {
SCIENCE,
SOCIETY,
ECONOMY,
//COMPUTER, COMPUTER_SCIENCE 로 변경됩니다.
COMPUTER_SCIENCE,
UNCATEGORIZED,
LANGUAGE;
}
매핑 방법을 변경하지 않고 영향을 최소화할 수도 있을 것 같습니다.
enum class BookType constructor(
val value: String
) {
SCIENCE("SCIENCE"),
SOCIETY("SOCIETY"),
ECONOMY("ECONOMY"),
//COMPUTER("COMPUTER"),
COMPUTER_SCIENCE("COMPUTER"),
UNCATEGORIZED("UNCATEGORIZED"),
LANGUAGE("LANGUAGE");
}
만약 enum의 속성을 사용하도록 비즈니스 로직이 작성되어 있다면 수정할 부분이 줄어들 것입니다.
이 값을 활용해 쿼리를 작성할 수도 있을 것입니다.
이제 문제는 과거 데이터베이스에 저장된 속성 값을 어떻게 가져오느냐에 초점을 두려고 합니다.
과거에는 "COMPUTER"라는 문자열이 저장되어 있고 이는 현재 "COMPUTER_SCIENCE"라는 문자열로 변경되어야 하기 때문입니다.
이때 우리는 다른 매핑 방법을 활용할 수 있습니다.
@Converter
AttributeConverter를 상속받아 특정 속성이 데이터베이스에 저장될 때 혹은 데이터베이스에서 조회될 때 작동하는 컨버터를 사용자 커스텀 할 수 있습니다.
@Converter
class BookTypeConvertor : AttributeConverter<BookType, String>{
override fun convertToDatabaseColumn(bookType: BookType?): String? {
if(bookType == null) return null;
return bookType.value
}
override fun convertToEntityAttribute(dbData: String?): BookType? {
if(dbData == null) return null
return BookType.fromValue(dbData)
}
}
다만 주의사항이 몇 가지 존재합니다.
@Id, @Version, @Enumerated, @Temporal로 명시적으로 애노테이션이 적용된 속성은 컨버터에 적용을 받지 않습니다.
@Target({TYPE}) @Retention(RUNTIME)
public @interface Converter {
boolean autoApply() default false;
}
autoApply가 true인 경우는 @Convert을 사용하여 특정 속성에서 자동으로 적용되는 변환을 재정의하거나 비활성화할 수 있습니다.
false라면 명시적으로 @Convert 애노테이션으로 지정된 속성만 변환됩니다.
이는 명시적으로 지정하는 것이 좋다고 생각합니다. 코드를 통해 한눈에 파악할 수 있다는 장점 때문입니다.
convertToDatabaseColumn : 이름 그대로 데이터베이스에 저장될 때 실행됩니다.
convertToEntityAttribute : 데이터베이스에서 조회한 이후 엔티티로 변환될 때 실행됩니다.
이를 활용하면 과거에 저장된 상수와 현재는 제거된 상수를 매핑할 수도 있을 것입니다.
override fun convertToEntityAttribute(dbData: String?): BookType? {
if(dbData == null) return null
return BookType.fromValue(dbData)
}
companion object {
fun fromValue(value: String): BookType {
if(value == "COMPUTER"){
return COMPUTER_SCIENCE
}
return values().firstOrNull {
it.value == value
} ?: throw IllegalArgumentException("Format $value is illegal")
}
}
하드코딩이 마음에 들지 않는다면 과거의 데이터를 표현하는 상수를 정의하고 사용하면 될 것 같습니다.
정리
@Enumerated는 JPA의 기본적인 Enum 매핑 방법 중 하나입니다. 하지만 변경에 유연하지 않습니다.
만약 미래를 생각하여 Enum 속성을 데이터베이스에 저장할 때 @Converter를 사용하여 Enum의 속성을 저장
하도록 하고, 엔티티로 변환 시 그 속성 값을 기준으로 Enum을 반환하도록 한다면 매우 유연해지므로 변경에
도 일관된 데이터 및 변경의 영향을 최소화할 수 있습니다.
JPA에서 ENUM을 활용할 때 이러한 미래의 영향까지 생각해 매핑 방법을 결정하는 것은 미래에 큰 도움이 될 것 같습니다.
'데이터 접근 기술 > JPA' 카테고리의 다른 글
Hibernate 5 Bootstrapping API (0) | 2023.02.01 |
---|---|
DTO와 Entity (0) | 2022.12.11 |
상속관계 매핑 (0) | 2022.12.04 |
값 타입 (0) | 2022.11.20 |
OSIV (0) | 2022.10.31 |
댓글