배경
특정 데이터가 DB에서는 숫자로 표현되는 경우가 다수 있다.
예를 들어, 사용자 타입에 대한 정보를 저장할 경우 아래처럼 정의할 수 있다.
값 | 설명 |
1 | 정상회원 |
2 | 탈퇴회원 |
3 | 정지회원 |
4 | 블랙회원 |
해당 데이터를 그대로 사용하게 된다면 아래와 같게 된다.
if (userInfo.getStatus() == 3) {
// Do Something
}
위 코드 같은 경우 직관적으로 해당 값이 무엇을 의미하는지 알 수 없어 DB 명세서를 자주 확인해봐야한다.
만약 이전 코드처럼이 아니라 아래처럼 되어 있다면??
if (userInfo.getStatus().equals(UserStatusType.SUSPENDED)) {
// Do Something
}
단순 숫자 값을 사용했을 때 보다 훨씬 더 직관적이고 명세서를 확인하지 않아도 해당 값이 무엇을 의미하는지 알 수 있다.
이를 위해 관련 값들을 ENUM으로 관리하기로 했다.
하지만, 이내 불편한 점들이 하나둘씩 생겼다.
DB에는 Integer로 저장되어 있는 값을 데이터를 불러올 때 ENUM 타입으로 변경하고 응답을 내릴때는 String 타입의 설명을 내리길 원했다.
이를 위해서는 모든 ENUM 클래스에 ofCode, toCode, toDescription 등등의 함수들이 필요했고 이내 해당 코드들은 반복되는 코드라는 것을 파악할 수 있었다.
해결 방법을 찾던 중에 아래 관련 내용을 정리한 우아한기술블로그에서 원하던 내용을 찾을 수 있었다.
(한번 읽고 오는 것을 추천~!!!)
해결책
해당 글에서 소개한대로 아래 내용들을 구현했다.
1) CommonEnum -> 다른 ENUM 클래스들이 상속받을 공통 ENUM 클래스
2) EnumConverterUtils -> 공통으로 사용할 변환 유틸
3) AttributeConverter -> DB 데이터 값을 ENUM으로, ENUM에서 DB 데이터 값으로 변경해줄 공통 컨버터
그리고 돌려보니 원하는대로 동작하는 것을 확인할 수 있었다~!
다만, 살짝 아쉬운 점이 하나 있었다.
해당 글의 데이터 값 타입이 String으로 되어 있어 ENUM의 code가 String일 경우에 대해서 소개해주었지만 우리의 데이터 값 타입은 Integer이다.
Integer 타입을 다루려면 별도의 Integer 컨버터를 만들어야 하나 고민했는데 이후에 boolean 또는 다른 타입의 데이터를 다루게 될 경우 그 만큼 위 3 객체가 반복적으로 구현되게 될것이 예상 좋은 방법은 아니라고 생각되었다.
따라서, 다양한 데이터 타입을 변환해줄 수 있도록 변환 유틸의 타입을 제네릭 타입을 받도록 수정하였다.
개선
사용하고자 하는 ENUM 클래스에 공통 ENUM 클래스를 상속받는다.
@Getter
@RequiredArgsConstructor
public enum UserStatusType implements CommonEnum<Integer> {
NORMAL(1, "정상"),
DROPPED(2, "탈퇴"),
SUSPENDED(3, "정지"),
BLACKED(4, "없음");
private final Integer code;
private final String description;
}
공통 ENUM 클래스 CommonEnum은 공통으로 사용할 get 함수들을 정의하고 있다.
여기까지는 블로그와 동일하게!
public interface CommonEnum<T> {
T getCode();
String getDescription();
}
ENUM 변환 유틸은 ofCode 함수를 실행할 때 String 타입이 아닌 제네릭 타입 T를 받도록 한다.
이렇게 하면 ENUM의 code가 Integer타입이든 String 타입이든 모두 사용할 수 있게 된다.
toCode도 제네릭 타입 T를 반환하도록 함수 타입을 수정했다.
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class EnumConverterUtils {
public static <E extends Enum<E> & CommonEnum<T>, T> E ofCode(Class<E> enumClass, T code) {
if (code instanceof String && StringUtils.isBlank((String) code)) {
return null;
} else if (code == null) {
return null;
}
return EnumSet.allOf(enumClass).stream()
.filter(e -> e.getCode().equals(code))
.findFirst()
.orElseThrow(() -> new GeneralException(ResponseCode.EMPTY_ENUM_CODE_ERROR));
}
public static <E extends Enum<E> & CommonEnum<T>, T> T toCode(E enumValue) {
if (enumValue == null) {
return null;
}
return enumValue.getCode();
}
}
공통 컨버터도 ENUM 변환 유틸의 변경에 따라 제네릭 타입을 사용하도록 알맞게 수정한다.
@Getter
public class AbstractEnumAttributeConverter<E extends Enum<E> & CommonEnum<T>, T> implements AttributeConverter<E, T> {
private Class<E> targetEnumClass;
private boolean nullable;
private String enumName;
public AstractEnumAttributeConverter(Class<e> targetEnumClass, boolean nullable, String enumName) {
this.targetEnumClass = targetEnumClass;
this.nullable = nullable;
this.enumName = enumName;
}
@Override
public T convertToDatabaseColumn(E attribute) {
if (!nullable && attribute == null) {
throw new IllegalArgumentException(String.format("%s(은)는 NULL로 저장할 수 없습니다.", enumName));
}
return EnumConverterUtils.toCode(attribute);
}
@Override
public E convertToEntityAttribute(T dbData) {
if (!nullable && (dbData instanceof String && StringUtils.isBlank((String) dbData))
|| (dbData instanceof Integer && dbData == null)) {
throw new IllegalArgumentException(String.format("%s(이)가 DB에 NULL 혹은 Empty로 (%s) 저장되어 있습니다.",
enumName, dbData));
}
return EnumConvererUtils.ofCode(targetEnumClass, dbData);
}
}
필요한 ENUM 타입의 컨버터를 아래와 같이 생성하고
@Converter
public class UserStatusTypeConverter extends AbstractEnumAttributeConverter<UserStatusType, Integer> {
public static final String ENUM_NAME = "회원상태";
public UserStatusTypeConverter() {
super(UserStatusType.class, false, ENUM_NAME);
}
}
Entity에 해당하는 칼럼에 컨버터를 Convert 어노테이션으로 달아 놓으면 끄읏~
@Entity
@Table
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private String phone;
@Convert(converter = UserStatusTypeConverter.class)
private UserStatusType status;
....
}
덕분에 ENUM 클래스 별로 ofCode 함수를 생성하는 것을 막을 수 있었고
타입별로 컨버터 클래스를 생성할 필요 없이 제네릭 타입을 이용해 공통 컨버터를 상속받도록 구현할 수 있었다.
👍🏻