JPA Enum Type 적용기~

YouMakeMeSmile·2021년 5월 19일
9

현재 모놀리식 구조를 갖고 있는 레거시 서비스를 마이크로서비스 구조로 전환중인 프로젝트를 진행 중이다.

레거시 서비스의 자바 버전은 1.6 이지만 상수들이 static final로 정의되어 사용되고 있어 이번 마이크로서비스로 전환하면서 해당 상수들을 Enum으로 관리하기로 하였다.

본사 프로젝트 진행시에는 DB칼럼 사용 방법에 대한 제한이 없어 Enum의 상수명의 DB에 저장하는 방식(@Enumerated(EnumType.STRING))을 사용했었다. 사실 Enum에 사용방법에 대해 생각을 하지 않았던 것이다.
이번 고객사에서는 전사 테이블에 대한 각 칼럼들의 데이터 길이, 타입, 명명규칙, 등이 존재했고 프로젝트 내부적으로 필드의 명명 규칙은 필드명으로 해당 속성의 용도를 알 수 있도록 풀네임을 지향했다.

하지만 고객사의 전사 테이블은 코드를 코드 형태로 다음과 같은 데이터 형태를 갖고 있었다.

요구사항은 소스상에서는 Enum에 정의된 상수명으로 해당 상수가 어떤 상수인지 알 수 있어야 하고 해당 상수에는 저장될때에는 상수의 Code가 저장되어야 하며 해당 Code의 값도 가지고 있어야 했다.


이러한 요구사항은 충분히 발생 할 수 있는 것으로 생각되어 찾아 보던중 역시나 나의 요구사항과 동일한 경우를 경험하신 우아한형제들의 이은경님의 글(Legacy DB의 JPA Entity Mapping (Enum Converter 편))을 찾을 수 있었다.

@ConvertAttributeConverter<X,Y> 인터페이스 구현 클래스를 지정하여 구현한 메소드 convertToDatabaseColumn(field -> DB), convertToEntityAttribute(DB -> field)값으로 저장, 조회하는 방식이다.


우선 Enum상수명과 같이 관리되어야 할 항목이 Code, Value가 존재하여 CodeValue 라는 interface를 생성하였으며 해당 인터페이스에는 codevalue를 반환하는 getter를 정의하였다.

public interface CodeValue {
    String getCode();
    String getValue();
}

EnumCodeValue 인터페이스를 구현하여 정의하도록 하였다.

public enum Gender implements CodeValue{
    MAN("M", "남성"),
    WOMAN("W", "여성");
    
    private String code;
    private String value;

    Gender(String code, String value) {
        this.code = code;
        this.value = value;
    }
    @Override
    public String getCode() {
        return code;
    }
    @Override
    public String getValue() {
        return value;
    }
}

이렇게 정의된 EnumJPA Entity에 필드로 정의하여 해당 필드가 저장될 경우에는 해당 상수의 Code 값이 저장되고 조회될 경우에는 Code로 해당 상수를 반환해야 하도록 Converter 클래스를 구현해야 한다.

public class GenderConverter implements AttributeConverter<Gender, String> {

    @Override
    public String convertToDatabaseColumn(Gender attribute) {
        return attribute.getCode();
    }

    @Override
    public Gender convertToEntityAttribute(String dbData) {
        return EnumSet.allOf(Gender.class).stream()
                .filter(e->e.getCode().equals(dbData))
                .findAny()
                .orElseThrow(()-> new NoSuchElementException());
    }
}
@Entity
@Table
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Sample {
    @Id
    private String id;
    @Convert(converter = GenderConverter.class)
    private Gender gender;
}

위와 같이 설정 후 다음의 로직으로 저장하였을 경우 Enum에 정의한 상수의 Code 값이 정상적으로 저장되는 것을 확인 할 수 있으며 소스에서도 상수명을 사용하여 해당 상수가 어떤 값을 나타내는지 추측하기 쉽다.

    public List<Sample> test(){
        sampleRepository.save(new Sample(UUID.randomUUID().toString(), Gender.MAN));
        sampleRepository.save(new Sample(UUID.randomUUID().toString(), Gender.WOMAN));

        return sampleRepository.findAll();
    }


하지만 Enum 증가에 따라 Converter 클래스의 생성도 반드시 이루어져야 하며 이러한 Converter 클래스의 구현 메소드의 내용은 중복이 일어나게된다.
Converter 클래스의 생성은 어쩔수 없지만 메소드 내용 중복은 하나의 부모Converter 클래스를 상속받아 관리 가능한다.

public class CodeValueConverter<E extends Enum<E> & CodeValue> implements AttributeConverter<E, String> {

    private Class<E> clz;

    CodeValueConverter(Class<E> enumClass){
        this.clz = enumClass;
    }

    @Override
    public String convertToDatabaseColumn(E attribute) {
        return attribute.getCode();
    }

    @Override
    public E convertToEntityAttribute(String dbData) {
        return EnumSet.allOf(clz).stream()
                .filter(e->e.getCode().equals(dbData))
                .findAny()
                .orElseThrow(()-> new NoSuchElementException());
    }
}
@Converter(autoApply = true)
public class GenderConverter extends CodeValueConverter<Gender> {
    GenderConverter() {
        super(Gender.class);
    }
}

위와 같이 CodeValueConverter 클래스를 상속받아 생성자에 해당 Enum을 넘기는 방식으로 조금이나마 쉽게 관리가 가능하게되며 @Converter(autoApply = true) 설정을 통해서는 Entity@Convert 명시없이도 해당 Converter 클래스가 적용된다.


위의 방법을 적용하여 레거시의 상수 관리를 하기로 하였지만 Enum에 대응되는 Converter 클래스가 반드시 존재해야 한다는 아쉬운 점이 존재한다. 어떻게든 제네릭을 이용하여 하나의 Converter 클래스만을 구현하여 사용하는 것을 시도하고 찾아봤으나 결국은 찾지 못했다.

추가적으로 다음 이슈가 발생할 수 있는 문제는 레거시는 프론트에서 상수를 Code 값으로 요청을 보내며 서버에서 응답 또한 Code 값을 응답하였다. 하지만 Jackson은 기본적으로 Enum 타입의 객체 변환은 Enum상수명으로 하며 응답도 상수명을 반환한다. 즉 요청도 Man, Woman 으로 전달되어야 하며 응답 또한 Man, Woman으로 응답된다.

다음 요구사항은 아마도 Json Serialize, Deserialize가 발생 할 것으로 생각된다.

소스

profile
알고싶고 하고싶은게 많은 주니어 개발자 입니다.

0개의 댓글