[Spring] JPA Enum Converter로 DB Entity Mapping하기
Spring JPA에서 Enum을 데이터베이스 컬럼과 매핑하기 위해 Enum Converter 추상 클래스를 사용할 수 있다.
이 방식을 이용하면 Enum을 더 유연하고 안전하게 DB와 연동할 수 있고, 유지보수성이 향상되는 장점이 생김!
일반적인 방식 - 1
Enum Converter 없이 일반적으로 Enum을 사용하는 구조(JPA 기본 방식)는 다음과 같다.
- @Enumerated(EnumType.STRING) : enum 이름("ACTIVE", "INACTIVE")을 DB에 저장
- @Enumerated(EnumType.ORDINAL) : enum의 순서, index값(0, 1)을 DB에 저장
public enum Status {
ACTIVE, INACTIVE;
}
@Entity
public class State {
@Enumerated(EnumType.STRING)
private Status status;
}
이 방식에는 다음과 같은 단점이 존재한다.
- ORDINAL 방식은 숫자 0, 1만 저장되어 의미를 알기 어려움
- Enum 순서가 바뀌면 ORDINAL 값이 꼬여 버그가 발생할 가능성 (유지보수성 저하)
- DB에 저장되는 값이 Enum 이름 그대로라 변경 시 데이터 일괄 수정 필요
- 여러 Enum에서 공통 로직을 사용할 수 없음
- "Y", "N" 등과 같이 비즈니스에 맞는 코드 사용이 어려움
일반적인 방식 - 2
나의 경우 대부분의 프로젝트에서 사용해왔던 방식. 사용자가 Enum에 별도의 값을 넣고 getX()로 사용하는 방법이다.
간단하게 문자열 코드와 Enum을 연결할 수 있기 때문에, 직관적이고 사용이 쉬워 복잡하지 않은 Entity에 사용하기 좋다.
- 필드 getX()로 접근해 사용
public enum Status {
ACTIVE("ACTIVE"),
INACTIVE("INACTIVE");
private final String status;
Status(String status) {
this.status = status;
}
}
public class Example {
Status status = Status.ACTIVE;
System.out.println(status.getStatus()); // "ACTIVE"
}
이 방식에는 다음과 같은 단점이 존재한다.
- "ACTIVE" → "A"로 변경하고 싶다면, DB 값과 매핑이 어긋나는 문제
- 변환 로직을 매번 직접 구현하거나 @Enumerated를 사용해야 하는 문제
- 여러 Enum이 비슷한 패턴을 가진다면 중복이 많아지고 재사용이 어려워지는 문제
JPA Enum Converter
EnumConverter를 구현하여 사용 시 다음과 같은 장점을 기대할 수 있다.
- "Y", "N"과 같이 비즈니스 코드에 맞는 값으로 저장 가능
- Enum 순서 변경이 DB 값에 영향을 주지 않음
- DB 값이 도메인에서 사용되는 코드값과 일치하여 의미를 명확히 전달. 단위 테스트와 디버깅에 유리
- 코드값만 변경하면 되므로 Enum 이름 변경 등에도 유연함
Legacy DB의 JPA Entity Mapping (Enum Converter 편) | 우아한형제들 기술블로그
안녕하세요. 저는 우아한형제들 비즈상품개발팀의 이은경입니다. Legacy DB의 JPA Entity Mapping (복합키 매핑 편)에 이어 저는 DB의 코드값과 Java Enum을 연결해주는 과정에서 유용하게 사용한 @Convert에
techblog.woowahan.com
추상 클래스 구현
import lombok.RequiredArgsConstructor;
import jakarta.persistence.*;
import java.util.Arrays;
import java.util.Objects;
@RequiredArgsConstructor
public abstract class AbstractEnumConverter<T extends Enum<T> & IEnum<E>, E> implements AttributeConverter<T, E> {
private final Class<T> clazz;
@Override
public E convertToDatabaseColumn(T attribute) {
return Objects.isNull(attribute) ? null : attribute.getCode();
}
@Override
public T convertToEntityAttribute(E e1) {
if (Objects.isNull(e1)) return null;
return Arrays.stream(clazz.getEnumConstants())
.filter(e -> e.getCode().equals(e1))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unknown code: " + e1));
}
}
설명
1) 클래스 선언부
@RequiredArgsConstructor
public abstract class AbstractEnumConverter<T extends Enum<T> & IEnum<E>, E> implements AttributeConverter<T, E>
- T extends Enum<T> & IEnum<E> : T는 Enum이면서 IEnum<E> 인터페이스를 구현해야 함
- E : 실제 DB 컬럼에 저장될 값의 타입 (String, Integer 등)
- AttributeConverter<T, E> : JPA에서 Entity ↔ DB 컬럼 변환을 담당하는 인터페이스
2) 필드
private final Class<T> clazz;
- 변환 대상 Enum 클래스의 타입 정보를 담는 필드
- Status.class와 같이 Enum 타입을 전달 받아 사용
3) DB에 저장할 값을 추출하는 메서드
@Override
public E convertToDatabaseColumn(T attribute) {
return Objects.isNull(attribute) ? null : attribute.getCode();
}
- Entity 객체에서 DB에 저장할 값을 추출하는 메서드
- attribute가 null이면 null 반환, 아니면 attribute.getCode()로 코드값 반환
- getCode()는 IEnum<E> 인터페이스에서 제공된 메서드로, DB에 저장될 코드값
IEnum<E>
IEnum<E> 인터페이스는 Enum이 자신의 고유 코드값을 외부에 제공하도록 표준화한 인터페이스.
이 인터페이스를 구현함으로써 Enum마다 일관된 방식으로 코드 값을 제공할 수 있게 됨
- IEnum<E> : enum에서 DB에 저장할 고유 코드값을 추출하는 인터페이스
- 이를 통해 AbstractEnumConverter가 enum ↔ DB 값을 유연하고 재사용성 있게 변환할 수 있음
→ 코드 통일성도 높아지고, 실무에서도 유지보수성이 크게 향상됨!
- 이를 통해 AbstractEnumConverter가 enum ↔ DB 값을 유연하고 재사용성 있게 변환할 수 있음
public interface IEnum<E> {
E getCode();
}
- 제네릭 <E> : enum이 제공할 코드의 타입 (예: String, Integer, 등)
- getCode() : enum 내부 필드를 반환하는 메서드 (DB 저장용으로 사용됨)
- 사용 목적 : 컨버터(AbstractEnumConverter)가 enum의 코드 값을 가져오기 위해 호출
String 타입으로 사용하는 예시
MemberRole이 IEnum<String>을 구현했기 때문에, 컨버터는 getCode()를 통해 "A" 혹은 "U"를 얻을 수 있음.
@Getter
@AllArgsConstructor
public enum MemberRole implements IEnum<String> {
ROLE_ADMIN("A"),
ROLE_USER("U");
private final String code;
@Override
public String getCode() {
return code;
}
}
Integer 타입으로 사용하는 예시
@Getter
@AllArgsConstructor
public enum OrderStatus implements IEnum<Integer> {
ORDERED(1),
SHIPPED(2),
DELIVERED(3);
private final Integer code;
@Override
public Integer getCode() {
return code;
}
}
4) Enum 객체로 변환하는 메서드
@Override
public T convertToEntityAttribute(E e1) {
if (Objects.isNull(e1)) return null;
return Arrays.stream(clazz.getEnumConstants())
.filter(e -> e.getCode().equals(e1))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unknown code: " + e1));
}
- DB에서 읽어온 코드 e1을 Enum 객체로 변환하는 코드
- clazz.getEnumConstants() : Enum의 모든 상술르 가져옴
- 각 Enum의 getCode() 값과 e1을 비교해 일치하는 Enum을 찾고 없으면 "Unknown code" 예외를 던짐
사용 예시
@Getter
@AllArgsConstructor
public enum Status implements IEnum<String> {
ACTIVE("A"), INACTIVE("I");
private final String code;
@Converter(autoApply = true)
static class EnumConverter extends AbstractEnumConverter<Status, String> {
public EnumConverter() {
super(Status.class);
}
}
}
세 가지 방식 비교
1. @Enumerated 방식 | 2. getX() 방식 | 3. Enum Converter 방식 | |
설명 | JPA 기본 방식. Enum 자체를 저장 (STRING / ORDINAL) | 사용자가 Enum에 별도의 값을 넣고 getX()로 사용 | 공통 인터페이스 및 컨버터를 사용하는 확장 가능한 방식 |
저장 방식 | Enum 이름 또는 순서 | 필드(getX())로 접근해 사용 | Enum 필드 값 (getCode()) |
변환 방식 | 자동 | 수동 | 컨버터를 통한 자동 변환 |
재사용성 | 없음 | 낮음 | 매우 높음 |
확장성 | 낮음 | 중간 | 높음 |
예외 처리 | 불가 | 직접 처리해야 함 | 컨버터에서 예외 처리 가능 |
간단한 구현 | ✔️ (매우 쉬움) | ✔️ (쉬움) | ❌ (약간 복잡) |
이름 변경 안정성 | ❌ | ❌ | ✔️ (코드로 매핑되므로) |
순서 변경 안정성 | ❌ (ORDINAL에서 치명적) | ✔️ | ✔️ |
유지보수 | ❌ | ⭕️ | ✔️ |
재사용성 | ❌ | ❌ | ✔️ (추상화 구조) |
DB-도메인 분리 | ❌ | ⭕️ (제한적) | ✔️ (비즈니스-DB 코드 분리) |
권장 상황 | 단순 Enum, 테스트용 | 특정 Enum에서 코드만 필요 시 | 실무, 다수 Enum, DB ↔ 도메인 분리 필요할 때 |