DB에는 VARCHAR로 저장된 값을 자바에서는 Enum으로 쓰고 싶을 때, 변환 코드를 매번 작성하고 계신가요?

MyBatis는 자바 타입과 JDBC 타입 사이의 변환을 TypeHandler라는 컴포넌트에 위임합니다. 기본 제공되는 핸들러 외에 커스텀 핸들러를 만들면 Enum, JSON, 암호화 등 다양한 변환을 자동화할 수 있습니다.

개념 정의

TypeHandler 는 자바 타입과 JDBC 타입 사이의 양방향 변환을 담당하는 MyBatis 컴포넌트입니다. 파라미터를 PreparedStatement에 설정할 때와, ResultSet에서 값을 꺼낼 때 모두 TypeHandler가 동작합니다.

왜 필요한가

  • **Enum 매핑 **: DB에는 "ACTIVE" 문자열로 저장하지만, 자바에서는 Status.ACTIVE Enum으로 사용합니다
  • **JSON 컬럼 **: MySQL의 JSON 타입 컬럼을 자바 객체로 자동 변환하고 싶습니다
  • ** 암호화/복호화 **: DB 저장 시 자동 암호화, 조회 시 자동 복호화가 필요합니다

동작 원리

TypeHandler가 개입하는 두 지점을 확인합니다.

PLAINTEXT
[파라미터 바인딩]
자바 객체 → TypeHandler.setParameter() → PreparedStatement

[결과 매핑]
ResultSet → TypeHandler.getResult() → 자바 객체
  1. ** 쿼리 실행 시 **: #{status}에 Enum 값 Status.ACTIVE가 들어오면, TypeHandler가 이를 "ACTIVE" 문자열로 변환하여 PreparedStatement에 설정합니다.
  2. ** 결과 조회 시 **: ResultSet에서 "ACTIVE" 문자열을 꺼낼 때, TypeHandler가 이를 Status.ACTIVE Enum으로 변환하여 자바 객체에 주입합니다.

MyBatis는 String, Integer, Date 등 기본 타입에 대한 TypeHandler를 내장하고 있어서, 별도 설정 없이도 기본 타입 변환이 동작합니다.

커스텀 TypeHandler 구현 — Enum 매핑

기본 EnumTypeHandler는 Enum의 name()을 사용합니다. 만약 DB에 코드값("01", "02")으로 저장해야 한다면 커스텀 핸들러가 필요합니다.

JAVA
public enum OrderStatus {
    PENDING("01"), CONFIRMED("02"), SHIPPED("03");

    private final String code;
    OrderStatus(String code) { this.code = code; }
    public String getCode() { return code; }

    // 코드값으로 Enum 조회
    public static OrderStatus fromCode(String code) {
        return Arrays.stream(values())
            .filter(s -> s.code.equals(code))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException(
                "알 수 없는 코드: " + code));
    }
}

이 Enum을 위한 TypeHandler를 구현합니다.

JAVA
@MappedTypes(OrderStatus.class)
public class OrderStatusHandler
    extends BaseTypeHandler<OrderStatus> {

    @Override
    public void setNonNullParameter(
        PreparedStatement ps, int i,
        OrderStatus param, JdbcType type) {
        ps.setString(i, param.getCode());  // ← Enum → 코드값
    }

    @Override
    public OrderStatus getNullableResult(
        ResultSet rs, String col) {
        return OrderStatus.fromCode(rs.getString(col));  // ← 코드값 → Enum
    }
    // getResult(ResultSet, int), getResult(CallableStatement, int) 도 동일 패턴
}

위 코드에서 핵심은 setNonNullParametergetNullableResult입니다. 이 두 메서드가 양방향 변환의 전부입니다. 등록은 설정 파일에서 합니다.

XML
<typeHandlers>
  <typeHandler handler="com.example.handler.OrderStatusHandler"/>
</typeHandlers>

등록 후에는 별도 지정 없이 OrderStatus 타입이 파라미터나 결과에 나타나면 자동으로 이 핸들러가 적용됩니다.

JSON 컬럼 매핑

MySQL의 JSON 타입 컬럼을 자바 객체로 변환하는 TypeHandler입니다.

JAVA
@MappedTypes(Map.class)
public class JsonTypeHandler
    extends BaseTypeHandler<Map<String, Object>> {

    private static final ObjectMapper mapper = new ObjectMapper();

    @Override
    public void setNonNullParameter(
        PreparedStatement ps, int i,
        Map<String, Object> param, JdbcType type)
        throws SQLException {
        ps.setString(i, toJson(param));
    }

    @Override
    public Map<String, Object> getNullableResult(
        ResultSet rs, String col) throws SQLException {
        return fromJson(rs.getString(col));
    }
    // ObjectMapper를 이용한 toJson/fromJson 메서드 생략
}

ResultMap에서 특정 컬럼에만 적용할 수도 있습니다.

XML
<result property="metadata" column="metadata"
        typeHandler="com.example.handler.JsonTypeHandler"/>

주의할 점

TypeHandler 등록 범위 혼동

전역 등록(<typeHandlers>)과 컬럼 단위 지정(typeHandler 속성)을 혼용하면 우선순위가 헷갈립니다. 컬럼 단위 지정이 전역 등록보다 우선합니다. 같은 자바 타입에 여러 TypeHandler가 필요한 경우(예: 같은 String인데 컬럼마다 다른 변환이 필요)에는 전역 등록 대신 컬럼 단위로 지정해야 합니다.

Enum 매핑의 두 가지 함정

MyBatis는 Enum에 대해 두 가지 기본 핸들러를 제공합니다.

  • EnumTypeHandler: name()을 문자열로 저장 (기본값)
  • EnumOrdinalTypeHandler: ordinal()을 숫자로 저장

EnumOrdinalTypeHandler를 쓰면 Enum 상수의 순서가 바뀔 때 DB 데이터가 엉망이 됩니다. PENDING이 0번이었는데 새 상수를 앞에 추가하면 모든 기존 데이터의 의미가 바뀝니다. ordinal 기반 저장은 운영 환경에서 절대 사용하면 안 됩니다.

null 처리 누락

BaseTypeHandlersetNonNullParameter는 null이 아닌 경우에만 호출됩니다. null 값은 MyBatis가 자동으로 setNull()을 호출합니다. 하지만 getNullableResult에서는 null 반환을 직접 처리해야 합니다. DB에서 NULL이 들어올 수 있는 컬럼이라면 null 체크를 빠뜨리지 않아야 합니다.

정리

항목설명
TypeHandler자바 타입 ↔ JDBC 타입 양방향 변환 담당
기본 제공String, Integer, Date 등 기본 타입은 내장 핸들러로 자동 처리
커스텀 구현BaseTypeHandler<T> 상속, setNonNullParametergetNullableResult 구현
Enum 매핑EnumOrdinalTypeHandler 사용 금지, 코드값 방식 권장
JSON 컬럼ObjectMapper + TypeHandler로 자동 변환 가능
등록 방식전역(<typeHandlers>) 또는 컬럼 단위(typeHandler 속성)
댓글 로딩 중...