MyBatis를 사용할 때 반복적인 XML 설정, 특히 데이터베이스 컬럼과 엔티티 속성 간의 매핑을 정의하는 ResultMap 작성이 번거롭게 느껴질 수 있습니다. 이러한 문제를 해결하기 위해, @Table 및 @Column 어노테이션 정보를 기반으로 MyBatis의 ResultMap을 자동으로 생성하는 방법을 소개합니다.
핵심 구현 아이디어
- 애플리케이션 내 모든 클래스 스캔
@Table어노테이션이 적용된 엔티티 클래스 필터링@Column어노테이션을 통해 필드와 컬럼 매핑 관계 추출- MyBatis 설정 완료 후 해당 매핑 정보로 ResultMap 동적 등록
구현 코드
다음은 MyBatis 설정 시점에 ResultMap을 자동으로 추가하는 구성 클래스입니다:
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import javax.persistence.Table;
import java.util.*;
@Configuration
@Order(1)
public class AutoResultMapConfigurer {
private final SqlSessionFactory sqlSessionFactory;
public AutoResultMapConfigurer(SqlSessionFactory factory) {
this.sqlSessionFactory = factory;
initializeResultMaps();
}
private void initializeResultMaps() {
var config = sqlSessionFactory.getConfiguration();
List<Class<?>> entities = scanAnnotatedEntities();
for (Class<?> entity : entities) {
String resultMapId = entity.getName();
List<ResultMapping> mappings = buildColumnMappings(config, entity);
ResultMap resultMap = new ResultMap.Builder(config, resultMapId, entity, mappings).build();
config.addResultMap(resultMap);
}
}
private List<Class<?>> scanAnnotatedEntities() {
return EntityScanner.findClassesWithAnnotation(Table.class);
}
private List<ResultMapping> buildColumnMappings(org.apache.ibatis.session.Configuration config, Class<?> entityType) {
List<ResultMapping> mappings = new ArrayList<>();
Map<String, Map<String, Object>> fieldInfo = ColumnMapper.extractColumnRelations(entityType);
for (Map.Entry<String, Map<String, Object>> entry : fieldInfo.entrySet()) {
String property = entry.getKey();
Map<String, Object> info = entry.getValue();
String columnName = (String) info.get("column");
Class<?> javaType = (Class<?>) info.get("type");
ResultMapping mapping = new ResultMapping.Builder(config, property, columnName, javaType).build();
mappings.add(mapping);
}
return mappings;
}
}
엔티티 스캐닝 및 어노테이션 분석 유틸리티 클래스:
import javax.persistence.Column;
import javax.persistence.Table;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public class EntityScanner {
private static final Set<Class<?>> scannedClasses = new HashSet<>();
static {
scannedClasses.addAll(scanPackage("com.example.domain"));
}
public static List<Class<?>> findClassesWithAnnotation(Class<? extends Annotation> annotation) {
List<Class<?>> result = new ArrayList<>();
for (Class<?> clazz : scannedClasses) {
if (clazz.isAnnotationPresent(annotation)) {
result.add(clazz);
}
}
return result;
}
private static Set<Class<?>> scanPackage(String basePackage) {
Set<Class<?>> classes = new HashSet<>();
String path = basePackage.replace('.', '/');
try {
Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources(path);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
if ("file".equals(resource.getProtocol())) {
scanFileSystem(resource.getPath(), basePackage, classes);
} else if ("jar".equals(resource.getProtocol())) {
scanJarFile(resource, path, classes);
}
}
} catch (Exception e) {
throw new RuntimeException("클래스 스캔 중 오류 발생", e);
}
return classes;
}
private static void scanFileSystem(String path, String packageName, Set<Class<?>> classes) {
File directory = new File(path);
if (!directory.exists()) return;
File[] files = directory.listFiles();
if (files == null) return;
for (File file : files) {
if (file.isDirectory()) {
scanFileSystem(file.getPath(), packageName + "." + file.getName(), classes);
} else if (file.getName().endsWith(".class")) {
String className = packageName + "." + file.getName().substring(0, file.getName().length() - 6);
try {
classes.add(Class.forName(className));
} catch (ClassNotFoundException ignored) {}
}
}
}
private static void scanJarFile(URL url, String path, Set<Class<?>> classes) throws Exception {
JarFile jar = ((java.net.JarURLConnection) url.openConnection()).getJarFile();
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (name.startsWith(path) && name.endsWith(".class") && !entry.isDirectory()) {
String className = name.replace('/', '.').substring(0, name.length() - 6);
try {
classes.add(Class.forName(className));
} catch (ClassNotFoundException ignored) {}
}
}
}
}
class ColumnMapper {
public static Map<String, Map<String, Object>> extractColumnRelations(Class<?> entityClass) {
Map<String, Map<String, Object>> relations = new HashMap<>();
processFields(entityClass, relations);
processMethods(entityClass, relations);
return relations;
}
private static void processFields(Class<?> clazz, Map<String, Map<String, Object>> relations) {
for (Field field : clazz.getDeclaredFields()) {
Column column = field.getAnnotation(Column.class);
if (column != null) {
Map<String, Object> info = new HashMap<>();
info.put("column", column.name());
info.put("type", field.getType());
relations.put(field.getName(), info);
}
}
}
private static void processMethods(Class<?> clazz, Map<String, Map<String, Object>> relations) {
for (Method method : clazz.getDeclaredMethods()) {
Column column = method.getAnnotation(Column.class);
if (column != null) {
String fieldName = deriveFieldName(method);
if (fieldName != null) {
Map<String, Object> info = new HashMap<>();
info.put("column", column.name());
info.put("type", method.getReturnType());
relations.put(fieldName, info);
}
}
}
}
private static String deriveFieldName(Method method) {
String methodName = method.getName();
if (methodName.startsWith("get")) {
return Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
} else if (methodName.startsWith("set")) {
return Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
return null;
}
}
이 방식을 사용하면 엔티티 클래스에 정의된 @Table 및 @Column 어노테이션 정보를 기반으로 MyBatis가 자동으로 ResultMap을 생성하여 XML 설정 없이도 데이터베이스 매핑을 처리할 수 있습니다.