MyBatis에서 Enum 쓰는 법

Posted by 서명현 on September 18, 2023

안녕하세요, 서명현입니다. 🤚

저희 프로젝트는 예약 상태나 주문 상태와 같이 비슷한 특성을 가진 상태나 객체를 Enum 타입으로 관리하고 있습니다.
데이터베이스에 Enum 타입을 정수로 저장하면서 느끼게 된 한계점과 개선 방안에 대해 공유하고자 합니다.

목차 소개

이번 포스팅은 다음과 같은 순서로 진행됩니다.

  1. 우리 팀이 정수 저장 방식을 선택한 이유
  2. Enum 값의 변경이 일어나게 된다면?
  3. Enum 타입의 저장 방식을 결정하는 요소
  4. 정수 저장 방식을 이미 사용하고 있다면 TypeHandler를 사용하자

1. 우리 팀이 정수 저장 방식을 선택한 이유

그동안의 프로젝트에서 Enum 타입을 데이터베이스에 저장할 때, 유지보수 측면에서 상수 그대로 저장하는 방식을 사용했는데요.
이번에는 팀원의 의견에 따라 성능과 쿼리의 간결함을 고려하여 정수 저장 방식을 사용해보기로 결정하였습니다.

1. 정수로 저장하는 방식

Enum 타입을 데이터베이스에서 저장할 때, 정수로 변환하여 저장하는 방식입니다.
주문 상태를 나타내는 Enum 타입이 있다고 가정하겠습니다. Enum 타입의 상수를 정수로 변환하여 저장하는 방식은 다음과 같습니다.

1
2
3
public enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
1
2
3
4
5
6
7
8
9
10
SELECT *
FROM day
WHERE day BETWEEN 0 AND 3; -- 정수로 조회하는 경우

SELECT *
FROM day
WHERE day = '월'
   OR day = '화'
   OR day = '수'
   OR day = '목'; -- 문자열로 조회하는 경우

문자열로 조회하는 방식보다 성능상 더 빠르고, 비교할 때 더 간결해진다는 점이 장점이 있습니다.
하지만 정수로 저장하는 방식은 데이터베이스에 저장된 값이 어떤 상태를 나타내는지 쉽게 알 수 없다는 단점이 있습니다.

1
2
3
public enum OrderStatus {
    ORDER, CANCEL
}
1
2
Insert into order (order_status)
values (0);

이러한 방식은 정수가 어떤 값을 나타내는지 쉽게 알 수 없고, 데이터베이스에 저장된 값을 조회할 때 Enum 타입의 상수를 정수로 변환하여 조회하기 때문에 Enum 타입의 상수를 사용할 수 없습니다.

2. 상수 그대로 저장하는 방식

Enum 타입을 데이터베이스에 저장할 때, Enum 타입의 상수 그대로 저장하는 방식입니다.

1
2
3
public enum OrderStatus {
    ORDER, CANCEL
}
1
2
Insert into order (order_status)
values ('ORDER');

이러한 방식은 Enum 타입의 상수 그대로 저장하기 때문에 데이터베이스에 저장된 값이 어떤 상태를 나타내는지 쉽게 알 수 있습니다.
또한, 데이터베이스에 저장된 값을 조회할 때, Enum 타입의 상수 그대로 조회하기 때문에 Enum 타입의 상수 그대로 사용할 수 있습니다.

2. Enum 값의 변경이 일어나게 된다면?

정수 저장 방식을 사용하면서, 정수 저장 방식은 값의 변경에 취약하다는 것을 느끼게 되었습니다.
예약 상태를 나타내는 Enum 타입이 있다고 가정하겠습니다.

1
2
3
public enum ReservationStatus {
    CANCELED(-1), WAITING(0), COMPLETED(1)
}

기존 요구 사항에서 갑자기 예약 상태에 대한 요구 사항이 추가된다면, 어떻게 해야할까요?
예약 상태에 대한 요구 사항이 추가된다면, Enum 타입의 상수를 추가해야 합니다.

1
2
3
public enum ReservationStatus {
    CANCELED(-1), WAITING(0), CONFIRMED(1), COMPLETED(2) // 예약 확인 상태 추가
}

Enum 타입의 상수를 추가하게 되면, 기존에 데이터베이스에 저장되어 있는 값들을 모두 변경해주어야 하고, 새로 추가된 상수를 사용하는 코드들을 모두 변경해주어야 합니다.
이러한 작업은 굉장히 번거롭고, 실수할 가능성이 높을 것입니다.

애플리케이션 개발 측면에서도, 값을 조회할 때 Enum 타입을 정수로 변환하여 조회하는 작업이 필요하고, 값이 어떤 상태를 나타내는지 알기 어려우며, 가독성이 낮고 추후 변경을 위해 추적하기에도 어렵다는 단점이 존재합니다.

이러한 문제점은 Enum 타입의 상수를 그대로 저장하는 방식을 사용하면 극복할 수 있습니다.
새로운 상수가 추가되더라도, 데이터베이스에 저장된 값이 어떤 상태를 나타내는지 쉽게 알 수 있고, Enum 타입의 상수를 그대로 사용하기 때문에 Enum 타입의 상수를 사용하는 코드를 변경할 필요가 없습니다.

3. Enum 타입의 저장 방식을 결정하는 요소

두 가지 방식 중 무조건 어느 방법이 낫다 라기보다는 프로젝트 특징이나 상황에 따라 적절하게 선택하여 사용해야 할 것입니다.

변경되는 요구 사항에 따라 유연하게 대처할 수 있는 설계를 위해서라면 Enum 타입의 상수를 사용하고, 변경할 일이 없고 성능적인 측면을 고려한다면 정수 저장 방식을 사용하는 것이 좋을 것입니다.

예를 들면, 예약/주문 상태와 같이 변경되는 요구 사항이 많은 경우에는 상수 저장 방식을 선택하고, 요일이나 성별과 같이 변경되는 요구 사항이 없는 경우에는 정수 저장 방식을 선택하는 것이 좋을 것 같습니다.

생각해보니 김영한 님의 JPA 강의에서도 Enum 타입의 상수를 그대로 저장하는 방식을 권장하고 있었던게 생각나네요ㅎㅎ 성능적인 측면을 고려하여 정수로 저장하기보다는 변경에 유연한 설계를 하는 것이 더 중요하기 때문입니다.

4. 정수 저장 방식을 이미 사용하고 있다면 TypeHandler를 사용하자

저희 프로젝트는 짧은 기간 안에 개발을 완료해야 했기 때문에, Enum 타입의 상수를 그대로 저장하는 방식으로 변경하기에는 시간적인 여유가 없었습니다. 이런 이유로 정수 저장 방식을 사용하면서, Enum 타입의 상수를 그대로 사용할 수 있도록 TypeHandler를 사용하였습니다.

TypeHandler 란?

데이터베이스 컬럼과 자바 객체의 필드 간에 데이터를 매핑하는 역할을 담당하는 컴포넌트입니다.
MyBatis 자체에 기본 제공 타입 핸들러가 포함되어 있으며 varchar, int, long, boolean 등의 기본 타입을 매핑이 가능한데요. TypeHandler를 직접 구현하여 데이터베이스 컬럼과 자바 객체의 필드 간에 데이터를 매핑하는 방법을 사용하여 매번 Enum 타입으로 변환하지 않고도 Enum 타입을 사용할 수 있도록 할 수 있습니다.

다음은 TypeHandler를 구현한 예시입니다.
예약 상태에 대한 Enum 클래스와 데이터베이스 정수 필드를 매핑해주는 TypeHandler입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@MappedJdbcTypes(JdbcType.INTEGER)
@MappedTypes(ReservationStatus.class)
public class ReservationStatusTypeHandler extends BaseTypeHandler<ReservationStatus> {

    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement,
                                    int i,
                                    ReservationStatus status,
                                    JdbcType jdbcType) throws SQLException {
        preparedStatement.setInt(i, status.getCode());
    }

    @Override
    public ReservationStatus getNullableResult(ResultSet resultSet, String s) throws SQLException {
        int code = resultSet.getInt(s);
        return ReservationStatus.fromCode(code);
    }

    @Override
    public ReservationStatus getNullableResult(ResultSet resultSet, int i) throws SQLException {
        int code = resultSet.getInt(i);
        return ReservationStatus.fromCode(code);
    }

    @Override
    public ReservationStatus getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        int code = callableStatement.getInt(i);
        return ReservationStatus.fromCode(code);
    }
}

생성한 TypeHandler를 MyBatis 설정 파일에 등록하고, Enum 타입을 사용하는 필드에 TypeHandler를 설정해주면 됩니다.

1
2
mybatis:
type-handlers-package:com.bonestew.popmate.*.persistence.typehandler
1
2
3
4
5
<resultMap id="reservationMap" type="Reservation">
  ...
  <result column="reservation_status" property="reservationStatus"
    typeHandler="com.bonestew.popmate.reservation.persistence.typehandler.ReservationStatusTypeHandler"/>
</resultMap>

TypeHandler 설정이 끝나면 정수를 다음과 같이 조회하여 Enum 타입의 상수를 사용할 수 있습니다.

1
2
3
4
<select id="selectByReservationId" resultMap="reservationMap">
  select * from reservation
  where reservation_id = #{reservationId}
</select>
1
2
3
4
public class Reservation {
    ...
    private ReservationStatus reservationStatus;
}

데이터베이스 저장 방식과는 상관없이 코드 레벨에서 Enum 타입을 사용할 경우 추적하기도 쉽고 가독성도 좋아집니다.
추후 Enum 타입의 상수를 그대로 저장하는 방식으로 변경할 때도 TypeHandler를 사용하면 변경해야 할 코드의 양이 줄어들 수도 있을 것입니다.

마무리

Enum 타입의 상수 저장 방식 대신 정수 저장 방식을 사용하면서, 어느 방식이 항상 옳다라기 보다는 프로젝트의 특징이나 변경 가능성을 고려하여 적절하게 선택해야 한다는 것을 느꼈습니다.

성능적인 측면도 물론 중요하지만, 소프트웨어 개발 요구 사항이 계속해서 변경되고 추가됨에 따라 변경에 유연하게 대처할 줄 아는 것이 개발자에게 더 필요한 역량이 아닐까 생각합니다. 성능은 추후 최적화를 통해 개선할 수 있지만, 변경에 유연하게 대처할 수 없는 설계는 추후 변경에 대한 비용이나 시간이 더 많이 들테니까요

이번 포스팅은 Enum 타입을 데이터베이스에 저장하는 방식에 대한 소개와 회고에 대해 정리하였습니다.

이상으로 Enum 타입을 데이터베이스에 저장하는 방식에 대한 회고에 대한 포스팅을 마치겠습니다.🖐️
끝까지 읽어주셔서 감사합니다.