AttributeConverter 활용
Album 프로젝트를 진행하면서, 한 가지 문제점이 생겼다.
enum 값의 특정 순서대로 데이터를 정렬하여 조회하고 싶은데, JPA에서는 이러한 기능을 직접적으로 제공하지 않는 것이 었다.
예를 들어 FIRST, NORMAL, LOCKED, WITHDRAW라는 enum 값들이 있다.
데이터베이스 컬럼에는 해당 값들이 그대로 저장되어있는 상황이다.
JPA에서 직접 제공하는 기능으로는 해당 컬럼을 기준으로 오름차순 또는 내림차순으로 정렬만 할 수 있다.
NORMAL > LOCKED > WITHDRAW > FIRST 라는 특정한 순서대로 정렬할 수 없는 것이다.
이러한 문제를 해결할 수 있는 방법을 찾아보다가 AttributeConverter라는 기능을 알게되었다.
AttributeConverter
AttributeConverter는 자바 어플리케이션과 DB 사이에서 값을 변환해주는 기능을 제공한다.
예를 들어 NORMAL이라는 enum 값을 DB에는 "001"이라는 값으로 저장할 수 있고,
DB에서 조회한 "001"이란 값을 자바 어플리케이션에서는 NORMAL이라는 enum 값으로 사용할 수 있는 것이다.
실제 적용
EnumType 정의 및 구현
모든 enum에 AttributeConverter를 적용하기 위해, enum들이 구현할 EnumType을 정의한다.
public interface EnumType {
String getDescription();
String getCode();
}
그리고 AttributeConverter를 적용할 enum들이 EnumType을 구현한다.
이번 게시글에서는 Member 엔티티에 사용되는 enum인 MemberStatus를 기준으로 설명하겠다.
그리고 DB에 저장될 필드와 값을 정의한다. 이번 게시글에서는 code 값을 DB에 저장할 것이다.
@Getter
@AllArgsConstructor
public enum MemberStatus implements EnumType {
NORMAL("정상", "001"),
LOCKED("정지", "002"),
WITHDRAW("탈퇴", "003"),
FIRST("첫 로그인", "004");
private final String description;
private final String code;
@JsonCreator
public static MemberStatus from(@JsonProperty("memberStatus") String val){
for(MemberStatus memberStatus : MemberStatus.values()){
if(memberStatus.name().equals(val)){
return memberStatus;
}
}
return null;
}
}
EnumConvertorUtil 구현
실제로 DB에 저장된 값을 enum으로, enum을 DB에 저장될 값으로 변환하는 기능을 수행할 EnumConvertorUtil을 구현한다.
toValue 메소드는 DB에서 가져온 code를 자바에서 사용할 enum으로 변환해주는 메소드이다.
- toVaule 메소드는 제네릭 타입 T를 반환하는 메소드이다. T는 enum 클래스이면서 EnumType을 구현한 클래스여야한다.
- 파라미터로 Class<T> 타입인 enumClass와 String 타입인 code를 사용한다.
- enumClass의 enum을 순회하면서, code와 일치하는 값을 가진 enum을 반환한다.
toCode는 자바에서 사용하는 enum을 DB에 저장할 code로 변환해주는 메소드이다.
- toCode 메소드는 String 타입의 code를 반환하는 메소드이다.
- toCode 메소드 또한 enum 클래스이면서 EnumType을 구현한 제네릭 타입 T를 사용한다.
- 파라미터로 T 타입인 enumValue를 사용한다.
- enumValue의 code 값을 반환한다.
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class EnumConvertorUtil {
public static <T extends Enum<T> & EnumType> T toValue(Class<T> enumClass, String code) {
if (code == null) {
return null;
} else {
return EnumSet.allOf(enumClass).stream()
.filter(v -> v.getCode().equals(code))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(
String.format("enum=[%s], legacyCode=[%s]가 존재하지 않습니다.",
enumClass.getName(), code)));
}
}
public static <T extends Enum<T> & EnumType> String toCode(T enumValue) {
if (enumValue == null) {
return null;
}
return enumValue.getCode();
}
}
AttributeConverter의 구현체 EnumConvertor
AttributeConverter는 인터페이스이기 때문에 구현체가 필요하다.
EnumConvertor는 AttributeConverter의 구현체이다.
EnumConvertor는 enum 클래스이면서 EnumType을 구현하는 클래스인 제네릭 타입 T를 사용한다.
- 생성자의 파라미터로 Class<T> 타입의 enumClass를 사용한다.
- 그리고 해당 파라미터의 값으로 초기화되는 enumClass 필드를 가진다.
그리고 AttributeConverter는 T를 첫 번째 제네릭 타입으로, String을 두 번째 제네릭 타입으로 사용한다.
- convertToDatabaseColum은 T 타입의 enumValue를 파라미터로 사용하며, EnumConvertorUtil.toCode를 통해 enumValue를 매핑된 code로 변환하여 반환한다.
- convertToEntityAttribute는 String 타입의 dbData를 파라미터로 사용하며, EnumConverterUtil.toValue를 통해 dbData와 일치하는 code와 매핑된 enum 상수를 반환한다.
@Converter(autoApply = true)는 @Converter를 글로벌 적용할 수 있게 해준다.
EnumConvertor가 구현하고 있는 AttributeConverter의 첫 번째 제네릭 타입인 T와 일치하는 타입의 엔티티 필드에 @Converter를 따로 적용하지 않아도, 자동으로 적용된다.
@Converter(autoApply = true)
public abstract class EnumConvertor<T extends Enum<T> & EnumType> implements AttributeConverter<T, String> {
private Class<T> enumClass;
public EnumConvertor(Class<T> enumClass) {
this.enumClass = enumClass;
}
@Override
public String convertToDatabaseColumn(T enumValue) {
if (enumValue == null) {
return null;
} else {
return EnumConvertorUtil.toCode(enumValue);
}
}
@Override
public T convertToEntityAttribute(String dbData) {
if (dbData == null) {
return null;
} else {
return EnumConvertorUtil.toValue(enumClass, dbData);
}
}
}
MemberStatusConvertor 구현
이제 enum 클래스인 MebmerStatus 내부에 EnumConvertor를 상속하는 MemberStatusConvertor를 구현한다.
EnumConvertor의 T는 MemberStatus로 지정한다.
그리고 기본 생성자 내부에 EnumConvertor의 생성자가 MemberStatus.class를 파라미터로 사용하도록 구현한다.
@Getter
@AllArgsConstructor
public enum MemberStatus implements EnumType {
NORMAL("정상", "001"),
LOCKED("정지", "002"),
WITHDRAW("탈퇴", "003"),
FIRST("첫 로그인", "004");
private final String description;
private final String code;
public static class MemberStatusConvertor extends EnumConvertor<MemberStatus> {
public MemberStatusConvertor() {
super(MemberStatus.class);
}
}
@JsonCreator
public static MemberStatus from(@JsonProperty("memberStatus") String val){
for(MemberStatus memberStatus : MemberStatus.values()){
if(memberStatus.name().equals(val)){
return memberStatus;
}
}
return null;
}
}
결과
이제 DB에는 MemberStatus의 enum 상수들과 매핑된 code 값들이 저장된다.
그리고 데이터를 조회해 올 때는 DB의 code 값이 enum 상수로 변환된다.
이것을 통해 특정 enum 상수 순서대로 정렬하여 데이터를 조회할 수 있다.
각 enum 상수에 정렬하고자 하는 순서대로 code 값을 부여하고, JPA를 통해 오름차순 또는 내림차순으로 조회하면 된다.
예를 들어 현재 FIRST("004"), NORMAL("001"), LOCKED("002"), WITHDRAW("003")으로 enum 상수와 code가 매핑되어있다.
따라서 오름 차순으로 정렬하여 조회하면 NORMAL > LOCKED > WITHDRAW > FIRST 순으로 정렬된 데이터를 조회할 수 있다.