Mybatis의 resultType = Map의 camelcase 적용
mapUnderscoreToCamelCase의 사용 이유
mybatis를 사용하여 DB와 통신 시 실제 table의 컬럼명이 emp_name 이런 식으로 스네이크 케이스인 경우가 존재한다.
스네이크 케이스로된 칼럼명에 맞춰 DTO의 변수명도 “String emp_name”이라고 할 경우, 자바의 기본적인 카멜 케이스 형태랑 맞지 않는다.
이런 문제를 해결하기 위해 DTO로 받아올 경우, 해당 DTO 내 변수명에 맞게 카멜 케이스로 변환하여 매핑시켜주는 옵션이 “mapUnderscoreToCamelCase” 옵션이다.
mapUnderscoreToCamelCase 옵션을 사용하기 위해서는 mybatis-config.xml의 settings 태그 내에 아래의 옵션을 선언하여 사용할 수 있다.
1
<setting name="mapUnderscoreToCamelCase" value="true"/>
mapUnderscoreToCamelCase의 한계
상황에 따라 DTO를 선언하여 데이터를 받아오고 싶지만 resultType=”Map” 처럼 사용해야 하는 경우가 존재한다.
이 경우에는 mapUnderscoreToCamelCase 옵션이 true로 선언되어 있어도 emp_name를 받아올 경우, key값이 empName이 아닌 emp_name이 된다.
반환타입이 Map일 때, mapUnderscoreToCamelCase가 적용되지 않는 이유
- 단순 SELECT 쿼리를 디버깅하여 따라갈 경우, 객체에 대한 접근과 수정 기능을 제공하는 클래스인 MetaObject와 MetaObejct 생성자 내에서 실제 반환 타입에 전달될 값이 저장된 originalObject, originalObject와 반환객체의 매핑을 수행하는 Wrapper클래스를 확인할 수 있다.
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
// org.apache.ibatis.reflection.MetaObject 경로에 존재 public class MetaObject { private final Object originalObject; private final ObjectWrapper objectWrapper; private final ObjectFactory objectFactory; private final ObjectWrapperFactory objectWrapperFactory; private final ReflectorFactory reflectorFactory; private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) { ... if (object instanceof ObjectWrapper) { this.objectWrapper = (ObjectWrapper) object; } else if (objectWrapperFactory.hasWrapperFor(object)) { this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object); } else if (object instanceof Map) { this.objectWrapper = new MapWrapper(this, (Map) object); } else if (object instanceof Collection) { this.objectWrapper = new CollectionWrapper(this, (Collection) object); } else { this.objectWrapper = new BeanWrapper(this, object); } } ... }
위의 코드를 보면 반환타입이 Map일 경우 MapWrapper를, DTO의 경우 BeanWrapper를 따라가게 된다.
MapWrapper, BeanWrapper 모두 findProperty 메소드를 호출하여 key값으로 사용할 변수명을 반환하게 된다.
- 먼저 MapWpper의 findProperty는 useCamelCaseMapping을 파라미터로 받지만, 그 값을 확인하지 않고 name을 반환한다.
1 2 3 4 5
// org.apache.ibatis.reflection.wrapper.MapWrapper 경로에 존재 @Override public String findProperty(String name, boolean useCamelCaseMapping) { return name; }
- 반면 BeanWrapper는 metaClass.findProperty() 메소드를 활용하여 useCamelCaseMapping의 값에 따라 카멜 케이스로 변환하여 반환한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// org.apache.ibatis.reflection.wrapper.BeanWrapper 경로에 존재 // BeanWrapper의 findProperty @Override public String findProperty(String name, boolean useCamelCaseMapping) { return metaClass.findProperty(name, useCamelCaseMapping); } ... // org.apache.ibatis.reflection.MetaClass 경로에 존재 // MetaClass의 findProperty public String findProperty(String name, boolean useCamelCaseMapping) { if (useCamelCaseMapping) { name = name.replace("_", ""); } return findProperty(name); }
반환타입이 Map일 때, camelcase 적용하기
따라서, resultType=Map일 때 자동으로 카멜 케이스로 변환하기 위해선 별도의 설정이 필요하다.
아래는 커스텀 Wrapper 구현 및 적용하여 해당 문제를 해결하는 방법이다.
커스텀 Wrapper 구현 및 적용
- Custom MapWrapper 구현
- MapWrapper를 상속받아 키를 camelCase로 변환하는 로직을 추가한다.
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 32 33
import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.wrapper.MapWrapper; import java.util.HashMap; import java.util.Map; public class CustomMapWrapper extends MapWrapper { public CustomMapWrapper(MetaObject metaObject, Map<String, Object> map) { super(metaObject, map); } @Override public String findProperty(String name, boolean useCamelCaseMapping) { if (useCamelCaseMapping && name != null) { StringBuilder result = new StringBuilder(); boolean upperCase = false; for (int i = 0; i < name.length(); i++) { char c = name.charAt(i); if (c == '_') { upperCase = true; } else if (upperCase) { result.append(Character.toUpperCase(c)); upperCase = false; } else { result.append(c); } } return result.toString(); } return name; } }
- MapWrapper를 상속받아 키를 camelCase로 변환하는 로직을 추가한다.
- Custom ObjectWrapperFactory 구현
- MyBatis에서 기본 Wrapper 대신 커스텀 Wrapper를 사용할 수 있도록 설정하기 위한 CustomObjectWrapperFactory을 구현한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.factory.ObjectFactory; import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory; import org.apache.ibatis.reflection.wrapper.ObjectWrapper; import java.util.Map; public class CustomObjectWrapperFactory extends DefaultObjectWrapperFactory { @Override public boolean hasWrapperFor(Object object) { return object instanceof Map; } @Override public ObjectWrapper getWrapperFor(MetaObject metaObject, Object object) { if (object instanceof Map) { return new CustomMapWrapper(metaObject, (Map<String, Object>) object); } return super.getWrapperFor(metaObject, object); } }
- MyBatis에서 기본 Wrapper 대신 커스텀 Wrapper를 사용할 수 있도록 설정하기 위한 CustomObjectWrapperFactory을 구현한다.
- MyBatis Configuration에 CustomObjectWrapperFactory 등록
- mybatis-config.xml에 CustomObjectWrapperFactory를 등록한다.
1 2 3 4 5
... <configuration> <objectWrapperFactory type="com.example.CustomObjectWrapperFactory"/> </configuration> ...
- mybatis-config.xml에 CustomObjectWrapperFactory를 등록한다.
- 위의 방법을 통해 반환타입이 Map이어도 카멜 케이스로 변환되어 매핑시킬 수 있으며 필요에 따라 추가적인 WrapperFactory를 만들어 다른 커스텀 Wrapper를 구현, 적용할 수 있다.