5 minute read

mybatis에서 enum 타입을 리턴 받으려 하다보면 에러를 만나게 된다. 이에 대해서 알아보자 :)

나는 개인적으론 enum을 잘 사용 안하는 편이다. 아니 잘 사용 못하는게 더 옳은 표현이려나?…

원래 익숙한게 제일 좋은 코드? 이므로 잘 안쓰게 되는거 같다

그러다 이번 회사 프로젝트들을 merge 하는 단계에서 좀 코드를 깔금하게 하고 싶은 욕심에 enum을 사용하였다.

물론 사용은 그냥 하면 된다. 하지만 enum mybatis에서 사용하기 위해선(VALUE에 해당하는 값을 사용하면 상관없음) 약간의 수고? 가 필요하다.

이제 그 방법을 알아보도록 하자.

0. 차례Permalink

  1. enum type 다시 보기
  2. TypeHandler 란 무엇인가?
  3. Mybatis에서 enum사용하기

1. enum type 다시 보기Permalink

1-1 enum 기초 개념 1-2 enum 의 실무 이용 으로 나누어 설명을 하도록 한다.

1-1. enum(열거형) 기초 개념Permalink

enum은 사전적으로 열거형(enumerated type) 이라고 부른다. 열거형은 서로 연관된 상수들의 집합이라고도 할 수 있다.

enum는 JAVA 1.5 버전 부터 등장하였다. 이전에는 상수값 관리르 정수 열거 패턴을 이용하여 관리 하였다.

정수 열거 패턴은 static final로 불변의 상수 값을 만들어 사용하는 것이다. 네이밍 규칙은 대문자로 하고, 변수명을 의미있고 다른 상수들과 구분을 지어 명명한다

Enum은 이미 선언 되었던 Enum 상수 외의 객체는 사용할 수 없으며, toString 메서드를 호출하면 인쇄 가능 문자열로 쉽게 변환 할 수 있다.

1.5버전 이전의 열거방식인 int enum pattern

public class Company {
    private static final int APPLE = 1;
    private static final int GOOGLE = 2;
    private static final int TESLA = 3;
}

정수 열거 패턴의 단점은 타입 안전을 보장할 방법이 없으며 표현력도 그다지 좋지가 않다.

자바가 정수 열거 패턴을 위한 별도 이름 공간 namespace을 지원하지 않기 때문에 언더바(_)를 통하여 접두어로 이름 충돌을 방지 한다.

가장 큰 단점 중 하나는 정수 열거 패턴을 사용한 상수값(constant)는 컴파일을 하면 그 값이 클라이언트 파일에 그대로 새겨지므로, 상수 값을 변경하면 다시 컴파일을 해야 하는 번거로움도 있었다고 한다.

이러한 열거 패턴의 단점을 극복하고자 Enum Type이 생겼다.

Enum Type은 class keyword자리에 enum만 적어주면 된다.

위의 예를 enum type으로 변경하면 아래와 같다

enum Company {
    APPLE(1);
    GOOGLE(2);
    TESLA(3);
    
    private final int companys;
    
    Company(int companys) {
        this.companys = companys;
    }
}

위의 열거타입을 보면 int자료형의 값이 저장되어 있는데, 이 의미는 생성자를 호출하여 특정 데이터 값을 Enum 필드에 저장한다는 의미이다.

enum 객체를 생성하면서 값을 넣어 생성자를 초기화 하는 것은 불가능하다.

enum의 생성자는, 기본적으로 Java에서 private로 인식 하기 때문에 이러한 접근이 불가능하다.

그러면 왜 private으로 인식할까? 그 이유는 Enum은 서로 연관된 고정 상수의 집합 이기 때문이다. 즉 외부에서 객체를 생성하지 못하게 막고 따라서 생성자를 통해 값을 초기화 하거나 변경하는것을 막기 위함이다.

아래 코드를 직접 실행하며 확인해 보자

enum CompanyType {
    APPLE, EHANG, TESLA, GOOGLE
}

public class Companies {
    public String name;
    public int size;
    public CompanyType company;

    public static void main(String[] qrgs) {
        Companies company = new Companies();

        company.name = "애플";
        company.size = 126;
        company.company = CompanyType.APPLE;

        // valueOf : 매개변수로 주어진 String과 열거형에서 일치하는 이름을 갖는 원소를 반환
        // 주어진 String과 일치하는 원소가 없는 경우 IllegalArgumentException가 발생한다.
        for(CompanyType companys : CompanyType.values()) {
            System.out.println(companys);
        }
        // 값을 가져오는 방법
        // 1. enum 형 객체를 만들어 값 가져오는 방법
        CompanyType type1 = CompanyType.APPLE;
        CompanyType type2 = CompanyType.valueOf("APPLE");

        System.out.println(type1);
        System.out.println(type2);
        // 2. valueOf() 메소드를 통해 가져오는 방법

    }
}
// 열거형 상수를 다른 값과 연
enum Type {
    // 상수("연결할 문자")
    WALKING("워킹화"), RUNNING("러닝화")
    , TRACKING("트래킹화"), HIKING("등산화");

    final private String name;

    private Type(String name) { //enum에서 생성자 같은 역할
        this.name = name;
    }
    public String getName() { // 문자를 받아오는 함수
        return name;
    }
    }
public class Shoes {
    public static void main(String[] args) {
        for(Type type : Type.values()){
            System.out.println(type.getName());
        }
    }
}
1-2 enum 의 실무 이용Permalink

우아형제들 기술블로그 : Java Enum 활용기 의 내용을 참고 하였습니다.

그리고 이번장에서는 mybaits의 enum사용 법만 다루지만 위 링크에서는 JPA내에서 ENUM을 사용 하는 방법 또한 다루고 있다.

위 링크에서는 Enum의 장점을 아래와 같이 설명하고 있다.

  • 문자열과 비교해, IDE의 적극적인 지원을 받을 수 있다.
    • 자동완성, 오타검증, 텍스트 리팩토링 등
  • 허용 가능한 값들을 제한 할 수 있다.
  • 리펙토링시 변경 범위가 최소화 된다.
    • 내용의 추가가 필요하더라도, Enum 코드외 수정이 필요 없다.
  • Java의 enum은 class이기 때문에 다양한 기능이 사용 가능하다.

나는 다른것보다 아래 예제 코드와 같은 부분에서 Enum의 편리함을 확인 하였다.

enum type에 따른 method기능 추가의 편의성에 대한 예제

Enum을 사용 하지 않을때

위의 그림을 보면 Enum을 사용하지 않으면 각 메소드를 원하는 때에 사용하기 위해 독립적으로 구성하는데,

그럴때마다 결제 종류( CASH, CARD, ETC) 를 분기하는 코드가 필요하게 된다.

이건 관리의 측면에서 굉장히 좋지 못한 코드 이다.

결제종류, 결제 수단등의 관계를 명확히 표현하며, 각 타입은 본인이 수행해야할 기능과 책임만 가질 수 있게 하려면 기존 방식으로는 해결이 어렵다.

하지만 Enum을 사용하면 이를 해결 가능하다.

Enum을 사용하면


public enum PayGroup {
    CASH("현금", Arrays.asList("계좌이체", "무통장입금", "현장결제", "토스")),
    CARD("카드", Arrays.asList("페이코", "신용카드", "카카오페이", "배민페이")),
    EMPTY("없음",Collections.EMPTY_LIST);

    private String title;
    private List<String> payList;

    PayGroup(String title, List<String> payList) {
        this.title = title;
        this.payList = payList;
    }

    public static PayGroup findByPayCode(String code) {
        return Arrays.stream(PayGroup.values())
                .filter(payGroup -> payGroup.hasPayCode(code))
                .findAny()
                .orElse(EMPTY);
    }

    public boolean hasPayCode(String code) {
        return payList.stream().anyMatch(pay -> pay.equals(code));
    }

    public String getTitle() {return title;}
}

와 같이 사용이 가능하다.

위 예제 확인

@Test
public void PayGroup에게_직접_결제종류_물어보기 () throws Exception {
    String payCode = selectPayCode();
    PayGroup payGroup = PayGroup.findByPayCode(payCode);
    
    assertThat(payGroup.name(), is("배민페이"));
    assertThat(payGroup.getTitle(), is("배민페이"));

}

2. TypeHandler 란 무엇인가?Permalink

TypeHandler는 mybatis가 PreparedStatement에 파라미터를 설정하고 ResultSet에서 값을 가져올때 적절한 자바 타입의 값을 가져오거나 INSERT시에 PreparedStatement에 적절한 자바 타입의 값을 셋팅할때 사용이 된다.

Statement 클래스 ? ;
- SQL 구문을 실행하는 역할
- 스스로는 SQL 구문 이해를 하지 못한다. -> 전달자 역할
- SQL 관리 O + 연결 정보 X
- 캐시를 사용하지 않는다!. - 느리다

PreparedStatement : 
- statement를 상속받은 인터페이스로 SQL 구문을 실행시키는 기능을 갖는 객체
- PreCompiled된 SQL문을 표현 즉, statement객체는 실행시 sql명령어를 지정하여 여러 sql구문을 하나의 statement객체로 수행이 가능하다.
(재사용도 가능 하다) 하지만 prepreparedStatement 객체 생성시에 지정된 sql명령어만 실행 가능, ( 다른 sql구문은 실행 불가 )
- 동일한 sql 구문을 반복 실행 한다면 preparedState가 캐시를 사용하므로 성능면에서 굉장 빠르다

또한 TypeHandler는 알맞은 자바타입의 값을 추적하기 위해 쓰인다.

지원하지 않거나 표준 타입이 아닌 값들을 다루기 위해 기존 타입을 오버라이드 하거나 새로 만드는 것 또한 가능하다.

  • interface(org.apache.ibatis.type.TypeHandler) 구현
  • org.apache.ibatis.type.BaseTypeHandler를 extend 한다.

위 방법들로 overrode 하였다면, optionally하게 그것을 JDBC 타입으로 매핑해준다.

TypeHandler를 사용하면 아래 와 같은 Enum Type의 값을 선택하여 가져오는 것이 가능하다.

public enum PaymentMethodType {

    CREDIT("00", PayServiceType.GLOBAL),
    PREPAID("01", PayServiceType.TH, PayServiceType.SG),
    WALLET("02", PayServiceType.VN);
    
    ////////////////// 생략
}

PaymentMethodType과 같은 Enum클래스를 예를들면, “CREDIT” 이라는 Value 대신에 “00” 라는 Value가 insert/select 되기를 기대할때 TypeHandler를 이용하면 된다. 아래 문서에서는 Enum을 위해 TypeHandler 클래스를 어떻게 정의하고 활용하는지 나와있다.

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

Example

@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i,
                                  Object parameter, 
                                  JdbcType jdbcType) throws SQLException {
    // parameter를 Encrypt 한다.
    ps.setString(i, (String)encrypt(parameter));
  }

  @Override
  public String getNullableResult(ResultSet rs, 
                                  String columnIndex) throws SQLException {

    // ResultSet으로 부터 가져온 Data를 Decrypt 한다.
    return decrypt(rs.getString(columnName));
  }

  @Override
  public String getNullableResult(ResultSet rs, 
                                  int columnIndex) throws SQLException {
    // ResultSet으로 부터 가져온 Data를 Decrypt 한다.
    return decrypt(rs.getString(columnName));
  }

  @Override
  public String getNullableResult(CallableStatement cs, 
                                  int columnIndex) throws SQLException {
    // ResultSet으로 부터 가져온 Data를 Decrypt 한다.
    return decrypt(rs.getString(columnName));
  }
  
  private String encrypt(String toEncrypt) { ... }

  private String decrypt(String toDecrypt) { ... }
}

Select 에서의 사용 mapper.xml 에서 아래와 사용한다.

<result column="col_name" 
        jdbcType="VARCHAR" 
        property="pojo_member_name" 
        typeHandler="xxx.yyy.zzz.ExampleTypeHandler" />

Spring Boot에서의 TypeHandler 등록

mybatis 설정시 SqlSessionFactoryBean 클래스에 아래와 같이 TypeHandler 등록이 가능하다. 이렇게 myBatis에서 Enum클래스를 관리하는 방법을 알아보고, 요구사항에 맞게 특정 코드성값을 Json Object 또는 DB에서 사용할 수 있게 되었다.

@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource);
        sessionFactoryBean.setTypeAliasesPackage(MyBatisProperties.TYPE_ALIASES_PACKAGE);
        sessionFactoryBean.setConfigLocation(applicationContext.getResource(myBatisProperties.getConfigLocation()));
        sessionFactoryBean.setMapperLocations(applicationContext.getResources(myBatisProperties.getMapperLocations()));
        sessionFactoryBean.setTypeHandlers(new TypeHandler[] {
        new BooleanTypeHandler(),
        new PaymentMethodCodeType.TypeHandler()
        });

        return sessionFactoryBean.getObject();
}

Categories:

Updated: