티스토리 뷰

728x90

Mybatis에서 enum을 사용하기 전에 알아야 할 것이 바로 TypeHandler이다.

공식문서에 있는 것을 그대로 가져오면 TypeHandler란

마이바티스가 PreparedStatement에 파라미터를 설정하고 ResultSet에서 값을 가져올 때마다 TypeHandler는 적절한 자바 타입의 값을 가져오기 위해 사용된다.

 

https://mybatis.org/mybatis-3/ko/configuration.html#typeHandlers

 

MyBatis – 마이바티스 3 | 매퍼 설정

매퍼 설정 마이바티스 XML 설정파일은 다양한 설정과 프로퍼티를 가진다. 문서의 구조는 다음과 같다.: configuration properties 이 설정은 외부에 옮길 수 있다. 자바 프로퍼티 파일 인스턴스에 설정할

mybatis.org

이 TypeHandler가 우리가 자바에서의 boolean 또는 int, double, long, String 타입을 JDBC 타입으로 변경? 해주는 것이다.

많은 TypeHandler가 있는데 그중에 EnumTypeHandler와 EnumOrdinalTypeHandler 가 있는데 이걸 알아야 한다.

아무 설정도 해주지 않으면 EnumTypeHandler가 사용이 된다.

자바에서 enum 클래스를 만들어서 mybatis에 enum 타입을 넘기면 enum 상수 그대로 저장이 될 것이다.

그때 사용되는 게 EnumTypeHandler이다.

public enum UserType {

    ADMIN,
    USER,
    GUEST;
}

enum 클래스가 위와 같으면  저장했을 때 DB에는 ADMIN, USER, GUEST의 값으로 들어갈 것이다.

//Mapper가 아래라고 가정
@Insert("insert into user_tbl(type, name) values (#{type}, #{name})")
int insertUser(UserVO userVO);

@Test
void USER_입력_테스트() {
    UserVO userVO = new UserVO();
    userVO.setName("회원");
    userVO.setType(UserType.USER);
    int i = mapper.insertUser(userVO);
    assertThat(i).isEqualTo(1);
}

실제로는 enum의 순서 값을 사용하면 안 되고 사용할 일도 많이 없겠지만  EnumOrdinalTypeHandler를 사용하려면 어떻게 해야 할까?

mybatis-config에 해당 TypeHandler를 명시해주면 된다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="cacheEnabled" value="false"/>
        <setting name="useGeneratedKeys" value="true"/>
        <setting name="defaultExecutorType" value="REUSE"/>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <typeHandlers>
        <typeHandler javaType="com.sample.mybatis.enums.UserType" handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
    </typeHandlers>
</configuration>

UserType의 javaType enum의 핸들러를 EnumOrdinalTypeHandler를 설정해 준다.

그러고 나서 저장하면 순서 값이 들어간다.

@Test
void USER_입력_테스트() {
    UserVO userVO = new UserVO();
    userVO.setName("관리자");
    userVO.setType(UserType.ADMIN);
    int i = mapper.insertUser(userVO);
    assertThat(i).isEqualTo(1);
}

회원으로 저장을 한번 하고 관리자로 한번 더 테스트해서 3행이 나온 결과이다. enum 순서처럼 ADMIN 0번 USER 1번 GUEST 2번 순이다.

Custom TypeHandler

그런데 여기서부터가 실제 하고 싶은 것이다. enum을 보통 상수값을 그대로 넣거나 순서 번호를 그대로 넣는 경우는 없을 것이다.

보통 실무에서는 공통 코드 테이블을 따로 관리한다. 그리고 이런 공통코드 값이 따로 존재한다. 예를들어 관리자(ADMIN)이면 USER000

회원(USER) USER001, 비회원(GUEST) USER002 이런 식으로 공통코드 테이블에 들어가 있다. (이건 물론 예제임..)

그럼 DB에 저장할 때는 관리자면 USER000 값이 테이블에 들어가야 하는데 이럴 때 TypeHandler를 Custom 해줘야 한다.

public enum UserType {

    ADMIN("USER000"),
    USER("USER001"),
    GUEST("USER002");

    private String code;

    UserType(String code) {
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

공식문서에 이렇게 나와 있다.

지원하지 않거나 비표준인 타입에 대해서는 당신 스스로 만들어서 타입핸들러를 오버라이드할 수 있다. 그러기 위해서는 TypeHandler 인터페이스를 구현하고 자바 타입에 TypeHandler를 매핑하면 된다. 

그렇다면 TypeHandler 인터페이스를 구현할 UserTypeHandler를 작성한다.

@MappedTypes(UserType.class)
public class UserTypeHandler implements TypeHandler<UserType> {

    @Override
    public void setParameter(PreparedStatement ps, int i, UserType parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter.getCode());
    }

    @Override
    public UserType getResult(ResultSet rs, String columnName) throws SQLException {
        String code = rs.getString(columnName);
        return getCodeEnum(code);
    }

    @Override
    public UserType getResult(ResultSet rs, int columnIndex) throws SQLException {
        String code = rs.getString(columnIndex);
        return getCodeEnum(code);
    }

    @Override
    public UserType getResult(CallableStatement cs, int columnIndex) throws SQLException {
        String code = cs.getString(columnIndex);
        return getCodeEnum(code);
    }

    private UserType getCodeEnum(String code) {
        switch (code) {
            case "0":
                return UserType.ADMIN;
            case "1":
                return UserType.USER;
            case "2":
                return UserType.GUEST;
        }
        return null;
    }
}

그리고 mybatis-config 설정

<typeHandlers>
    <typeHandler handler="com.sample.mybatis.enums.UserTypeHandler"/>
</typeHandlers>

공식문서에는 이렇게 나와있다.

이러한 TypeHandler를 사용하는 것은 자바 String프로퍼티와 VARCHAR파라미터 및 결과를 위해 이미 존재하는 핸들러를 오버라이드 하게 될 것이다. 마이바티스는 타입을 판단하기 위해 데이터베이스의 메타데이터를 보지 않는다. 그래서 파라미터와 결과에 정확한 타입 핸들러를 매핑해야 한다. 마이바티스가 구문이 실행될 때까지는 데이터 타입에 대해 모르기 때문이다.

 

TypeHandler 인터페이스를 구현한 UserTypeHandler의  @MappedTypes 애노테이션으로 매핑을 하던가 config 파일에서 javaType 속성으로 정의해줘도 된다.

 

테스트를 해본다.

@Test
void USER_입력_테스트() {
    UserVO userVO = new UserVO();
    userVO.setName("관리자");
    userVO.setType(UserType.ADMIN);
    int i = mapper.insertUser(userVO);
    assertThat(i).isEqualTo(1);
}

ID가 6번인 값을 보면 TYPE에 정의한 코드 값으로 들어간 것을 확인할 수 있다.

그럼 SELECT 할 때를 테스트해보자.

//mapper
@Select("select * from user_tbl")
List<UserVO> getAllUser();

@Test
void USER_SELECT_TEST() {
    mapper.getAllUser()
            .forEach(i -> {
                log.info("===> {}, {}, {}", i.getId(), i.getName(), i.getType());
            });
}

결과

[           main] c.s.mybatis.service.MybatisServiceTest   : ===> 1, 회원, null
[           main] c.s.mybatis.service.MybatisServiceTest   : ===> 2, 회원, USER
[           main] c.s.mybatis.service.MybatisServiceTest   : ===> 3, 관리자, ADMIN
[           main] c.s.mybatis.service.MybatisServiceTest   : ===> 6, 관리자, null

이게 왜 이런 결과가 나왔냐면 TypeHandler를 구현한 클래스에서 getCodeEnum을 아래와 같이 작성했기 때문이다.

본인 코드값에 맞게 변경해주면 된다.

private UserType getCodeEnum(String code) {
    switch (code) {
        case "0":
            return UserType.ADMIN;
        case "1":
            return UserType.USER;
        case "2":
            return UserType.GUEST;
    }
    return null;
}

이제 INSERT 할 때는 원하는 DB의 코드값으로, SELECT 할때는 java의 enum 타입으로 가져올 수 있게 되었다.

그런데 여기서 한 가지 불편한 점이 있다. 이런 enum들이 많아지면 enum클래스마다 TypeHandler를 구현해야 하는 수고스러움이 생긴다. 

 

여기서부터 출처는 https://www.holaxprogramming.com/2015/11/12/spring-boot-mybatis-typehandler/


 

 

문자열인 코드를 반환하기 위한 인터페이스

public interface CodeEnum {
    String getCode();
}

TypeHandler를 구현한 CodeEnumTypeHandler 클래스

(솔직히 여기서부터 우와 했다...)

public abstract class CodeEnumTypeHandler<E extends Enum<E> & CodeEnum> implements TypeHandler<CodeEnum> {

    private Class<E> type;

    public CodeEnumTypeHandler(Class<E> type) {
        this.type = type;
    }

    @Override
    public void setParameter(PreparedStatement ps, int i, CodeEnum parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter.getCode());
    }

    @Override
    public CodeEnum getResult(ResultSet rs, String columnName) throws SQLException {
        return getCodeEnum(rs.getString(columnName));
    }

    @Override
    public CodeEnum getResult(ResultSet rs, int columnIndex) throws SQLException {
        return getCodeEnum(rs.getString(columnIndex));
    }

    @Override
    public CodeEnum getResult(CallableStatement cs, int columnIndex) throws SQLException {
        return getCodeEnum(cs.getString(columnIndex));
    }

    private CodeEnum getCodeEnum(String code) {
        return EnumSet.allOf(type)
                .stream()
                .filter(value -> value.getCode().equals(code))
                .findFirst()
                .orElseGet(null);
    }
}

최종적으로 UserType enum 클래스이다.

이전의 UserType과는 다르게 CodeEnum을 구현하고 Enum 클래스 내부에 TypeHandler와 같은 static 클래스를 정의하며, 이 클래스는 CodeEnumTypeHandler를 상속하여 공통적인 비즈니스 로직을 처리하게 된다.

public enum UserType implements CodeEnum{

    ADMIN("USER000"),
    USER("USER001"),
    GUEST("USER002");

    private String code;

    UserType(String code) {
        this.code = code;
    }

    @MappedTypes(UserType.class)
    public static class TypeHandler extends CodeEnumTypeHandler<UserType> {
        public TypeHandler() {
            super(UserType.class);
        }
    }

    @Override
    public String getCode() {
        return code;
    }
}

config 파일

$표시는 내부 클래스를 의미한다.

<typeHandlers>
    <typeHandler handler="com.sample.mybatis.enums.UserType$TypeHandler"/>
</typeHandlers>

테스트 코드

@Test
void USER_입력_테스트() {
    UserVO userVO = new UserVO();
    userVO.setName("비회원");
    userVO.setType(UserType.GUEST);
    int i = mapper.insertUser(userVO);
    assertThat(i).isEqualTo(1);
}

결과

출처 및 도움

https://www.holaxprogramming.com/2015/11/12/spring-boot-mybatis-typehandler/

https://androphil.tistory.com/707

샘플코드

https://github.com/sskim91/java-spring-lab/tree/main/sample-mybatis-h2

 

GitHub - sskim91/java-spring-lab

Contribute to sskim91/java-spring-lab development by creating an account on GitHub.

github.com

 

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
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
글 보관함