개요

개발 중 ENUM 값을 데이터베이스에 저장하니 ENUM 값의 순서, 인덱스가 저장되었다. 나는 상태코드를 기대하면서 저장했는데 숫자가 나와 당황했다. 그런고로 ENUM 의 데이터베이스 저장 및 처리에 대해서 알아보도록 하자.

ENUM 값 저장

@Entity
@Builder
@RequiredArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

    private UserStatus status;
}

public enum UserStatus {
    ACTIVE, INACTIVE, DELETED, BLOCKED, PENDING
}

...
userService.createUser("min9805", "min1925k@gmail.com", UserStatus.ACTIVE);
userService.createUser("test", "test@test.com", UserStatus.DELETED);

...

UserStatus 에 관한 아주 간단한 ENUM 을 생성하였다. createUser 를 통해 ENUM 값을 가진 유저를 저장해보자!

image

위와 같이 ACTIVE 가 0번째이므로 0, DELETE 는 2가 저장되는 것을 확인할 수 있다.

만약 저렇게 숫자로만 표현되어있다면 해당 0, 2 가 어떤 코드를 나타내는 것인지 하나하나 확인해야하며, ENUM 이 많아지게 된다면 한 줄의 데이터를 파악하는데에도 시간이 소요될 것이다.

이를 위해 ENUM 의 상태코드를 데이터베이스에 저장하는 방법이 나타났다.

@Enumerate

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

    @Enumerated(EnumType.STRING)
    private UserStatus status;
}

ENUM to String 으로 값을 저장하는 가장 간편한 형태이다.

image

동일한 코드를 실행시켰을 때, Status 에 상태코드값이 그대로 들어가는 것을 확인할 수 있다.

@Enumerated(EnumType.ORDINAL)

EnumType 에는 Stirng 이외에 ORDINAL 이 존재한다. 이는 처음과 같이 숫자로 ENUM 을 표현하는 방법이다. 하지만 이런 표기법에서는 ENUM 의 순서가 변경되거나 혹은 중간에 ENUM 이 추가되는 등의 일이 발생하면 순서가 기존과 달라져 기존 DB 와 ENUM 의 매핑이 꼬여버리는 심각한 사태가 발생할 수 있다. 이를 방지하고자 위 방법은 지양하도록 하자..!

@Convert

String 타입으로 직접 저장하는 방법에도 한계가 존재했다.

선언한 상수의 이름이 변경되거나, 문자열로 DB에 저장되기에 더 많은 공간이 할당된다는 점이다.

이런 한계를 극복하기 위해 JPA 2.1 부터 도입된 것이 @Convert 이다.

public enum UserStatus {
    ACTIVE(0, "활성화"),
    INACTIVE(1, "비활성화"),
    DELETED(2, "삭제"),
    BLOCKED(3, "차단"),
    PENDING(4, "대기");

    private final int code;
    private final String description;

    UserStatus(int code, String description) {
        this.code = code;
        this.description = description;
    }
}

실제 사용되는 ENUM 은 위의 형태로 이루어져있다. ACTIVE 는 0, “활성화” 등과 동일시 되어 데이터들의 연관 관계를 표현할 수 있다.

이외에도 ENUM 값들은 함수를 가지거나 기능과 책임을 확실하게 구분 지을 수 있는 수단이 된다. 이는 ENUM 활용기 를 참고하면 자세하게 나와있다.

어쩃든 우리는 저 형태의 코드를 데이터베이스에는 숫자로 저장해 이름 변경에 대비하며 공간을 아끼고, 객체에서는 상태코드로 가독성을 높일 것이다.

...
    @Convert(converter = UserStatusConverter.class)
    private UserStatus status;
...

public class UserStatusConverter implements AttributeConverter<UserStatus, Integer> {
    @Override
    public Integer convertToDatabaseColumn(UserStatus attribute) {
        return attribute.getCode();
    }

    @Override
    public UserStatus convertToEntityAttribute(Integer dbData) {
        return UserStatus.of(dbData);
    }
}

User 의 엔티티에 @Convert 를 추가하고 UserStatusConverter 를 작성하자.

  • convertToDatabaseColumn() : enum을 DB에 어떤 값으로 넣을 것인지 정의
  • converToEntityAttribute() : DB에서 읽힌 값에 따라 어떻게 enum과 매칭 시킬 것인지 정의

AttributeConverter 를 상속받아 위 두 가지 메서드를 정의해야한다. 위에서는 아주 단순하게 정의하였다.

실제 테스트코드를 작성시켜보면,

image

데이터베이스에 저장될 때에는 Code, 숫자가 저장된다.

image

실제 객체를 데이터베이스에서 찾아서 받아온다면 해당 “0” 이라는 숫자는 ENUM 으로 매핑되어 객체에 들어가있는 것을 확인할 수 있다!!

위 방법을 사용하면 ENUM 의 순서에 영향을 받지 않으며 상수의 이름을 변경하더라도 데이터베이스 상에서 문제를 일으키지 않는다.

Converter..?

하지만 위의 방법은 모든 ENUM 에 대해서 Converter 를 매번 만들어줘야하는 문제가 있다. ENUM 을 10개 생성하면 10개의 Converter 가 생성된다는 뜻이다!

반복되는 부분? 상속으로 없애보자.

public interface LegacyCommonType {

    int getCode();
    String getDescription();
}

@Getter
public enum UserStatus implements LegacyCommonType{
    ACTIVE(0, "활성화"),
    INACTIVE(1, "비활성화"),
    DELETED(2, "삭제"),
    BLOCKED(3, "차단"),
    PENDING(4, "대기");

    private final int code;
    private final String description;
 
    ...
}

우선 코드의 Get 부분이 필수이므로 interface 를 하나 생성하자.

모든 Status 는 해당 인터페이스를 구현해야한다. (물론 Getter 하나면 충분) 이를 통해 타입을 확실히 통일시킬 수 있다.

public class AbstractLegacyEnumAttributeConverter<E extends Enum<E> & LegacyCommonType> implements AttributeConverter<E, String> {

    private final Class<E> enumClass;

    public AbstractLegacyEnumAttributeConverter(Class<E> enumClass) {
        this.enumClass = enumClass;
    }

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

    @Override
    public E convertToEntityAttribute(String dbData) {
        int code = Integer.parseInt(dbData);
        return LegacyEnumUtils.of(enumClass, code);
    }
}

이후 Status 에 적용시킬 Converter 를 생성하자!

해당 컨버터는 Enum 에 대해서 code, String 으로 변환시키는 동일한 역할을 한다.

이를 위해서는 enum 에서 code 를 통해 String 값으로 변환시켜줄 LegacyEnumUtils 가 필요하다.

public class LegacyEnumUtils {

        public static <E extends Enum<E> & LegacyCommonType> E of(Class<E> enumClass, int code) {
            E[] enumConstants = enumClass.getEnumConstants();
            for (E e : enumConstants) {
                if (e.getCode() == code) {
                    return e;
                }
            }
            throw new IllegalArgumentException("Unknown code: " + code);
        }
}

마지막으로 enum 을 String 으로 변환시켜줄 Utils 를 작성하면 끝!

public class UserStatusConverter extends AbstractLegacyEnumAttributeConverter<UserStatus> {
    public UserStatusConverter() {
        super(UserStatus.class);
    }
}

Status 의 Converter 를 상속받아 간편하게 구현할 수 있다!

image

image

실행 결과 또한 동일하게 나오는 것을 직접 확인할 수 있다!

코드

Github Code

참고

Java ENUM 활용기

Legacy DB의 JPA Entity Mapping (Enum Converter 편)

[Spring] DB 코드값과 Enum의 매핑 방법 @Enumerated vs @Convert

업데이트:

댓글남기기