섭섭한 개발일지

JPA EnumConverter Entity Mapping (공통 기능) 본문

프로그래밍/JPA

JPA EnumConverter Entity Mapping (공통 기능)

Seop 2024. 7. 15. 18:06

지난 프로젝트에서 JPA에서 사용하는 Enum 값을 Converter를 통해 DB에 값을 텍스트가 아닌 숫자(컬럼 타입은 텍스트)로 저장하는 방식을 도입했었다.

 

이번 프로젝트에서 Entity 매핑과 enum converter화를 진행함에 있어

EnumConverter를 작성하는 데에 불편함을 느꼈고 이를 조금 더 편하게 구성하는 방법이 없을까 생각을 했고 이를 위해 글을 찾아봤다.

 

* 참고 게시글
우아한기술블로그 (https://techblog.woowahan.com/2600/)
기억을 기록으로 (https://velog.io/@rnqhstlr2297/JPA-Enum-Converter%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-Entity-Mapping)

 

 

기존 Converter 사용 방식

Entity

    @Comment("회원 권한")
    @Column(name = "role", columnDefinition = "VARCHAR(50)")
    @Convert(converter = MemberRoleConverter.class)
    private MemberRole role;

 

Enum

@Getter
public enum MemberRole {
    MEMBER("MEMBER", "5"),;

    private final String role;
    private final String roleNumber;

    MemberRole(String role, String roleNumber) {
        this.role = role;
        this.roleNumber = roleNumber;
    }

    public static MemberRole byRole(String role) {
        return Arrays.stream(MemberRole.values())
                .filter(value -> value.getRole().equals(role))
                .findFirst()
                .orElseThrow(() -> BusinessException.builder()
                        .response(HttpResponse.Fail.NOT_FOUND_ROLE)
                        .build());
    }

    public static MemberRole byRoleNumber(String roleNumber) {
        return Arrays.stream(MemberRole.values())
                .filter(value -> value.getRoleNumber().equals(roleNumber))
                .findAny()
                .orElseThrow(() -> BusinessException.builder()
                        .response(HttpResponse.Fail.NOT_FOUND_ROLE_NUMBER)
                        .build());
    }
}

Converter

@Converter
public class MemberRoleConverter implements AttributeConverter<MemberRole, String> {
    @Override
    public String convertToDatabaseColumn(MemberRole attribute) {
        return attribute.getRoleNumber();
    }

    @Override
    public MemberRole convertToEntityAttribute(String dbData) {
        return MemberRole.byRoleNumber(dbData);
    }
}

 

 

이 방식에서 불편한 점은 1개의 Enum마다 Converter를 작성하는 것과

Enum에 로직을 구현해야 하는 것.

 

2가지가 불편함을 느꼈고 찾은 자료를 토대로 구현을 하게 되면 2개가 모두 해결되는 것은 아니지만 Enum에서 중복되는 로직 구현을 통합할 수 있다.

 

 

 

공통 로직 통합 구현 Converter

이 방식을 요약하면 "interface를 통해 enum을 표준화 시켜 공통 기능을 통해 기능을 확장하는 것" 이다.

 

먼저 enum을 표준화하기 위해 interface를 작성한다.

 

CommonEnumType

public interface CommonEnumType {
    /**
     * Enum 공통 getter 기능
     */
    String getDesc();
    int getCode();
}

 

인터페이스를 사용할 enum에 구현한다.

 

Entity

@Getter
public enum MemberRole implements CommonEnumType {
    MEMBER("일반", 1),
    OWNER("사업자", 2),
    ADMIN("관리자", 3),
    ;

    private final String desc;
    private final int code;

    MemberRole(String desc, int code) {
        this.desc = desc;
        this.code = code;
    }
}

 

구현체에서 @Getter 를 사용하게 될 경우 interface에 정의된 메서드를 따로 구현하지 않아도 되니 참고하자.

 

 

다음으로는 공통으로 사용되는 기능을 구현해야 한다.

공통 사용 기능은 desc <-> code 를 상호간 변경해 주는 기능이다.

 

 

CodeEnumValueConverterUtils (공통 기능)

/**
 * desc <-> code util
 * common enum function
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CodeEnumValueConverterUtils {
    // code -> desc
    public static <T extends Enum<T> & CommonEnumType> T ofCode(Class<T> enumClass, Integer code) {
        if (code == null) {
            throw new IllegalArgumentException("this value cannot be null");
        }

        return EnumSet.allOf(enumClass).stream()
                .filter(e -> e.getCode() == code)
                .findAny()
                .orElseThrow(() -> new IllegalArgumentException("not found"));
    }

    // desc -> code
    public static <T extends Enum<T> & CommonEnumType> Integer toCode(T desc) {
        return desc.getCode();
    }
}

 

이 코드는 앞서 중복되던 byRole과 byRoleNumber를 구현한 것이다.

 

 

Converter 기능을 구현하도록 하자

 

AbstractEnumAttributeConverter (공통 기능)

@Getter
@Converter
@RequiredArgsConstructor
public class AbstractEnumAttributeConverter<E extends Enum<E> & CommonEnumType> implements AttributeConverter<E, Integer> {
    private final Class<E> targetEnumClass;

    @Override
    public Integer convertToDatabaseColumn(E attribute) {
        if (attribute == null) {
            throw new IllegalArgumentException("not found");
        }
        return CodeEnumValueConverterUtils.toCode(attribute);
    }

    @Override
    public E convertToEntityAttribute(Integer dbData) {
        return CodeEnumValueConverterUtils.ofCode(targetEnumClass, dbData);
    }
}

여기서는 interface를 통해 표준화 시킨 Enum class를 converter 동작을 위한 구현체를 만든 것이다.

 

 

마지막으로 사용할 converter class를 생성하여 상속만 시키면 된다.

 

MemberRoleConverter

public class MemberRoleConverter extends AbstractEnumAttributeConverter<MemberRole> {
    public MemberRoleConverter() {
        super(MemberRole.class);
    }
}

 

여기까지 구현해 주면 공통기능이 작동이 된다.

 

 

 

 

이 방식의 장점

공통 기능을 반복적으로 구현하는 것이 아닌 상속을 통해 확장함으로 반복적인 코드 작성을 하지 않아도 된다는 장점이 있다.

 

 

이 방식의 단점

enum class에서 사용하는 필드명이 동일해야 한다는 단점이 존재한다.

예제에서는 MemberRole에 관련해서만 나왔지만 Item의 분류와 같이 type으로 분리되는 것을 고려했을 때

각각 role과 type로 분리하고 싶지만 공통 기능을 합치기 위해서는 어쩔 수 없이 통일해야 한다. (따로 분리해서 사용하는 것도 무방하지만..)

 

공통 기능으로 구현하게 될 경우 또 하나의 문제점은 exception 발생 시

role의 오류 메시지와 type의 오류 메시지를 다르게 해야 할 경우.. 메시지 반환에도 문제가 있을 수 있다.

 

 

 

느낀점

enum class 마다 converter class가 생기는 것이 해결되지는 않았지만 공통 기능을 반복적으로 구현하지 않으므로 효율적인 구현과 관리가 될 것 같다.

 

 

 

Comments