현재 모놀리식 구조를 갖고 있는 레거시 서비스를 마이크로서비스 구조로 전환중인 프로젝트를 진행 중이다.
레거시 서비스의 자바 버전은 1.6 이지만 상수들이 static final
로 정의되어 사용되고 있어 이번 마이크로서비스로 전환하면서 해당 상수들을 Enum
으로 관리하기로 하였다.
본사 프로젝트 진행시에는 DB칼럼 사용 방법에 대한 제한이 없어 Enum
의 상수명의 DB에 저장하는 방식(@Enumerated(EnumType.STRING)
)을 사용했었다. 사실 Enum
에 사용방법에 대해 생각을 하지 않았던 것이다.
이번 고객사에서는 전사 테이블에 대한 각 칼럼들의 데이터 길이, 타입, 명명규칙, 등이 존재했고 프로젝트 내부적으로 필드의 명명 규칙은 필드명으로 해당 속성의 용도를 알 수 있도록 풀네임을 지향했다.
하지만 고객사의 전사 테이블은 코드를 코드 형태로 다음과 같은 데이터 형태를 갖고 있었다.
요구사항은 소스상에서는 Enum
에 정의된 상수명으로 해당 상수가 어떤 상수인지 알 수 있어야 하고 해당 상수에는 저장될때에는 상수의 Code
가 저장되어야 하며 해당 Code
의 값도 가지고 있어야 했다.
이러한 요구사항은 충분히 발생 할 수 있는 것으로 생각되어 찾아 보던중 역시나 나의 요구사항과 동일한 경우를 경험하신 우아한형제들의 이은경님의 글(Legacy DB의 JPA Entity Mapping (Enum Converter 편))을 찾을 수 있었다.
@Convert
에 AttributeConverter<X,Y>
인터페이스 구현 클래스를 지정하여 구현한 메소드 convertToDatabaseColumn
(field -> DB), convertToEntityAttribute
(DB -> field)값으로 저장, 조회하는 방식이다.
우선 Enum
에 상수명
과 같이 관리되어야 할 항목이 Code
, Value
가 존재하여 CodeValue
라는 interface
를 생성하였으며 해당 인터페이스에는 code
와 value
를 반환하는 getter
를 정의하였다.
public interface CodeValue {
String getCode();
String getValue();
}
Enum
은 CodeValue
인터페이스를 구현하여 정의하도록 하였다.
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;
}
}
이렇게 정의된 Enum
은 JPA 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
가 발생 할 것으로 생각된다.