Mybatis Log interceptor 정리(JAVA)

Click·2025년 2월 28일
0

Mybatis interceptor 기반으로 logging하는 소스.

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.Statement;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;

@Slf4j
@Intercepts({
        @Signature(type = StatementHandler.class, method = "parameterize", args = { Statement.class })
})
public class MybatisLogInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();

        // 쿼리문을 가져온다(이 상태에서의 쿼리는 값이 들어갈 부분에 ?가 있다)
        String sql = boundSql.getSql();


        // 쿼리실행시 맵핑되는 파라미터를 구한다
        Object param = handler.getParameterHandler().getParameterObject();

        if (param == null) {                // 파라미터가 아무것도 없을 경우
            sql = sql.replaceFirst("\\?", "''");
        }
        else {                        // 해당 파라미터의 클래스가 Integer, Long, Float, Double 클래스일 경우
            if (param instanceof Integer || param instanceof Long || param instanceof Float || param instanceof Double) {
                sql = sql.replaceFirst("\\?", param.toString());
            }
            else if (param instanceof String) {    // 해당 파라미터의 클래스가 String 일 경우(이 경우는 앞뒤에 '(홑따옴표)를 붙여야해서 별도 처리
                sql = sql.replaceFirst("\\?", "'" + param + "'");
            }
            else if (param instanceof Map) {        // 해당 파라미터가 Map 일 경우

                /*
                 * 쿼리의 ?와 매핑되는 실제 값들의 정보가 들어있는 ParameterMapping 객체가 들어간 List 객체로 return이 된다.
                 * 이때 List 객체의 0번째 순서에 있는 ParameterMapping 객체가 쿼리의 첫번째 ?와 매핑이 된다
                 * 이런 식으로 쿼리의 ?과 ParameterMapping 객체들을 Mapping 한다
                 */
                List<ParameterMapping> paramMapping = boundSql.getParameterMappings();

                for (ParameterMapping mapping : paramMapping) {
                    String propValue = mapping.getProperty();        // 파라미터로 넘긴 Map의 key 값이 들어오게 된다
                    Object value = ((Map) param).get(propValue);    // 넘겨받은 key 값을 이용해 실제 값을 꺼낸다
                    if (value instanceof String) {            // SQL의 ? 대신에 실제 값을 넣는다. 이때 String 일 경우는 '를 붙여야 하기땜에 별도 처리
                        sql = sql.replaceFirst("\\?", "'" + value + "'");
                    }
                    else {
                        sql = sql.replaceFirst("\\?", value.toString());
                    }

                }
            }
            else {                    // 해당 파라미터가 사용자 정의 클래스일 경우

                /*
                 * 쿼리의 ?와 매핑되는 실제 값들이 List 객체로 return이 된다.
                 * 이때 List 객체의 0번째 순서에 있는 ParameterMapping 객체가 쿼리의 첫번째 ?와 매핑이 된다
                 * 이런 식으로 쿼리의 ?과 ParameterMapping 객체들을 Mapping 한다
                 */
                List<ParameterMapping> paramMapping = boundSql.getParameterMappings();

                Class<? extends Object> paramClass = param.getClass();
                // logger.debug("paramClass.getName() : {}", paramClass.getName());
                for (ParameterMapping mapping : paramMapping) {
                    String propValue = mapping.getProperty();            // 해당 파라미터로 넘겨받은 사용자 정의 클래스 객체의 멤버변수명
                    Field field = paramClass.getDeclaredField(propValue);    // 관련 멤버변수 Field 객체 얻어옴
                    field.setAccessible(true);                    // 멤버변수의 접근자가 private일 경우 reflection을 이용하여 값을 해당
                    // 멤버변수의 값을 가져오기 위해 별도로 셋팅
                    Class<?> javaType = mapping.getJavaType();            // 해당 파라미터로 넘겨받은 사용자 정의 클래스 객체의 멤버변수의 타입

                    if (String.class == javaType) {                // SQL의 ? 대신에 실제 값을 넣는다. 이때 String 일 경우는 SQL string으로 변환 시 ' 를 붙여야 하기때문에 별도 처리
                        sql = sql.replaceFirst("\\?", "'" + field.get(param) + "'");
                    }
                    else {
                        sql = sql.replaceFirst("\\?", field.get(param).toString());
                    }

                }
            }

        }
        log.info("sql : \n{}", sql);
        long start = System.currentTimeMillis();
        Object retVal = invocation.proceed(); // 쿼리 실행
        long end = System.currentTimeMillis();
        log.info("elapsed time : {} ms", end - start);
        log.info("=====================================================================");
        return retVal;
    }

    @Override
    public Object plugin(Object target) {
        return Interceptor.super.plugin(target);
    }

    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }

}

yaml Property에서 가져와 auditing하는 interceptor

다음과 같은 프로퍼티를 정의했다고 가정하자.

myproperties:
  execution-location: "myhome"

위와 같은 yml을 저장하는 configuration class는 다음과 같이 정의할 수 있다.

@ConfigurationPropertis(prefix = "myproperties")
public record MyProperties(
   String executionLocation
) {}

configurationspring bean으로 정의된다. executionLocation을 mybatis mapper 내 어느 쿼리에서든 사용하고 싶을 때, interceptor를 사용해볼 수 있다.

일단 interceptor를 정의해보자.

LocationInterceptor

@Slf4j
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }),
        @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class })
})
@RequiredArgsConstructor
@Component
public class LocationInterceptor implements Interceptor {
    private final MyProperties myProperties;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        Object[] args = invocation.getArgs();
        Object param = args[1];
        Object newParam = setAuditing(param);
        args[1] = newParam;
        final String sqlId = ((MappedStatement) args[0]).getId();
        // MybatisLogInterceptor에서는 StatementHandler 계층을 intercept하기 때문에 MappedStatement를 가져올 수 없다.
        // 그러므로 현 시점에서 sql id를 찍는다
        log.info("====== sqlId: {}", sqlId);
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Interceptor.super.plugin(target);
    }

    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }

    private Object setAuditing(Object param) {
        
        MapperMethod.ParamMap<Object> newParam = null;
        if (param instanceof MapperMethod.ParamMap<?>) {
            newParam = (MapperMethod.ParamMap<Object>) param;
            newParam.put("executionLocation", myProperties.executionLocation());
        }
        else if (param == null) {
            // 파라미터가 없는 경우 강제로 넣어준다
            newParam = new MapperMethod.ParamMap<>();
            newParam.put("executionLocation", myProperties.executionLocation());
        }
        return newParam;
    }
}

다음으로는 mybatis session을 자바 코드로 설정해준다. 스프링 빈을 가져와야하기 때문에 xml설정보다는 java 설정으로 진행하는 편이 간편하다.

MybatisConfig

@Configuration
@RequiredArgsConstructor
public class MybatisConfig {
	private final LocationInterceptor interceptor;
    
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws 
	
}

profile
갈려나가는 개발자

0개의 댓글

관련 채용 정보