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를 만들어야겠다고 생각했다.

<br>
  • DB에 쓸 때 1. 우선 모든 Enum을 하나의 핸들러로 다뤄야 하므로 공통 클래스 혹은 인터페이스가 있어야 한다. 2. 그런데 모든 Enum 클래스는 묵시적으로 java.lang.Enum 클래스를 상속받는다. 3. 그렇다고 핸들러의 파라메터 타입을 java.lang.Enum으로 하면 내가 만든 Enum의 code 필드를 사용할 수 없다. 4. 따라서 모든 Enum이 구현해야 하는 인터페이스를 만들어서(ex: MyEnum) 거기에 getCode() 메소드를 선언해야 한다. 5. getCode() 메소드로 얻은 String을 DB에 쓴다.
<br> 쓰기의 경우 이렇게 하면 깔끔하다. 그런데 읽을 때가 문제였다.<br>
  • DB에서 읽을 때 1. DB에서 code String을 읽는다. 2. 현재 Enum 타입 안에 있는 모든 Enum 키들을 가져와서 code와 비교해 찾는다. 3. 찾은 Enum 키를 리턴한다.
<br> ### 이렇게 하면 될줄 알았는데...!

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 Handler
public 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 Configuration
sqlSessionFactoryBean.setTypeHandlers(new TypeHandler[]{
	// Enum Type Handler
	new AgeEnumTypeHandler(),
	new SexEnumTypeHandler(),
	..
});
<br> ##### Common Enum Interface
public interface MyEnum {

	public String getCode();

	// description은 그냥 덤 ㅎㅎㅎㅎㅎ
	public String getDescription();

}
<br> ##### Age Enum
public 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 설정에 추가해야 하다니...

혹시 훨씬 더 깔끔하고 좋은 방법을 알고 있는 분은 조언 부탁드립니다 ㅜㅜ