Spring Boot 및 MyBatis를 사용한 자동 CRUD 로직 생성: @Table 및 @Column 주석 기반

기존 JPA 방식에 익숙해져서 MyBatis 사용 시 SQL 작성의 번거로움을 느꼈습니다. 필요한 기본 쿼리 작업(등록/수정/삭제)은 자동으로 처리되도록 구현했습니다. 이 기능은 애플리케이션 시작 시 모델 클래스를 분석해 동적으로 SQL을 생성하고, SqlSessionFactory에 등록합니다. XML 파일이나 Mapper 인터페이스를 생성하지 않으며, 모델 변경 시 자동으로 업데이트됩니다.

필요 조건:

  • 모델 클래스에 @Table 주석 필수
  • 필드에 @Column 주석 필수 (javax 패키지 기준)

핵심 구현 로직:

  1. 애플리케이션 시작 시 MyBatis 기본 설정 완료 후 커스텀 설정 추가
  2. @Table 주석이 포함된 모든 클래스 수집
  3. 각 클래스의 필드에서 @Column 주석 정보 추출
  4. 테이블명과 컬럼 정보를 기반으로 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);
    }
}

태그: MyBatis Auto Mapper Annotation Processing Dynamic SQL Generation Spring Boot Integration Runtime Code Generation

7월 5일 21:57에 게시됨