Java Enum 활용하기1 - AttributeConverter
Java Enum을 활용한 방법 중 첫 번째,
AttributeConverter 인터페이스를 구현한 CustomConverter를 만들어 DB에는 Enum의 legacyCode 값으로 데이터를 저장하고, Java에서는 DB에 legacyCode 값으로 저장된 데이터를 다시 Enum으로 변환해서 사용하는 방법입니다.
AttributeConverter의 기본적인 사용 방법 예시
@Getter
public enum UserState {
NORMAL("정상", 1),
SUSPENSION("정지", 2),
WITHDRAWAL("탈퇴", 3);
private String desc;
private Integer legacyCode;
UserState(String desc, Integer legacyCode) {
this.desc = desc;
this.legacyCode =legacyCode;
}
public static UserState ofLegacyCode(Integer legacyCode) {
return Arrays.stream(UserState.values())
.filter(e -> e.getLegacyCode().equals(legacyCode))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(String.format("상태 코드에 legacyCode : [%s]가 존재하지 않습니다.", legacyCode)));
}
}
(사용자의 상태 값을 나타내는 Enum인 UserState)
legacyCode의 경우에는 @Getter 롬복 어노테이션에 의해 구현된 getLegacyCode() 메서드로 가져올 수 있고, legacyCode로 Enum 클래스를 가져올 때는 구현된 ofLegacyCode() 메서드를 통해 가져올 수 있습니다.
@Converter
public class UserStateConverter implements AttributeConverter<User1State, Integer> {
@Override
public Integer convertToDatabaseColumn(User1State attribute) {
return attribute.getLegacyCode();
}
@Override
public UserState convertToEntityAttribute(Integer dbData) {
return UserState.ofLegacyCode(dbData);
}
}
(AttributeConverter를 상속받은 UserStateConverter)
각 Converter는 AttributeConverter 인터페이스의 convertToDatabaseColumn(Enum -> DB 데이터) 메서드와 convertToEntityAttribute(DB 데이터 -> Enum) 메서드를 구현합니다.
@Entity
public class User {
...
@Convert(converter = UserStateConverter.class)
private UserState userState;
...
}
(UserState Enum Class를 사용하는 User Entity)
위에서 구현한 Converter를 적용하는 방법은 Entity의 필드를 Enum으로 정의하고 해당 필드의 상단에 @Convert(converter = ) 어노테이션을 붙이는 방법으로 사용할 수 있습니다.
(@Convert 어노테이션은 클래스 레벨이나 글로벌 레벨에서의 설정도 가능합니다.)
***
위와 같은 방식을 통해 Enum의 legacyCode를 자동으로 DB에 저장할 수 있으며, Java에서 사용할 때 역시 저장된 legacyCode를 자동으로 Enum으로 변환하여 사용할 수 있게 되는 것인데요.
문제는 실제 프로젝트에서 Enum 클래스가 한두 개가 아니라는 점입니다.
따라서 각각의 Enum 클래스마다 EnumConverter를 구현해야 하며, Enum 내부적으로도 ofLegacyCode() 메서드를 반복적으로 구현해야 하는 번거로움이 생기게 됩니다.
이런 문제를 해결하기 위해서 아래 방법을 적용할 수 있는데요.
AttributeConverter의 구현 클래스를 Enum 클래스에 공통으로 적용하기
public interface EnumMapperType {
String getCode();
String getDesc();
Integer getLegacyCode();
}
각 Enum 클래스는 공통으로 사용되는 getter 메서드를 가진 EnumMapperType 인터페이스를 구현하도록 합니다.
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LegacyCodeConverterUtils {
public static <T extends Enum<T> & EnumMapperType> T ofLegacyCode(Class<T> enumClass, Integer legacyCode) {
if (legacyCode == null) {
return null;
}
return EnumSet.allOf(enumClass).stream()
.filter(v -> v.getLegacyCode().equals(legacyCode))
.findAny()
//TODO: Exception 생성 후 처리
.orElseThrow(() -> new IllegalArgumentException(String.format("enum=[%s], legacyCode=[%s]가 존재하지 않습니다.", enumClass.getName(), legacyCode)));
}
public static <T extends Enum<T> & EnumMapperType> Integer toLegacyCode(T enumValue) {
if (enumValue == null) {
return null;
}
return enumValue.getLegacyCode();
}
}
그리고 위와 같이 legacyCode를 Enum 클래스로 바꿔주는 기능과 Enum 클래스를 legacyCode로 바꿔주는 공통된 작업을 하나의 클래스에서 하기 위한 LegacyCodeConverterUtils Class를 생성합니다.
/*
AccessLevel.PRIVATE
NoArgumentConstructor의 접근 제어자를 private으로 설정하여 기본 생성자를 통한 인스턴스를 생성할 수 없도록 설정
*/
@Converter(autoApply = true)
public abstract class LegacyCodeConverter<E extends Enum<E> & EnumMapperType> implements AttributeConverter<E, Integer> {
private Class<E> targetEnumClass;
private boolean nullable;
private String enumName;
public LegacyCodeConverter(Class<E> targetEnumClass, boolean nullable, String enumName) {
this.targetEnumClass = targetEnumClass;
this.nullable = nullable;
this.enumName = enumName;
}
@Override
public Integer convertToDatabaseColumn(E attribute) {
if (!nullable && attribute == null) {
throw new IllegalArgumentException(String.format("%s(은)는 NULL로 저장할 수 없습니다.", enumName));
}
return LegacyCodeConverterUtils.toLegacyCode(attribute);
}
@Override
public E convertToEntityAttribute(Integer dbData) {
if (!nullable && dbData == null) {
throw new IllegalArgumentException(String.format("%s(이)가 DB에 NULL 혹은 Empty로 저장되어 있습니다.", enumName));
}
return LegacyCodeConverterUtils.ofLegacyCode(targetEnumClass, dbData);
}
}
(AttributeConverter를 상속한 공통 Converter)
이전에 각각의 Converter에서 AttributeConverter 인터페이스를 상속받아 구현하던 부분을 공통 Converter에 구현하여 처리하게 됩니다. (LegacyCodeConverterUtils를 사용하기 때문에 각각의 Enum에서 구현하던 ofLegacyCode()를 구현하지 않을 수 있습니다.)
공통 Converter 클래스인 LegacyCodeConverter는 타깃이 되는 Enum 클래스와 로그 및 에러 메시지에 사용될 Enum name, 그리고 null 허용 여부를 확인하기 위한 nullable 필드를 가지고 있습니다.
public enum UserState implements EnumMapperType {
...
public static class UserStateConverter extends LegacyCodeConverter<UserState> {
private static final String ENUM_NAME = "사용자 상태";
public UserStateConverter() {
super(UserState.class, false, ENUM_NAME);
}
}
}
모든 Converter(Enum 내부에서 static class로 생성)는 공통 Converter 클래스인 LegacyCodeConverter를 상속하는데, 생성자를 통해 null이 들어올 수 있는지와 Enum의 이름을 지정해줍니다.
Converter 클래스를 각각 만들어줘야 하는 번거로움은 남아있지만, 구현된 공통 컨버터를 상속하여 사용함으로써 Enum이 늘어날 때마다 각각 구현했어야 하는 부분들이 줄어드는 장점이 있습니다.
***
이 기능을 구현하면서 DB에 legacyCode로 Enum 데이터를 저장했을 때의 이점이 무엇이 있을까 생각해보고, 또 주변 개발자들에게도 물어봤는데요.
기존의 시스템에서 DB에 code 값을 저장하여 사용하고 있는 경우에 이 방식을 적용하면 Java 내부적으로 Enum을 편하게 쓸 수 있을 것 같지만, 그런 것이 아니라면 Enum 자체를 쓰는 것이 직관적이며 성능상으로도 큰 문제가 없어 Enum 값 자체를 쓰는 것을 선호한다는 의견도 있었습니다.
/*
legacyCode로 저장했을 때 DB에 저장되는 용량이 조금 줄어드는 이점이 있을 것 같은데, Enum 값으로 조회하는 경우에도 legacyCode가 빠르지 않을까 생각했지만 DB indexing을 적용하면 Enum 값을 그대로 사용하는 경우와 큰 차이가 없다고 합니다.
*/
결론적으로 이러한 기능이 적용될 수 있다는 것만 알아두고 시스템의 상황에 따라 필요한 경우에 적용해볼 수 있는 기능일 것 같습니다.
이어지는 Java Enum 활용하기 2편에서는 ConverterFactory 인터페이스를 활용하여 외부에서 legacyCode로 들어오는 요청을 내부에서는 Enum으로 받을 수 있게 하는 방법에 대해서 살펴보겠습니다.
< 함께 보면 좋은 자료 >