Spring JPA의 Enum, 안전하게 쓰자!

Gunwoo Kim
FRIP
Published in
5 min readJun 7, 2022

안녕하세요, 프립의 백엔드 엔지니어 김건우입니다. 서버를 개발하다보면 Enum을 쓸 일이 참 많습니다. 가장 흔한 예시로는, 각 도메인 객체의 status를 지정할 때 사용하게 됩니다. 하지만, 대수롭지 않게 사용하는 Enum 은 우리도 모르는 사이에 꽤나 심각한 문제를 초래할 수도 있습니다.아래부터는 프립의 Order Entity 를 기반으로 설명하겠습니다.

주의 사항

본 글은 Kotlin 기반으로 작성되었습니다!

Photo by Justus Menke on Unsplash

얼핏 보았을 때는 문제가 없어 보이고, 실제로도 잘 작동합니다. 하지만 프립에서는 enum class를 위와 같은 방법으로 사용하지 않습니다. 이유는 무엇일까요?

그 이유를 알기 위해서는 우선 Enumerated 에 대해 이해할 필요가 있습니다.

Enumerated

javax.persistence 패키지에는 Enumerated라는 어노테이션이 존재합니다. 해당 어노테이션 파일에는 다음과 같은 주석이 달려 있습니다.

If the enumerated type is not specified or the Enumerated annotation is not used, the EnumType value is assumed to be ORDINAL.

즉, 위의 예시처럼 Enumerated 어노테이션을 사용하지 않았거나, EnumType을 지정해주지 않았을 때는 EnumTypeORDINAL이 설정된다는 것입니다.

  • EnumType.ORDINAL : 엔티티를 DB에 저장할 때, Enum 값의 순서를 DB에 저장

예를 들어 PREPARED0, STAND_BY1, … CANCELED 에는 5라는 값이 매겨지는 것이죠. 위의 예시에서는 Orderstatus 값에 0이 저장되겠죠.

문제는, Enum의 값의 순서가 바뀌면 데이터의 정합성이 손상된다는 것입니다. 예를 들어, 신입 개발자가 들어와서 OrderStateCANCEL_REQUESTED 라는 값을 추가한다고 해봅시다.

Enum값을 추가했을 뿐, 다른 코드를 고치진 않았으니 아무 문제 없을 거라고 생각하겠지만, 실제로는 큰 문제가 있습니다.

방금 전까지는 주문 준비(PREPARED) 상태였던 Order가, 취소 요청이 된 상태로 바뀌었기 때문이죠(CANCEL_REQUESTED). 이렇듯, ORDINAL 값을 그대로 사용하는 것은 위험합니다.

이러한 위험상황을 막기 위해서, EnumType.STRING을 사용합니다.

저 한 줄을 추가하면, OrderState.PREPARED는 순서 상관 없이 “PREPARED”라는 문자열로 저장되기 때문에, 순서에 따라 값이 변경되는 문제를 해결할 수 있습니다.

다만 여전히 몇 가지 문제가 있습니다.

  • 해당 방식은 Enum 값을 엔티티에서 사용할 때마다 명시적으로 어노테이션을 추가해주어야 합니다. 따라서 누락될 가능성이 있습니다.
  • 때로는 Enum 값을 디비에 저장할 때 ORDINALSTRING도 아닌, 원하는 값을 사용하고 싶을 수 있습니다. 예를 들어 COMPLETED10, CANCELED90 과 같은 식으로 말이죠.

이 문제를 해결하기 위한 방법은 바로 Converter 입니다.

Converter

우선 Enum의 value에, DB에 저장하고자 하는 값을 추가해봅시다.

그 뒤, Converter 를 만들어봅시다. 간단히 설명하자면, ConverterAttributeConverter의 구현체이며, 아래 두가지 메소드를 오버라이딩 합니다.

  • convertToEntityAttribute : DB에서 읽어온 값을 엔티티 attribute에 사용할 값으로 변환하기 위한 메소드
  • convertToDatabaseColumn : 엔티티 attribute 값을 DB에 저장할 값으로 변환하기 위한 메소드

아래의 예시는, OrderState 의 Enum 값을 DB에 저장할 때,

  • ORDINAL이 아닌 특정 Int 값으로 저장하되
  • 각 엔티티에서 해당 설정을 하는 것이 아니라, 전역으로 설정 가능하게

만든 예시입니다.

autoApply = true 덕분에 해당 Converter 를 전역으로 사용할 수 있죠.

물론 autoApply 설정의 기본값은 false 이고, Converter 를 전역으로 설정하는 것이 아니라,

  • 각 엔티티에서
  • 각 엔티티의 필드에서

설정할 수 있습니다.

다만, 동일한 Entity의 값을 DB에 다르게 저장해야할 상황 — 이 상황 자체가 그리 좋은 상황은 아닌 듯 합니다 — 이 아니라면 autoApply 설정을 true로 하는 것이 좋을 듯 합니다.

결론

프립에서는 대부분의 경우에 Converter 를 사용합니다. 한 번 만들어 두면 코드량도 부담되지 않고, 프립에서는 대부분의 Enum 값을 Int형으로 저장하기 때문이죠. 하지만 개발이라는 것은 상황에 맞게 다른 방법을 선택해야하기 마련입니다. 아래에 각각의 장단점을 요약해두었으니, 여러분은 상황에 맞게 알맞은 방식을 선택하셨으면 좋겠습니다.

Enumerated

장점

사용이 간단하다.

단점

STRING 값을 저장할 경우, 문자열 자체를 DB에 저장하기 때문에, DB 공간에 낭비가 발생한다.

해당 Enum class가 아니라, 해당 Enum을 사용하는 엔티티에서Enumerated 어노테이션을 사용하기 때문에, 누락의 가능성이 있다.

ORDINALSTRING 두가지 선택지 밖에 없다

Converter

장점

@Converter(autoApply = true) 로 사용하면 전역으로 Converter 를 설정할 수 있고, 누락의 가능성이 적다.

DB에 저장되는 값을 마음대로 커스텀 할 수 있다.

단점

초기 설정을 위한 코드량Enumerated 어노테이션을 사용할 때보다 많다.

전역으로 Converter 를 설정해두면, 해당 설정을 한 눈에 인식하기힘들다.

--

--

Gunwoo Kim
FRIP
Writer for

KAIST CS undergraduate Software Engineer of Toss