스프링

Spring Boot JPA 에서 enum type 사용하기

담쏙 2024. 8. 26. 22:25
728x90

최근 spring boot + kotlin + jpa를 이용해서 개인(?) 프로젝트를 하고 있는데,

JPA에서 enum type을 이용할 때 주의점을 알게 되어 기록해본다.

 

Enum Type Column을 만들자

테이블(엔티티) 컬럼 하나를 아래와 같이 enum type으로 선언하고 빌드하게 되면 enum 컬럼의 constaint인 CHECK가 자동으로 생성되는데 생각했던 것과 다른 형식으로 만들어지게 된다.

enum class TestEnum {
    HELLO,
    THIS,
    IS,
    TEST
}
@Entity
class TestTable (
    /* pk */
    @Id var no: Int,

    /* 타입 */
    @Column(nullable = false)
    var enum: TestEnum,){
}

 

 

뭔가 잘못된 거 같다.. 자동생성된 DDL을 살펴보면 아래와 같은데, 선언해둔 "HELLO", "THIS", "IS", "TEST" 들은 어디로 가고 0~3 사이의 숫자만 가능하다는 제약조건이 생성된 것인가?

CREATE TABLE test_table (
	"no" int4 NOT NULL,
	"enum" int2 NOT NULL,
	CONSTRAINT test_table_enum_check CHECK (((enum >= 0) AND (enum <= 3))),
	CONSTRAINT test_table_pkey PRIMARY KEY (no)
);

 

Enumerated

그 이유는 Enumerated 어노테이션 때문이다.

Enumerated를 에노테이션을 사용하지 않았거나, EnumType을 지정해주지 않으면 EnumType에 ORDINAL이 설정이 되어버린다.

*참고 : Enumerated의 소스이다. default로 EnumType.ORDINAL이 선언되어 있다.

 

그렇다면, 이 EnumType.ORDINAL은 무엇인가?

ORDINAL은 Enum이 선언된 순서를 Integer 값으로 변환하여 DB 컬럼에 저장한다.

즉, DB 내부에서 HELLO는 0, THIS는 1, IS는 2, TEST는 3이 된 것이다.

 

이는 enum으로 선언된 상수들의 순서가 매우 중요하게 된다는 뜻이다.

 

만약, enum에 선언된 상수들의 순서를 바꾸거나 새로운 상수를 추가하게 된다면 DB에 저장된 값과 입력되는 값이 차이가 나게 된다. 다시 말해, 데이터의 정합성이 손상되는 것이다.

 

누군가 아래와 같이 TestEnum에 A라는 값을 추가했다고 가정하자. DB에는 TEST가 3인데, 이후부터 들어온 데이터는 A가 3이고 TEST가 4다. 이전에 저장된 데이터의 상태가 모두 바뀌게 되는 것이다.

enum class TestEnum {
    HELLO,
    THIS,
    IS,
    A,
    TEST
}

 

그렇다면 이러한 문제를 해결하기 위해선 어떻게 해아할까?

 

EnymType.STRING

@Entity
class TestTable (
    /* pk */
    @Id var no: Int,

    /* 타입 */
    @Column(nullable = false)
    @Enumerated(value = EnumType.STRING)
    var enum: TestEnum,){
}

enum 타입으로 선언된 컬럼에 @Enumerated(value = EnumType.STRING)만 적어도 enum 에 선언된 순서와 상관없이 문자열로 저장된다. DB컬럼도 varchar로 선언되어 이전의 문제를 해결할 수 있게 된다.

 

하지만 해당 방식도 여전히 문제가 남아있다.

 

Enum을 사용할때마다 명시적으로 어노테이션 선언이 필요하며, 숫자만 저장하면 되는 ORDINAL과 다르게 DB 공간의 낭비가 생긴다. 또한, Enum 값을 저장할 때 Enum 자체의 STRING 값이 아닌 다른 값으로 저장하고 싶을 수 있다.

 

Converter

Converter를 사용해 값들을 직접 변환 시켜준다면 이런 문제를 모두 해결할 수 있다.

 

먼저 enum의 value에 DB에 저장하려는 값을 추가한다.

enum class TestEnum (var value : Int) {
    HELLO(0),
    THIS(1),
    IS(3),
    TEST(4)
}

 

Coverter는 AttributeConverter를 구현한 구현체이며, covertToEntityAttribute, covertToDatabaseColumn을 오버라이딩 한다.

  • convertToEntityAttribute : DB에서 읽어온 값을 Entity Attribute에 사용할 값으로 변환
  • covertToDatabaseColumn : Entity Attribute를 DB에 저장할 값으로 변환
@Converter(autoApply = true)
class AssetConverter : AttributeConverter<TestEnum, Int>{
    override fun convertToDatabaseColumn(attribute: TestEnum): Int {
        return attribute.value;
    }

    override fun convertToEntityAttribute(db: Int): TestEnum {
        return TestEnum.entries.first{it.value == db}
    }

}

위는 Coverter 설정을 전역으로 적용한 것이며, autoApply의 기본 값은 false이다.

 

전역으로 사용하고 싶지 않다면 각 엔티티에서 @Converter(converter = AssetConverter::class, attributeName = "enum") 과 같이 선언해주거나, 엔티티의 필드에서 @Converter(converter = Assetconverter::class)로 선언해줄 수 있다.