본문 바로가기

Framework/Spring Boot

[Spring] JPA Enum Converter 개선기 - 반복 코드 줄이기

배경

특정 데이터가 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 등등의 함수들이 필요했고 이내 해당 코드들은 반복되는 코드라는 것을 파악할 수 있었다. 

해결 방법을 찾던 중에 아래 관련 내용을 정리한 우아한기술블로그에서 원하던 내용을 찾을 수 있었다.

(한번 읽고 오는 것을 추천~!!!)

 

Legacy DB의 JPA Entity Mapping (Enum Converter 편) | 우아한형제들 기술블로그

{{item.name}} 안녕하세요. 저는 우아한형제들 비즈상품개발팀의 이은경입니다. Legacy DB의 JPA Entity Mapping (복합키 매핑 편)에 이어 저는 DB의 코드값과 Java Enum을 연결해주는 과정에서 유용하게 사용

techblog.woowahan.com

 

해결책

해당 글에서 소개한대로 아래 내용들을 구현했다.

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 함수를 생성하는 것을 막을 수 있었고

타입별로 컨버터 클래스를 생성할 필요 없이 제네릭 타입을 이용해 공통 컨버터를 상속받도록 구현할 수 있었다.

 

👍🏻

 

References