⚙ Framework/Spring-스프링

[Spring] JPA Enum Converter로 DB Entity Mapping하기

mxnxeonx 2025. 3. 25. 11:01
728x90
728x90

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 값을 유연하고 재사용성 있게 변환할 수 있음
      → 코드 통일성도 높아지고, 실무에서도 유지보수성이 크게 향상됨!
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 ↔ 도메인 분리 필요할 때
728x90
320x100