qodot
June 17, 2015 3:8 am
MyBatis 에서 커스텀 EnumTypeHandler 만들(삽질)기
<br>현재 진행중인 프로젝트는 DB를 먼저 설계하고 MyBatis를 이용해 테이블에 맞춰 애플리케이션을 설계하는 방식으로 진행되고 있다. 애플리케이션에서는 수많은 구분 코드들이 필요한데(ex: 남자, 여자), 필요한 모든 구분값들을 코드 테이블을 만들어 따로 관리하고 있다.
그런데 코드 테이블에 저장된 코드들이 하나같이 한눈에 알아보기 힘든 String(ex: AG_TE_MA
등...)으로 되어있어서(사실 이게 본질적인 문제인데... 이런 정책을 본인이 정할수 있는게 아니라서 ㅜㅜ) 애플리케이션에서 그대로 사용하려니 가독성이 떨어져 맘에 들지 않았다. 게다가 실제 사용하려면 String으로 하드코딩하거나 아니면 일일히 DB에서 가져와야 하는데, 이것도 마음에 들지 않았다. (ㅜㅜ)
그래서 Enum을 쓰기로 결정했다. MyBatis에서는 Enum 객체와 DB를 연결할 때 TypeHandler
가 필요하고, 기본 EnumTypeHandler
를 지원하고 있다. 그 구현은 다음과 같다.
public class EnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
private Class<E> type;
public EnumTypeHandler(Class<E> type) {
if (type == null) throw new IllegalArgumentException("Type argument cannot be null");
this.type = type;
}
@Override
void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
if (jdbcType == null) {
ps.setString(i, parameter.name());
} else {
ps.setObject(i, parameter.name(), jdbcType.TYPE_CODE); // see r3589
}
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
String s = rs.getString(columnName);
return s == null ? null : Enum.valueOf(type, s);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String s = rs.getString(columnIndex);
return s == null ? null : Enum.valueOf(type, s);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String s = cs.getString(columnIndex);
return s == null ? null : Enum.valueOf(type, s);
}
}
보면 알겠지만 DB에 쓸때는 name()
메소드, 읽을때는 valueOf()
메소드를 이용해서 Enum 타입의 값을 그대로 String으로 변환해서 읽고 쓰게 되어있다.
그러나 우리 프로젝트의 Enum 구현은 다음과 같았다.
public enum SexType {
MALE("COMPLEX_CODE_FOR_MALE", "남자"),
FEMALE("COMPLEX_CODE_FOR_FEMALE", "여자");
private final String code;
private final String description;
SexType(String code, String description) {
this.code = code;
this.description = description;
}
}
명확한 의미 전달을 위해서 실제 애플리케이션에서 쓸 값(MALE
)을 키로 정하고, DB에 읽고 쓸 값(COMPLEX_CODE_FOR_MALE
)과 한글 설명(남자
)을 포함하는 Enum을 만들기로 한 것이다.
그런데 실제로 DB에 읽고 쓸 값이 MALE
이 아니라 COMPLEX_CODE_FOR_MALE
이기 때문에 기본 EnumTypeHandler
를 쓸 수가 없었고, 커스텀 EnumTypeHandler
를 만들어야 했다. 프로젝트에서 사용하는 Enum 클래스가 모두 code
, description
필드를 가지기 때문에 code
필드로 DB에 읽고 쓰는 하나의 EnumTypeHandler
를 만들어야겠다고 생각했다.
- DB에 쓸 때
1. 우선 모든 Enum을 하나의 핸들러로 다뤄야 하므로 공통 클래스 혹은 인터페이스가 있어야 한다.
2. 그런데 모든 Enum 클래스는 묵시적으로
java.lang.Enum
클래스를 상속받는다. 3. 그렇다고 핸들러의 파라메터 타입을java.lang.Enum
으로 하면 내가 만든 Enum의code
필드를 사용할 수 없다. 4. 따라서 모든 Enum이 구현해야 하는 인터페이스를 만들어서(ex:MyEnum
) 거기에getCode()
메소드를 선언해야 한다. 5.getCode()
메소드로 얻은 String을 DB에 쓴다.
- DB에서 읽을 때
1. DB에서
code
String을 읽는다. 2. 현재 Enum 타입 안에 있는 모든 Enum 키들을 가져와서code
와 비교해 찾는다. 3. 찾은 Enum 키를 리턴한다.
values()
메소드로 타입 안에 정의된 모든 Enum들을 받아오려고 했는데, java.lang.Enum
이나 내가 만든 MyEnum
인터페이스에서는 values()
메소드를 호출할 수가 없어서 실패. (java.lang.Enum
을 상속받은 클래스에서만 호출 가능)
<br>
그럼 MyBatis 설정에서 setTypeHandlers
를 통해 타입 핸들러를 설정할 때 values()
를 파라메터로 받아서 넘겨주면 되겠다...!
sqlSessionFactoryBean.setTypeHandlers(new TypeHandler[]{
new DateTimeTypeHandler(), new BooleanTypeHandler(),
// Enum Type Handler
new CommonEnumTypeHandler(SexType.values()),
new CommonEnumTypeHandler(AgeType.values()),
..
});
그런데 같은 타입의 TypeHandler
를 여러번 생성하면 가장 아래에 생성한 TypeHandler
로 오버라이딩이 되서 실패.
<br>
아 결국 Enum 타입마다 타입 핸들러를 따로 만들어주는 수 밖에 없는 건가... Enum을 생성할 때마다 타입 핸들러도 같이 만들어줘야 하다니... 너무 구리다...
어쨌거나 좋은 방법이 떠오르지 않아, 결국 Enum 마다 따로 만들기로 결정하고, 최대한 새로 만드는 코드가 최소화 되게 해야겠다고 생각했다.
최종 결과물은 다음과 같다.
<br> ##### Common Type Handlerpublic abstract class EnumTypeHandler implements TypeHandler<MyEnum> {
// DB에 쓰는 부분
@Override
public void setParameter(PreparedStatement ps, int i, MyEnum parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.getCode());
}
// DB에서 읽는 부분
@Override
public MyEnum getResult(ResultSet rs, String columnName) throws SQLException {
String code = rs.getString(columnName);
return getTypeByCodeWithCatch(code);
}
// DB에서 읽는 부분
@Override
public MyEnum getResult(ResultSet rs, int columnIndex) throws SQLException {
String code = rs.getString(columnIndex);
return getTypeByCodeWithCatch(code);
}
// DB에서 읽는 부분
@Override
public MyEnum getResult(CallableStatement cs, int columnIndex) throws SQLException {
String code = cs.getString(columnIndex);
return getTypeByCodeWithCatch(code);
}
// Exception 발생시 처리
private MyEnum getTypeByCodeWithCatch(String code) {
MyEnum myEnum = null;
try {
myEnum = getTypeByCode(code);
} catch (NotExistedEnumCodeException e) {
log.error("Fail to get result from DB (Wrong code)", e);
} catch (Exception e) {
log.error("Fail to get result from DB (Unknown)", e);
}
return myEnum;
}
// 각 Enum 타입 핸들러 마다 구현해야 할 메소드
abstract MyEnum getTypeByCode(String code) throws Exception;
}
<br>
##### Age Type Handler@MappedTypes(AgeType.class)
public class AgeEnumTypeHandler extends CommonEnumTypeHandler {
@Override
MyEnum getTypeByCode(String code) throws Exception {
for (AgeType ageType : AgeType.values()) {
if (ageType.getCode().equals(code)) {
return ageType;
}
}
// 잘못된 code 값(현재 Enum에 없는)이 들어온 경우
throw new NotExistedEnumCodeException("Wrong code for " + this.getClass().toString() + " enum type");
}
}
<br>
##### MyBatis ConfigurationsqlSessionFactoryBean.setTypeHandlers(new TypeHandler[]{
// Enum Type Handler
new AgeEnumTypeHandler(),
new SexEnumTypeHandler(),
..
});
<br>
##### Common Enum Interfacepublic interface MyEnum {
public String getCode();
// description은 그냥 덤 ㅎㅎㅎㅎㅎ
public String getDescription();
}
<br>
##### Age Enumpublic enum AgeType implements MyEnum {
YOUNG("DB_YOUNG_CODE", "젊음"),
OLD("DB_OLD_CODE", "늙음");
private final String code;
private final String description;
AgeType(String code, String description) {
this.code = code;
this.description = description;
}
@Override
public String getCode() {
return code;
}
@Override
public String getDescription() {
return description;
}
}
<br>
이렇게 했더니 일단 원하는대로 잘 동작한다. abstract 메소드와 상속을 이용해서 나름 중복을 최소화 해봤는데... 솔직히 별로 맘에 들지는 않는다... Enum을 만들 때마다 `MyEnum`을 구현하고, `CommonEnumTypeHandler`를 상속받는 타입 핸들러를 만들어서 MyBatis 설정에 추가해야 하다니...혹시 훨씬 더 깔끔하고 좋은 방법을 알고 있는 분은 조언 부탁드립니다 ㅜㅜ