public interface TypeHandler<T> {
void setParameter(PreparedStatement var1, int var2, T var3, JdbcType var4) throws SQLException;
T getResult(ResultSet var1, String var2) throws SQLException;
T getResult(ResultSet var1, int var2) throws SQLException;
T getResult(CallableStatement var1, int var2) throws SQLException;
}
Mybatis에서는 database에서의 column과 Java에서의 enum type 간의 conversion을 수행하기 위해 명시적으로 TypeHandler를 정의해 주어야 한다. org.apache.ibatis.type.TypeHandler
interface의 setParameter
와 getResult
method들을 구현한 구현체로 어떻게 database에 적절한 java type의 값을 저장하거나 가져올지를 지정해준다. 모든 enum type에 관한 TypeHandler를 일일히 구현해 주는 것은 비효율적인 면이 있기 때문에, 일반적으로는 conversion에 사용되는 code
나 index
따위의 공통 속성값을 enum 내부에 정의해둔 뒤 동일한 type의 공통 속성에 대해서는 abstract TypeHandler를 만들어 내부 logic을 구현해 사용하곤 한다.
public interface PreparedStatement extends Statement {
/* ... */
void setInt(int parameterIndex, int x) throws SQLException;
/* ... */
void setString(int parameterIndex, String x) throws SQLException;
/* ... */
}
type별로 abstract TypeHandler를 공유하는 방식은 동일한 type의 공통 속성에서만 유효한데, 다시 말하자면 enum의 공통 속성의 type이 int인 경우와 String인 경우는 abstract TypeHandler를 공유할 수 없다는 것이다. 이는 TypeHandler의 내부 method들을 구현할 때 사용하는 method들의 한계에서 기인하는데, PreparedStatement
, CallableStatement
, ResultSet
등 sql 구문의 실행을 위한 jdbc 객체들의 get/set method들이 type별로 따로 정의되어 있기 때문이다. PreparedStatement
객체의 특정 index에 int 값을 지정하고자 할 때는 setInt
를, String 값을 지정하고자 할 때는 setString
method를 사용해서 구현해야 하는 것이다. 주로 사용하는 특정 parameter type에 대한 TypeHandler가 아니라 type에 관계 없이 사용할 수 있는 하나의 공통 TypeHandler를 구현할 수 있을까?
public interface CodeEnum {
Object getCode();
}
TypeHandler의 구현 목적은 Java enum type의 특정 field들 database column과 compatible하도록 만드는 것이다. CodeEnum
interface는 TypeHandler에서 database column과 compatible하도록 구현할 enum field를 code
field로 고정하도록 정의되어 있다. CodeEnum
interface를 implements한 enum을 만들 때 반드시 code
field를 정의해야 한다. 이때 interface의 code
field의 type이 Object
로 정의되어 있어 구현체에서 여러 type의 code
field를 사용할 수 있다.
public abstract class CodeEnumTypeHandler<E extends CodeEnum> implements TypeHandler<CodeEnum> {
private final Class<E> type;
private final CodeType codeType;
enum CodeType {
INTEGER(INTEGER.class, "Int", int.class),
STRING(String.class, "String", String.class);
@Getter
final Class<?> codeType;
@Getter
final String suffix;
@Getter
final Class<?> paramType;
CodeType(Class<?> codeType, String suffix, Class<?> paramType) {
this.codeType = codeType;
this.suffix = suffix;
this.paramType = paramType;
}
public static CodeType get(Class<?> codeType) {
return Arrays.stream(values())
.filter(type -> type.getCodeType().equals(codeType))
.findAny()
.orElseThrow(() -> new TypeException("Code type is not allowed in CodeEnumTypeHandler"));
}
}
public CodeEnumTypeHandler(Class<E> type) {
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
this.codeType = CodeType.get(type.getEnumConstants()[0].getCode().getClass());
}
private Method getMethod(Class<?> objectType, String prefix, Class<?>... paramTypes) {
String methodName = prefix + codeType.getSuffix();
try {
return objectType.getDeclaredMethod(methodName, paramTypes);
} catch (NoSuchMethodException e) {
throw new MybatisSystemException(e);
}
}
private Object invokeMethod(Method method, Object instance, Object... params) {
try {
return method.invoke(instance, params);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new MybatisSystemException(e);
}
}
@Override
public void setParameter(PreparedStatement ps, int i, CodeEnum parameter, JdbcType jdbcType) throws SQLException {
Method method = getMethod(PreparedStatement.class, "set", int.class, codeType.getParamType());
invokeMethod(method, ps, i, parameter.getCode());
}
@Override
public CodeEnum getResult(ResultSet rs, String columnName) throws SQLException {
Method method = getMethod(ResultSet.class, "get", String.class);
Object code = invokeMethod(method, rs, columnName);
return getCodeEnum(code);
}
@Override
public CodeEnum getResult(ResultSet rs, int columnIndex) throws SQLException {
Method method = getMethod(ResultSet.class, "get", int.class);
Object code = invokeMethod(method, rs, columnIndex);
return getCodeEnum(code);
}
@Override
public CodeEnum getResult(CallableStatement cs, int columnIndex) throws SQLException {
Method method = getMethod(CallableStatement.class, "get", int.class);
Object code = invokeMethod(method, cs, columnIndex);
return getCodeEnum(code);
}
private CodeEum getCodeEnum(Object code) {
CodeEnum[] enumConstants = type.getEnumConstants();
for (CodeEnum codeEnum : enumConstants) {
if (codeEnum.getCode().equals(code)) {
return codeEnum;
}
}
return null;
}
}
앞에서 정의한 interface CodeEnum
을 타입 변수로 사용해 TypeHandler
를 구현한 CodeEnumTypeHandler
를 정의할 수 있다. CodeEnumTypeHandler
는 CodeEnum
의 하위 type만을 타입 변수로 받는 generic class(<E extends CodeEnum>
)로 정의되어 CodeEnum
의 구현체들에 관한 TypeHandler
로 상속해 사용할 수 있다. CodeEnumTypeHandler
의 생성자를 호출하는 시점에 conversion의 대상이 되는 type class 정보를 받아 내부 field로 받아와 사용하게 된다.
CodeEnumTypeHandler
에서는 CodeType
이라는 내부 enum을 활용해 내부적으로 method 호출에 필요한 정보들을 처리한다. code
field의 type, 호출할 method 이름의 suffix, method 호출 시에 필요한 인자의 class를 enum field로 사용한다. 생성자 호출 시점에 받아온 type class와 codeType
field의 비교를 바탕으로 얻어낸 enum 정보를 마찬가지로 내부 field에 저장해 사용한다. 개발 당시에는 enum을 사용해 database에 get/set하고자 했던 Java type이 두 종류밖에 없었기 때문에 해당 부분만 구현해 둔 상태이지만, 다른 type들도 CodeType
enum에 추가해서 충분히 사용 가능하다. 예를 들자면 사용하는 database가 Oracle이라 boolean
type을 사용하지 않았지만, boolean
type을 추가해 사용할 수도 있을 것이다.
jdbc 객체들이 호출해야 하는 method들을 결정하는 getMethod
method, 실제로 해당 method들을 호출해 결과를 받아오는 invokeMethod
method는 Java의 reflection 개념을 사용해 구현했다. codeType
enum 없이 code
field의 type에 따라 각각 처리 방식을 구현할 수도 있겠지만, CodeType
enum만 변경해서 사용 가능한 type을 지정할 수 있도록 method를 호출하는 공통적인 로직을 분리하고자 했다. 마침 호출하는 method들이 유사한 naming convention을 가지고 있어서 CodeType
enum의 정보들 만으로 충분히 구현 가능했다.
Java reflection과 관련된 exception들은 runtime에 실제로 호출하는 method들이 모두 존재하도록 정확히 구현되어 있기 때문에 실제로 발생할 일은 없다. 하지만 Class.getDeclaredMethod
, Method.invoke
가 throws 하는 checked exception들은 처리해 주어야 하고, TypeHandler
에서 overriding하는 method들은 이미 SQLException
만 throws 가능하도록 정의되어 있기 때문에 runtime에 exception handler가 처리할 수 있는 방식인 DataAccessException
하위의 MybatisSystemException
을 발생시키도록 지정하였다.
enum NotificationType implements CodeEnum {
REGISTER("REGISTER"),
REPLY("REPLY");
@Getter
private final String code;
NotificationType(String code) {
return Arrays.stream(values())
.filter(type -> type.getCode().equals(code))
.findAny()
.orElse(null);
}
@MappedTypes(NotificationType.class)
public static class TypeHandler extends CodeEnumTypeHandler<NotificationType> {
public TypeHandler() {
super(NotificationType.class);
}
}
}
CodeEnum
과 CodeTypeEnumHandler
를 실제로 사용하는 구현 예시이다. CodeEnum
interface를 implements한 enum type class 내부에 CodeEnumTypeHandler
를 extends한 TypeHandler
class를 선언해서 사용할 수 있다. CodeEnum
의 구현체이기 때문에 code
field와 @Getter
annotation을 사용한 getCode
method를 필수적으로 구현했음을 확인할 수 있다. code
field의 return type이 String.class
나 Integer.class
가 아닌 경우 runtime에 TypeException
이 발생하게 된다. TypeHandler
에는 @MappedTypes
annotation을 사용해 어떤 type에 관한 TypeHandler
인지를 명시해 준다. TypeHandler
의 생성자는 인자 없이 호출할 수 있도록 구현해야 한다.
<insert>
<!-- ... -->
#{notification.type, javaType=path.NotificationType,
typeHandler=path.NotificationType$TypeHandler}
<!-- ... -->
</insert>
insert, update 등 database에 Java type의 data를 set하는 경우, #{}
문법 내부에 javaType
과 함께 typeHandler
를 명시해주면 된다.
<resultMap>
<!-- ... -->
<result column="TYPE" property="type"
typeHandler="path.NotificationType$TypeHandler" />
<!-- ... -->
</resultMap>
마찬가지로 select 문을 사용해 database에서 Java type의 data를 get하려는 경우, resultMap에 지정하는 property에 typeHandler
를 명시해 주면 된다. 이때 CodeEnumTypeHandler
가 abstract class로 구현되었기 때문에 CodeEnumTypeHandler
를 상속받아 실제로 구현한 TypeHandler
를 명시해 주어야 한다. 만약 CodeEnumTypeHandler
를 abstract class가 아닌 일반 class로 구현한다면 CodeEnumTypeHandler
를 명시해 주어도 문제 없이 작동하게 된다. 여기에서는 CodeEnumTypeHandler
가 아니라 상속받은 TypeHandler
를 mapper에 명시하도록 강제하면 initalization 과정에 TypeHandler
instance가 만들어지기 때문에 exception을 이른 시점에 확인 가능하다는 점 때문에 abstract class로 CodeEnumTypeHandler
를 구현했다.
이렇게 만들어진 enum을 dto 내부에서 사용해서 response로 보내면 기본적으로 constant name 값이 보내지게 된다. 만약 enum이 가진 내부 field 값들을 json 형태로 response에 포함해 보내고 싶다면, 따로 interface를 구현해 주어도 되지만 @JsonFormat
annotation을 명시해 간단히 형태를 변경할 수 있다. 이때 enum 내부에 get
으로 시작하는 method가 있다면 해당 method의 return값이 field로 취급되어 함께 json 내부에 포함되어 나타난다. 이 annotation을 CodeEnum
에 지정해 준다면 상속받은 모든 enum에 annotation을 적용한 효과를 얻을 수 있다.
public static class Notification {
@JsonFormat(shape=JsonFormat.Shape.OBJECT)
private NotificationType type;
}