기존 JPA 방식에 익숙해져서 MyBatis 사용 시 SQL 작성의 번거로움을 느꼈습니다. 필요한 기본 쿼리 작업(등록/수정/삭제)은 자동으로 처리되도록 구현했습니다. 이 기능은 애플리케이션 시작 시 모델 클래스를 분석해 동적으로 SQL을 생성하고, SqlSessionFactory에 등록합니다. XML 파일이나 Mapper 인터페이스를 생성하지 않으며, 모델 변경 시 자동으로 업데이트됩니다.
필요 조건:
- 모델 클래스에 @Table 주석 필수
- 필드에 @Column 주석 필수 (javax 패키지 기준)
핵심 구현 로직:
- 애플리케이션 시작 시 MyBatis 기본 설정 완료 후 커스텀 설정 추가
- @Table 주석이 포함된 모든 클래스 수집
- 각 클래스의 필드에서 @Column 주석 정보 추출
- 테이블명과 컬럼 정보를 기반으로 ResultMap 및 SQL 문장 생성
예시 코드:
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import java.util.*;
@Configuration
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class MyBatisAutoMapperConfig {
private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass());
public MyBatisAutoMapperConfig(SqlSessionFactory sqlSessionFactory) {
logger.info("자동 ResultMap 생성 시작");
Configuration config = sqlSessionFactory.getConfiguration();
List<Class<?>> entityClasses = EntityScanner.findAnnotatedClasses(Table.class);
for (Class<?> entityClass : entityClasses) {
Map<String, FieldMetadata> fieldMap = FieldMetadataProcessor.processFields(entityClass);
ResultMap resultMap = createResultMap(config, entityClass, fieldMap);
config.addResultMap(resultMap);
generateMapperXml(config, entityClass, fieldMap);
}
logger.info("자동 ResultMap 생성 완료");
}
private ResultMap createResultMap(Configuration config, Class<?> entityType, Map<String, FieldMetadata> fields) {
List<ResultMapping> mappings = new ArrayList<>();
for (Map.Entry<String, FieldMetadata> entry : fields.entrySet()) {
String propertyName = entry.getKey();
FieldMetadata metadata = entry.getValue();
ResultMapping mapping = new ResultMapping.Builder(config, propertyName, metadata.columnName, metadata.javaType)
.build();
mappings.add(mapping);
}
return new ResultMap.Builder(config, entityType.getName(), entityType, mappings).build();
}
private void generateMapperXml(Configuration config, Class<?> entityType, Map<String, FieldMetadata> fields) {
StringBuilder xmlBuilder = new StringBuilder();
String className = entityType.getName();
Table tableAnnotation = entityType.getAnnotation(Table.class);
String tableName = tableAnnotation.name();
xmlBuilder.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
xmlBuilder.append("<mapper namespace=\"").append(className).append("\">\n");
// SELECT 구문 생성
xmlBuilder.append(" <select id=\"").append(className).append(".select\" resultMap=\"").append(className).append("\">\n");
xmlBuilder.append(" SELECT ").append(generateColumnList(fields)).append(" FROM ").append(tableName).append("\n");
xmlBuilder.append(" <where>\n");
for (FieldMetadata field : fields.values()) {
if (!field.isId) {
xmlBuilder.append(" <if test=\"").append(field.propertyName).append(" != null\">\n");
xmlBuilder.append(" AND ").append(field.columnName).append(" = #{" + field.propertyName + "}\n");
xmlBuilder.append(" </if>\n");
}
}
xmlBuilder.append(" </where>\n");
xmlBuilder.append(" </select>\n");
// INSERT 구문 생성
xmlBuilder.append(" <insert id=\"").append(className).append(".insert\" parameterType=\"").append(className).append("\">\n");
xmlBuilder.append(" INSERT INTO ").append(tableName).append(" (\n");
xmlBuilder.append(" ").append(generateColumnList(fields)).append("\n");
xmlBuilder.append(" ) VALUES (\n");
xmlBuilder.append(" ").append(generateValuePlaceholders(fields)).append("\n");
xmlBuilder.append(" )\n");
xmlBuilder.append(" </insert>\n");
// UPDATE 구문 생성
xmlBuilder.append(" <update id=\"").append(className).append(".update\" parameterType=\"").append(className).append("\">\n");
xmlBuilder.append(" UPDATE ").append(tableName).append(" SET \n");
for (FieldMetadata field : fields.values()) {
if (!field.isId) {
xmlBuilder.append(" ").append(field.columnName).append(" = #{" + field.propertyName + "},\n");
}
}
xmlBuilder.append(" WHERE id = #{id}\n");
xmlBuilder.append(" </update>\n");
// DELETE 구문 생성
xmlBuilder.append(" <delete id=\"").append(className).append(".delete\">\n");
xmlBuilder.append(" DELETE FROM ").append(tableName).append(" WHERE id = #{id}\n");
xmlBuilder.append(" </delete>\n");
xmlBuilder.append("</mapper>\n");
InputStream xmlStream = new ByteArrayInputStream(xmlBuilder.toString().getBytes());
new XMLMapperBuilder(xmlStream, config, className, config.getSqlFragments()).parse();
}
private String generateColumnList(Map<String, FieldMetadata> fields) {
return String.join(", ", fields.values().stream()
.map(f -> f.columnName)
.toArray(String[]::new));
}
private String generateValuePlaceholders(Map<String, FieldMetadata> fields) {
return String.join(", ", fields.values().stream()
.map(f -> "#{" + f.propertyName + "}")
.toArray(String[]::new));
}
}
사용 예시:
@Repository
public class GenericDao<T> {
@Autowired
private SqlSessionFactory sqlSessionFactory;
public List<T> query(String statement, Object param) {
return sqlSessionFactory.openSession().selectList(statement, param);
}
public int execute(String statement, Object param) {
return sqlSessionFactory.openSession().update(statement, param);
}
public int insert(String statement, Object param) {
return sqlSessionFactory.openSession().insert(statement, param);
}
}