마이바티스 파라미터 콘솔 출력 기능 구현 - spring boot interceptor

yuna·2023년 3월 17일
1

web

목록 보기
4/4
post-thumbnail
post-custom-banner

mybatis parameter console output

서론

기존의 log4j를 걷어내고 spring boot + logback을 사용하게 되었는데 logback 설정으로 myBatis 쿼리를 콘솔에 찍는 기능이 있었다. 그러나 log4j의 sqlonly처럼 파라미터가 매핑 되어 있지 않고 ‘?’로 출력 되었다. 콘솔을 그대로 긁어 쿼리를 실행시키고 싶었던 나는 콘솔에만 파라미터를 매핑하여 출력하는 방법을 구색했고 마이바티스 인터셉터를 이용하여 출력하면 되겠다는 생각을 했다.

우선 인터셉터가 무엇인지 간략히 설명을 해보자.

인터셉터(interceptor)

말그대로 동작을 하기 전에 가로채는 것을 말한다. URL에서 Controller로 요청을 할 때 사이에서 동작될 클래스를 말하는 것이며, 레이어별로 인터셉터를 지정할 수 있다.

이번에 활용한 것은 MyBatis Interceptor이며 서비스에서 SQL Mapper를 효출 할 때 개발자가 xml에 작성한 쿼리를 가로채어 동작하는 클래스이다.

@Signature 어노테이션을 분석해보자면

종류설명
typeExecuter라는 인터페이스는 Mybatis의 XML 파일에 작성된 SQL을 실행한다.
해당 인터페이스 내부를 보면 각 mybatis method 시그니처 정보를 볼 수 있다
methodinsert or update or delete가 실행되면 Executer의 update 라는 메소드를 호출하며 select는 query 라는 메소드를 호출한다.
argsmapper에 작성된 method를 호출할때 전달된 파라미터이다.
공통적으로 MapperStatement라는 객체가 Object[] 타입의 인덱스 0번에 필수로 저장된다.
여기에 XML 메타정보가 담겨있다.

본론

처음 구현 계획은 boundSql로 sql을 갖고 오고, 파라미터의 개수만큼 포문을 돌려 순서대로 문자 ‘?’을 replace하려고 했다.

문제는 mybatis에서 foreach로 동적쿼리를 만들면 ParameterMapping에서 ‘__frch_item_0’ 같은 파라미터가 생성되어 적재되었다. 이걸 어쩌지 하면서 불타는 구글링을 한 끝에 이를 예외처리 해놓은 포스팅을 찾을 수 있었고(맨 하단에 출처 기재) 그 코드를 기반으로 더 편하게 볼 수 있도록 약간의 수정을 했다.

MybatisLogInterceptor.java

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
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;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.codehaus.jackson.JsonProcessingException;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;

/**
 * MybatisLogInterceptor.java
 * query console 출력 클래스
 * @author yuna706
 * @since 2023.02.17
 */
@Slf4j
@Intercepts(value = {
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class } )})
public class MybatisLogInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object paramObj = invocation.getArgs()[1];
        MappedStatement statement = (MappedStatement)invocation.getArgs()[0];

        try {
            BoundSql boundSql = statement.getBoundSql(paramObj);
            String paramSql = getParamBindSQL(boundSql);
            log.debug(statement.getResource());
            log.debug(statement.getId());
            log.debug("sql: \n {}", paramSql);
            return invocation.proceed();
        }catch (NoSuchFieldException nsf){
            return invocation.proceed();
        }
    }

    // 파라미터 sql 바인딩 처리
    public String getParamBindSQL(BoundSql boundSql) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, JsonProcessingException {

        Object parameterObject = boundSql.getParameterObject();
        StringBuilder sqlStringBuilder = new StringBuilder(boundSql.getSql());

        // stringBuilder 파라미터 replace 처리
        BiConsumer<StringBuilder, Object> sqlObjectReplace = (sqlSb, value) -> {

            int questionIdx = sqlSb.indexOf("?");

            if(questionIdx == -1) {
                return;
            }

            if(value == null) {
                sqlSb.replace(questionIdx, questionIdx + 1, "null /**P*/");
            } else if(value instanceof String || value instanceof LocalDate || value instanceof LocalDateTime || value instanceof Enum<?>) {
                sqlSb.replace(questionIdx, questionIdx + 1, "'" + (value != null ? value.toString() : "") + "' /**P*/");
            } else {
                sqlSb.replace(questionIdx, questionIdx + 1, value.toString() + " /**P*/");
            }
        };

        if(parameterObject == null) {
            sqlObjectReplace.accept(sqlStringBuilder, null);
        } else {
            if(parameterObject instanceof Integer || parameterObject instanceof Long || parameterObject instanceof Float || parameterObject instanceof Double || parameterObject instanceof String) {
                sqlObjectReplace.accept(sqlStringBuilder, parameterObject);
            } else if(parameterObject instanceof Map) {
                Map paramterObjectMap = (Map)parameterObject;
                List<ParameterMapping> paramMappings = boundSql.getParameterMappings();

                for (ParameterMapping parameterMapping : paramMappings) {
                    String propertyKey = parameterMapping.getProperty();
                    try {
                        Object paramValue = null;
                        if(boundSql.hasAdditionalParameter(propertyKey)) {
                            // 동적 SQL로 인해 __frch_item_0 같은 파라미터가 생성되어 적재됨, additionalParameter로 획득
                            paramValue = boundSql.getAdditionalParameter(propertyKey);
                        } else {
                            paramValue = paramterObjectMap.get(propertyKey);
                        }

                        sqlObjectReplace.accept(sqlStringBuilder, paramValue);
                    } catch (Exception e) {
                        sqlObjectReplace.accept(sqlStringBuilder, "[cannot binding : " + propertyKey+ "]");
                    }

                }
            } else {
                List<ParameterMapping> paramMappings = boundSql.getParameterMappings();
                Class< ? extends Object> paramClass = parameterObject.getClass();

                for (ParameterMapping parameterMapping : paramMappings) {
                    String propertyKey = parameterMapping.getProperty();

                    try {
                        Object paramValue = null;
                        if(boundSql.hasAdditionalParameter(propertyKey)) {
                            // 동적 SQL로 인해 __frch_item_0 같은 파라미터가 생성되어 적재됨, additionalParameter로 획득
                            paramValue = boundSql.getAdditionalParameter(propertyKey);
                        } else {
                            Field field = ReflectionUtils.findField(paramClass, propertyKey);
                            field.setAccessible(true);
                            paramValue = field.get(parameterObject);
                        }

                        sqlObjectReplace.accept(sqlStringBuilder, paramValue);
                    } catch (Exception e) {
                        sqlObjectReplace.accept(sqlStringBuilder, "[cannot binding : " + propertyKey+ "]");
                    }
                }
            }
        }
        return sqlStringBuilder.toString().replaceAll("([\\r\\n\\s]){2,}([\\r\\n])+","\n");
    }
}

결론

인터셉터를 이용하여 실행 될 쿼리를 가로채고, 해당 쿼리에 ‘?’로 매핑되어 있는 파라미터를 순차적으로 매핑하였다.

느낀 점

그 과정에서 BiConsumer를 사용하였고 DTO를 사용해도 순차적으로 탐색할 수 있는 경우를 고려할 수 있게 되었다. 인터셉터에 대한 막연한 이해도가 있었지만 활용을 해보며 더 깊이 알게 된 것 같아 뿌듯하다.

참고

[Mybatis] - Mybatis Interceptor (tistory.com)

개발자(開發者) a developer :: mybatis 파라미터 바인딩 쿼리문 - boundsql __frch 처리 (tistory.com)

profile
나는야 개발자
post-custom-banner

0개의 댓글