parent
fb0f5e987e
commit
fe25fb496e
@ -0,0 +1,279 @@
|
|||||||
|
package egovframework.config;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import net.ttddyy.dsproxy.listener.logging.DefaultQueryLogEntryCreator;
|
||||||
|
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
|
||||||
|
import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener;
|
||||||
|
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
|
||||||
|
import net.ttddyy.dsproxy.ExecutionInfo;
|
||||||
|
import net.ttddyy.dsproxy.QueryInfo;
|
||||||
|
import net.ttddyy.dsproxy.listener.QueryExecutionListener;
|
||||||
|
import net.ttddyy.dsproxy.proxy.ParameterSetOperation;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DataSource Proxy 설정 클래스
|
||||||
|
*
|
||||||
|
* datasource-proxy 라이브러리를 사용하여 파라미터 바인딩이 된 실제 SQL 쿼리를 로그로 출력하도록 설정
|
||||||
|
* MyBatis의 include, foreach 등 복잡한 쿼리에도 적용됨
|
||||||
|
*
|
||||||
|
* Environment를 사용하여 수동으로 DataSource를 구성하고 프록시 적용
|
||||||
|
*
|
||||||
|
* @author XIT Framework
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class DataSourceProxyConfig {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Environment environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실제 데이터소스 빈 생성
|
||||||
|
* Environment에서 설정값을 읽어서 HikariDataSource를 수동으로 구성
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public DataSource actualDataSource() {
|
||||||
|
HikariDataSource dataSource = new HikariDataSource();
|
||||||
|
|
||||||
|
// application.yml에서 설정값 읽기
|
||||||
|
dataSource.setJdbcUrl(environment.getProperty("spring.datasource.url"));
|
||||||
|
dataSource.setUsername(environment.getProperty("spring.datasource.username"));
|
||||||
|
dataSource.setPassword(environment.getProperty("spring.datasource.password"));
|
||||||
|
dataSource.setDriverClassName(environment.getProperty("spring.datasource.driver-class-name"));
|
||||||
|
|
||||||
|
// HikariCP 설정
|
||||||
|
dataSource.setMaximumPoolSize(environment.getProperty("spring.datasource.hikari.maximum-pool-size", Integer.class, 10));
|
||||||
|
dataSource.setMinimumIdle(environment.getProperty("spring.datasource.hikari.minimum-idle", Integer.class, 5));
|
||||||
|
dataSource.setConnectionTimeout(environment.getProperty("spring.datasource.hikari.connection-timeout", Long.class, 30000L));
|
||||||
|
dataSource.setIdleTimeout(environment.getProperty("spring.datasource.hikari.idle-timeout", Long.class, 600000L));
|
||||||
|
dataSource.setMaxLifetime(environment.getProperty("spring.datasource.hikari.max-lifetime", Long.class, 1800000L));
|
||||||
|
dataSource.setValidationTimeout(environment.getProperty("spring.datasource.hikari.validation-timeout", Long.class, 60000L));
|
||||||
|
|
||||||
|
return dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 프록시 데이터소스 빈 생성 (Primary)
|
||||||
|
* actualDataSource를 래핑하여 SQL 쿼리 로깅 기능을 추가
|
||||||
|
*
|
||||||
|
* @param actualDataSource 실제 데이터소스
|
||||||
|
* @return 프록시가 적용된 데이터소스
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public DataSource dataSource(@Qualifier("actualDataSource") DataSource actualDataSource) {
|
||||||
|
// SLF4J 쿼리 로깅 리스너 생성
|
||||||
|
SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener();
|
||||||
|
|
||||||
|
// 로그 레벨 설정 (DEBUG 레벨로 출력)
|
||||||
|
loggingListener.setLogLevel(SLF4JLogLevel.DEBUG);
|
||||||
|
|
||||||
|
// 로거 이름 설정 (쿼리 로그 식별을 위함)
|
||||||
|
loggingListener.setLogger("go.kr.project.sql");
|
||||||
|
|
||||||
|
// 쿼리 로그 엔트리 생성자 설정
|
||||||
|
DefaultQueryLogEntryCreator logEntryCreator = new DefaultQueryLogEntryCreator();
|
||||||
|
logEntryCreator.setMultiline(true); // 멀티라인으로 보기 좋게 출력
|
||||||
|
loggingListener.setQueryLogEntryCreator(logEntryCreator);
|
||||||
|
|
||||||
|
// 커스텀 파라미터 바인딩 리스너 생성
|
||||||
|
CustomParameterBindingListener customListener = new CustomParameterBindingListener();
|
||||||
|
|
||||||
|
// 프록시 데이터소스 빌더를 사용하여 프록시 데이터소스 생성
|
||||||
|
return ProxyDataSourceBuilder
|
||||||
|
.create(actualDataSource)
|
||||||
|
.name("XIT-Framework-DB") // 데이터소스 이름 설정
|
||||||
|
.listener(loggingListener) // 기본 로깅 리스너 추가
|
||||||
|
.listener(customListener) // 커스텀 파라미터 바인딩 리스너 추가
|
||||||
|
.asJson() // JSON 형태로 파라미터 바인딩된 쿼리 출력
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파라미터가 바인딩된 실제 SQL 쿼리를 출력하는 커스텀 리스너
|
||||||
|
*/
|
||||||
|
private static class CustomParameterBindingListener implements QueryExecutionListener {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger("go.kr.project.sql.binding");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
|
||||||
|
// 쿼리 실행 전 처리 (필요시 구현)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
|
||||||
|
// 쿼리 실행 후 파라미터가 바인딩된 SQL 출력
|
||||||
|
for (QueryInfo queryInfo : queryInfoList) {
|
||||||
|
String query = queryInfo.getQuery();
|
||||||
|
List<List<ParameterSetOperation>> parametersList = queryInfo.getParametersList();
|
||||||
|
|
||||||
|
// 디버깅: 원본 파라미터 정보 로그
|
||||||
|
logger.info("[INFO] 원본 파라미터 리스트 크기: {}", parametersList != null ? parametersList.size() : 0);
|
||||||
|
if (parametersList != null && !parametersList.isEmpty()) {
|
||||||
|
logger.info("[INFO] 첫 번째 파라미터 세트 크기: {}", parametersList.get(0).size());
|
||||||
|
for (int i = 0; i < parametersList.get(0).size(); i++) {
|
||||||
|
ParameterSetOperation op = parametersList.get(0).get(i);
|
||||||
|
Object[] args = op.getArgs();
|
||||||
|
logger.info("[INFO] 파라미터 {}: 인덱스={}, 값={}, 타입={}",
|
||||||
|
i, args[0], args[1], args[1] != null ? args[1].getClass().getSimpleName() : "null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파라미터 값 추출
|
||||||
|
List<Object> parameterValues = extractParameterValues(parametersList);
|
||||||
|
|
||||||
|
// 디버깅: 추출된 파라미터 값들 로그
|
||||||
|
logger.info("[INFO] 추출된 파라미터 값들: {}", parameterValues);
|
||||||
|
|
||||||
|
// 디버깅: SQL에서 ? 개수 카운트
|
||||||
|
int questionMarkCount = 0;
|
||||||
|
for (int i = 0; i < query.length(); i++) {
|
||||||
|
if (query.charAt(i) == '?') {
|
||||||
|
questionMarkCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info("[INFO] SQL에서 ? 플레이스홀더 개수: {}, 파라미터 개수: {}", questionMarkCount, parameterValues.size());
|
||||||
|
|
||||||
|
// 파라미터 바인딩된 SQL 생성
|
||||||
|
String boundQuery = bindParameters(query, parameterValues);
|
||||||
|
|
||||||
|
// 로그 출력
|
||||||
|
logger.info("\n========== 파라미터 바인딩된 SQL ==========\n{}\n==========================================", boundQuery);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ParameterSetOperation 리스트에서 실제 파라미터 값들을 추출
|
||||||
|
*/
|
||||||
|
private List<Object> extractParameterValues(List<List<ParameterSetOperation>> parametersList) {
|
||||||
|
java.util.Map<Integer, Object> parameterMap = new java.util.TreeMap<>();
|
||||||
|
|
||||||
|
if (parametersList != null && !parametersList.isEmpty()) {
|
||||||
|
// 첫 번째 파라미터 세트를 사용 (일반적으로 PreparedStatement는 하나의 파라미터 세트를 가짐)
|
||||||
|
List<ParameterSetOperation> operations = parametersList.get(0);
|
||||||
|
|
||||||
|
if (operations != null) {
|
||||||
|
for (ParameterSetOperation operation : operations) {
|
||||||
|
Object[] args = operation.getArgs();
|
||||||
|
if (args != null && args.length >= 2) {
|
||||||
|
// [0]은 파라미터 인덱스 (1-based), [1]은 실제 값
|
||||||
|
Integer paramIndex = (Integer) args[0];
|
||||||
|
Object paramValue = args[1];
|
||||||
|
parameterMap.put(paramIndex, paramValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TreeMap을 사용했으므로 자동으로 인덱스 순서대로 정렬됨
|
||||||
|
return new java.util.ArrayList<>(parameterMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL 쿼리의 ? 플레이스홀더를 실제 파라미터 값으로 치환
|
||||||
|
* 주석 안에 있는 ? 는 치환하지 않음
|
||||||
|
*/
|
||||||
|
private String bindParameters(String query, List<Object> parameters) {
|
||||||
|
if (parameters == null || parameters.isEmpty()) {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
int paramIndex = 0;
|
||||||
|
int queryIndex = 0;
|
||||||
|
boolean inBlockComment = false; // /* */ 블록 주석 내부인지 여부
|
||||||
|
boolean inLineComment = false; // -- 라인 주석 내부인지 여부
|
||||||
|
boolean inStringLiteral = false; // ' ' 문자열 리터럴 내부인지 여부
|
||||||
|
|
||||||
|
while (queryIndex < query.length()) {
|
||||||
|
char currentChar = query.charAt(queryIndex);
|
||||||
|
char nextChar = (queryIndex + 1 < query.length()) ? query.charAt(queryIndex + 1) : '\0';
|
||||||
|
|
||||||
|
// 문자열 리터럴 처리 (홑따옴표로 둘러싸인 문자열)
|
||||||
|
if (currentChar == '\'' && !inBlockComment && !inLineComment) {
|
||||||
|
inStringLiteral = !inStringLiteral;
|
||||||
|
result.append(currentChar);
|
||||||
|
queryIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 블록 주석 시작 /* 검사
|
||||||
|
if (!inStringLiteral && !inLineComment && currentChar == '/' && nextChar == '*') {
|
||||||
|
inBlockComment = true;
|
||||||
|
result.append(currentChar);
|
||||||
|
result.append(nextChar);
|
||||||
|
queryIndex += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 블록 주석 끝 */ 검사
|
||||||
|
if (!inStringLiteral && inBlockComment && currentChar == '*' && nextChar == '/') {
|
||||||
|
inBlockComment = false;
|
||||||
|
result.append(currentChar);
|
||||||
|
result.append(nextChar);
|
||||||
|
queryIndex += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라인 주석 시작 -- 검사
|
||||||
|
if (!inStringLiteral && !inBlockComment && currentChar == '-' && nextChar == '-') {
|
||||||
|
inLineComment = true;
|
||||||
|
result.append(currentChar);
|
||||||
|
result.append(nextChar);
|
||||||
|
queryIndex += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라인 주석 끝 (줄바꿈) 검사
|
||||||
|
if (inLineComment && (currentChar == '\n' || currentChar == '\r')) {
|
||||||
|
inLineComment = false;
|
||||||
|
result.append(currentChar);
|
||||||
|
queryIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ? 파라미터 플레이스홀더 처리 (주석이나 문자열 리터럴 내부가 아닌 경우에만)
|
||||||
|
if (currentChar == '?' && !inBlockComment && !inLineComment && !inStringLiteral) {
|
||||||
|
if (paramIndex < parameters.size()) {
|
||||||
|
// ? 플레이스홀더를 실제 파라미터 값으로 치환
|
||||||
|
Object param = parameters.get(paramIndex);
|
||||||
|
String paramValue;
|
||||||
|
|
||||||
|
if (param == null) {
|
||||||
|
paramValue = "NULL";
|
||||||
|
} else if (param instanceof String) {
|
||||||
|
paramValue = "'" + param.toString().replace("'", "''") + "'";
|
||||||
|
} else if (param instanceof java.util.Date || param instanceof java.time.LocalDateTime || param instanceof java.time.LocalDate) {
|
||||||
|
paramValue = "'" + param.toString() + "'";
|
||||||
|
} else {
|
||||||
|
paramValue = param.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append(paramValue);
|
||||||
|
paramIndex++;
|
||||||
|
} else {
|
||||||
|
// 파라미터가 부족한 경우 ? 그대로 유지
|
||||||
|
result.append('?');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 그 외의 모든 문자는 그대로 복사
|
||||||
|
result.append(currentChar);
|
||||||
|
}
|
||||||
|
queryIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,88 +0,0 @@
|
|||||||
package egovframework.config;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
|
|
||||||
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
|
|
||||||
import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.context.annotation.Primary;
|
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MyBatis Configuration with DataSource Proxy
|
|
||||||
*
|
|
||||||
* datasource-proxy를 이용하여 동적 쿼리와 복잡한 include된 쿼리의
|
|
||||||
* 파라미터 바인딩된 결과를 로깅하도록 설정
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
|
||||||
//@Profile({"local", "dev"}) // local과 dev 프로파일에서만 활성화
|
|
||||||
public class MyBatisConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 기본 DataSource Properties 설정
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
@Primary
|
|
||||||
@ConfigurationProperties("spring.datasource")
|
|
||||||
public DataSourceProperties dataSourceProperties() {
|
|
||||||
return new DataSourceProperties();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 실제 DataSource 생성 (프록시되지 않은 원본)
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
@ConfigurationProperties("spring.datasource.hikari")
|
|
||||||
public DataSource actualDataSource(DataSourceProperties properties) {
|
|
||||||
return properties.initializeDataSourceBuilder().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DataSource Proxy 설정
|
|
||||||
*
|
|
||||||
* datasource-proxy를 사용하여 실제 쿼리와 파라미터 바인딩 결과를 로깅
|
|
||||||
* - 동적 쿼리 (if, choose 등) 처리된 결과 로깅
|
|
||||||
* - include된 복잡한 쿼리 결과 로깅
|
|
||||||
* - 파라미터 바인딩된 최종 쿼리 로깅
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
@Primary
|
|
||||||
public DataSource dataSource(@Qualifier("actualDataSource") DataSource actualDataSource) {
|
|
||||||
return ProxyDataSourceBuilder
|
|
||||||
.create(actualDataSource)
|
|
||||||
.name("XIT-Framework-DataSource")
|
|
||||||
// 쿼리 로깅 리스너 설정 - 실행된 모든 쿼리와 파라미터 바인딩 결과 로깅
|
|
||||||
.logQueryBySlf4j(SLF4JLogLevel.INFO, "go.kr.project.sql.query")
|
|
||||||
// 멀티라인으로 쿼리 포맷팅하여 가독성 향상
|
|
||||||
.multiline()
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MyBatis Query Interceptor 주입
|
|
||||||
*/
|
|
||||||
@Autowired
|
|
||||||
private MyBatisQueryInterceptor myBatisQueryInterceptor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MyBatis Configuration Customizer
|
|
||||||
*
|
|
||||||
* MyBatis에 커스텀 인터셉터를 추가하여
|
|
||||||
* datasource-proxy와 함께 상세한 쿼리 분석 제공
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public ConfigurationCustomizer mybatisConfigurationCustomizer() {
|
|
||||||
return configuration -> {
|
|
||||||
// 커스텀 쿼리 인터셉터 추가
|
|
||||||
configuration.addInterceptor(myBatisQueryInterceptor);
|
|
||||||
log.info("MyBatis Query Interceptor가 등록되었습니다 - 상세 쿼리 분석 활성화");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,450 +0,0 @@
|
|||||||
package egovframework.config;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import net.sf.jsqlparser.JSQLParserException;
|
|
||||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
|
||||||
import net.sf.jsqlparser.schema.Column;
|
|
||||||
import net.sf.jsqlparser.statement.Statement;
|
|
||||||
import net.sf.jsqlparser.statement.delete.Delete;
|
|
||||||
import net.sf.jsqlparser.statement.insert.Insert;
|
|
||||||
import net.sf.jsqlparser.statement.select.Join;
|
|
||||||
import net.sf.jsqlparser.statement.select.PlainSelect;
|
|
||||||
import net.sf.jsqlparser.statement.select.Select;
|
|
||||||
import net.sf.jsqlparser.statement.select.SelectItem;
|
|
||||||
import net.sf.jsqlparser.statement.update.Update;
|
|
||||||
import net.sf.jsqlparser.statement.update.UpdateSet;
|
|
||||||
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.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MyBatis Query Interceptor
|
|
||||||
*
|
|
||||||
* datasource-proxy와 함께 동작하여 상세한 쿼리 분석을 제공
|
|
||||||
* - 동적 쿼리의 처리 과정 분석
|
|
||||||
* - include된 복잡한 쿼리의 상세 정보
|
|
||||||
* - 파라미터 바인딩 전후 비교
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
//@Profile({"local", "dev"}) // local과 dev 프로파일에서만 활성화
|
|
||||||
@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})
|
|
||||||
})
|
|
||||||
@Slf4j
|
|
||||||
public class MyBatisQueryInterceptor implements Interceptor {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object intercept(Invocation invocation) throws Throwable {
|
|
||||||
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
|
|
||||||
Object parameter = invocation.getArgs()[1];
|
|
||||||
|
|
||||||
BoundSql boundSql = ms.getBoundSql(parameter);
|
|
||||||
String sql = boundSql.getSql();
|
|
||||||
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
|
|
||||||
|
|
||||||
// 실제 실행될 쿼리 추출 (MyBatis 내부에서 처리된 결과)
|
|
||||||
String actualSql = getActualSql(boundSql, parameter);
|
|
||||||
|
|
||||||
// 파라미터 정보 추출
|
|
||||||
Map<String, Object> paramMap = extractDetailedParameters(parameter, parameterMappings);
|
|
||||||
|
|
||||||
logDetailedQueryInfo(ms.getId(), sql, actualSql, paramMap);
|
|
||||||
|
|
||||||
// 원래 쿼리 실행
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
Object result = invocation.proceed();
|
|
||||||
long endTime = System.currentTimeMillis();
|
|
||||||
|
|
||||||
// 쿼리 실행 시간 로깅
|
|
||||||
log.debug("Query execution time: {} ms", (endTime - startTime));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getActualSql(BoundSql boundSql, Object parameter) {
|
|
||||||
String sql = boundSql.getSql();
|
|
||||||
|
|
||||||
if (parameter == null) {
|
|
||||||
return sql;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
|
|
||||||
|
|
||||||
if (parameterMappings.isEmpty()) {
|
|
||||||
return sql;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SQL 복사본 생성
|
|
||||||
String actualSql = sql;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 파라미터 값 추출
|
|
||||||
for (ParameterMapping parameterMapping : parameterMappings) {
|
|
||||||
String propertyName = parameterMapping.getProperty();
|
|
||||||
Object value;
|
|
||||||
|
|
||||||
if (boundSql.hasAdditionalParameter(propertyName)) {
|
|
||||||
value = boundSql.getAdditionalParameter(propertyName);
|
|
||||||
} else if (parameter instanceof Map) {
|
|
||||||
value = ((Map<?, ?>) parameter).get(propertyName);
|
|
||||||
} else if (parameter instanceof String) {
|
|
||||||
// String 타입 파라미터 처리
|
|
||||||
value = parameter;
|
|
||||||
} else if (parameter instanceof Number) {
|
|
||||||
// Number 타입 파라미터 처리 (Integer, Long, Double 등)
|
|
||||||
value = parameter;
|
|
||||||
} else if (parameter instanceof Boolean) {
|
|
||||||
// Boolean 타입 파라미터 처리
|
|
||||||
value = parameter;
|
|
||||||
} else {
|
|
||||||
value = getParameterValue(parameter, propertyName);
|
|
||||||
}
|
|
||||||
|
|
||||||
String valueStr = value != null ? value.toString() : "null";
|
|
||||||
// SQL 인젝션 방지를 위한 문자열 이스케이프 처리
|
|
||||||
valueStr = valueStr.replace("'", "''");
|
|
||||||
|
|
||||||
// 다양한 데이터 타입에 대한 처리
|
|
||||||
if (value instanceof String) {
|
|
||||||
// 문자열 타입은 따옴표로 감싸기
|
|
||||||
actualSql = actualSql.replaceFirst("\\?", "'" + valueStr + "'");
|
|
||||||
} else if (value instanceof Number) {
|
|
||||||
// 숫자 타입 (Integer, Long, Double 등)은 그대로 사용
|
|
||||||
actualSql = actualSql.replaceFirst("\\?", valueStr);
|
|
||||||
} else if (value instanceof Boolean) {
|
|
||||||
// Boolean 타입은 그대로 사용
|
|
||||||
actualSql = actualSql.replaceFirst("\\?", valueStr);
|
|
||||||
} else if (value instanceof java.util.Date) {
|
|
||||||
// 표준 타임스탬프 포맷 사용
|
|
||||||
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
|
||||||
actualSql = actualSql.replaceFirst("\\?", "'" + sdf.format(value) + "'");
|
|
||||||
} else if (value instanceof java.time.LocalDateTime) {
|
|
||||||
// Java 8 LocalDateTime 처리
|
|
||||||
actualSql = actualSql.replaceFirst("\\?", "'" + value.toString().replace('T', ' ') + "'");
|
|
||||||
} else {
|
|
||||||
// 기타 타입은 null이 아닌 경우 따옴표로 감싸기
|
|
||||||
actualSql = actualSql.replaceFirst("\\?", value != null ? "'" + valueStr + "'" : valueStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Failed to get actual SQL with parameters", e);
|
|
||||||
return sql;
|
|
||||||
}
|
|
||||||
|
|
||||||
return actualSql;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object getParameterValue(Object parameter, String propertyName) {
|
|
||||||
try {
|
|
||||||
// 현재 클래스부터 상위 클래스까지 순회하면서 필드 찾기
|
|
||||||
Class<?> currentClass = parameter.getClass();
|
|
||||||
while (currentClass != null) {
|
|
||||||
try {
|
|
||||||
Field field = currentClass.getDeclaredField(propertyName);
|
|
||||||
field.setAccessible(true);
|
|
||||||
return field.get(parameter);
|
|
||||||
} catch (NoSuchFieldException e) {
|
|
||||||
// 현재 클래스에서 필드를 찾지 못하면 상위 클래스로 이동
|
|
||||||
currentClass = currentClass.getSuperclass();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new NoSuchFieldException(propertyName);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("필드 값 추출 실패: {} ({})", propertyName, e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> extractDetailedParameters(Object parameter, List<ParameterMapping> parameterMappings) {
|
|
||||||
Map<String, Object> paramMap = new HashMap<>();
|
|
||||||
|
|
||||||
if (parameter == null) {
|
|
||||||
return paramMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameter instanceof Map) {
|
|
||||||
paramMap.putAll((Map<String, Object>) parameter);
|
|
||||||
} else {
|
|
||||||
// 객체의 필드 정보도 추출
|
|
||||||
paramMap.put("param", parameter);
|
|
||||||
extractObjectFields(parameter, paramMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
return paramMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void extractObjectFields(Object obj, Map<String, Object> paramMap) {
|
|
||||||
try {
|
|
||||||
Class<?> currentClass = obj.getClass();
|
|
||||||
while (currentClass != null) {
|
|
||||||
Field[] fields = currentClass.getDeclaredFields();
|
|
||||||
for (Field field : fields) {
|
|
||||||
try {
|
|
||||||
// String 타입의 객체는 건너뛰기
|
|
||||||
if (obj instanceof String) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 시스템 클래스의 필드는 건너뛰기
|
|
||||||
if (field.getDeclaringClass().getName().startsWith("java.")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
field.setAccessible(true);
|
|
||||||
Object value = field.get(obj);
|
|
||||||
paramMap.put(field.getName(), value);
|
|
||||||
|
|
||||||
} catch (IllegalAccessException | SecurityException e) {
|
|
||||||
// 개별 필드 접근 실패는 무시하고 계속 진행
|
|
||||||
log.debug("필드 접근 실패: {} ({})", field.getName(), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentClass = currentClass.getSuperclass();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("객체 필드 추출 실패", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void logDetailedQueryInfo(String mapperMethod, String originalSql, String actualSql, Map<String, Object> paramMap) {
|
|
||||||
StringBuilder logMessage = new StringBuilder();
|
|
||||||
logMessage.append("\n");
|
|
||||||
logMessage.append("┌─────────────── MyBatis Query Details ───────────────\n");
|
|
||||||
logMessage.append("│ Mapper Method: ").append(mapperMethod).append("\n");
|
|
||||||
logMessage.append("│ Parameters: ").append(paramMap).append("\n");
|
|
||||||
|
|
||||||
// 원본 SQL 포맷팅, prd, 운영
|
|
||||||
//logMessage.append("│ Original SQL:\n");
|
|
||||||
//formatSqlInLog(logMessage, originalSql);
|
|
||||||
|
|
||||||
// 실제 실행 SQL 포맷팅, local, dev 로컬 개발
|
|
||||||
logMessage.append("│ Actual SQL:\n");
|
|
||||||
formatSqlInLog(logMessage, actualSql);
|
|
||||||
|
|
||||||
logMessage.append("└──────────────────────────────────────────────────────");
|
|
||||||
|
|
||||||
log.info(logMessage.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatSqlInLog(StringBuilder logMessage, String sql) {
|
|
||||||
// SQL 키워드 하이라이트 및 들여쓰기
|
|
||||||
String formattedSql = formatSql(sql);
|
|
||||||
String[] lines = formattedSql.split("\n");
|
|
||||||
|
|
||||||
for (String line : lines) {
|
|
||||||
//logMessage.append("│ ").append(line).append("\n");
|
|
||||||
logMessage.append(line).append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String formatSql(String sql) {
|
|
||||||
try {
|
|
||||||
// SQL 파서를 사용한 포맷팅
|
|
||||||
Statement statement = CCJSqlParserUtil.parse(sql);
|
|
||||||
return formatStatement(statement, 0);
|
|
||||||
} catch (JSQLParserException e) {
|
|
||||||
log.debug("SQL 파싱 실패. 기본 포맷팅으로 대체합니다.", e);
|
|
||||||
log.info("SQL 파싱 실패. 기본 포맷팅으로 대체합니다.");
|
|
||||||
// 파싱 실패 시 기본 포맷팅 사용
|
|
||||||
return sql.replaceAll("\\s+", " ").trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String formatStatement(Statement statement, int indent) {
|
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
String indentation = String.join("", Collections.nCopies(indent, " "));
|
|
||||||
|
|
||||||
if (statement instanceof Select) {
|
|
||||||
formatSelect((Select) statement, result, indent);
|
|
||||||
} else if (statement instanceof Insert) {
|
|
||||||
formatInsert((Insert) statement, result, indent);
|
|
||||||
} else if (statement instanceof Update) {
|
|
||||||
formatUpdate((Update) statement, result, indent);
|
|
||||||
} else if (statement instanceof Delete) {
|
|
||||||
formatDelete((Delete) statement, result, indent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatSelect(Select select, StringBuilder result, int indent) {
|
|
||||||
String indentation = String.join("", Collections.nCopies(indent, " "));
|
|
||||||
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
|
|
||||||
|
|
||||||
// SELECT 절
|
|
||||||
result.append(indentation).append("SELECT\n");
|
|
||||||
formatSelectItems(plainSelect.getSelectItems(), result, indent + 2);
|
|
||||||
|
|
||||||
// FROM 절
|
|
||||||
if (plainSelect.getFromItem() != null) {
|
|
||||||
result.append(indentation).append("FROM\n");
|
|
||||||
result.append(indentation).append(" ").append(plainSelect.getFromItem()).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// JOIN 절
|
|
||||||
if (plainSelect.getJoins() != null) {
|
|
||||||
for (Join join : plainSelect.getJoins()) {
|
|
||||||
result.append(indentation).append(join.isLeft() ? "LEFT JOIN " : "JOIN ")
|
|
||||||
.append(join.getRightItem()).append("\n");
|
|
||||||
if (join.getOnExpression() != null) {
|
|
||||||
result.append(indentation).append(" ON ").append(join.getOnExpression()).append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WHERE 절
|
|
||||||
if (plainSelect.getWhere() != null) {
|
|
||||||
result.append(indentation).append("WHERE\n");
|
|
||||||
result.append(indentation).append(" ").append(plainSelect.getWhere()).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// GROUP BY 절
|
|
||||||
if (plainSelect.getGroupBy() != null) {
|
|
||||||
result.append(indentation).append("GROUP BY\n");
|
|
||||||
result.append(indentation).append(" ")
|
|
||||||
.append(plainSelect.getGroupBy().getGroupByExpressions().stream()
|
|
||||||
.map(Object::toString)
|
|
||||||
.collect(Collectors.joining(", ")))
|
|
||||||
.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// HAVING 절
|
|
||||||
if (plainSelect.getHaving() != null) {
|
|
||||||
result.append(indentation).append("HAVING\n");
|
|
||||||
result.append(indentation).append(" ").append(plainSelect.getHaving()).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ORDER BY 절
|
|
||||||
if (plainSelect.getOrderByElements() != null) {
|
|
||||||
result.append(indentation).append("ORDER BY\n");
|
|
||||||
result.append(indentation).append(" ")
|
|
||||||
.append(plainSelect.getOrderByElements().stream()
|
|
||||||
.map(Object::toString)
|
|
||||||
.collect(Collectors.joining(", ")))
|
|
||||||
.append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatSelectItems(List<SelectItem> items, StringBuilder result, int indent) {
|
|
||||||
String indentation = String.join("", Collections.nCopies(indent, " "));
|
|
||||||
for (int i = 0; i < items.size(); i++) {
|
|
||||||
result.append(indentation).append(items.get(i));
|
|
||||||
if (i < items.size() - 1) {
|
|
||||||
result.append(",");
|
|
||||||
}
|
|
||||||
result.append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatInsert(Insert insert, StringBuilder result, int indent) {
|
|
||||||
String indentation = String.join("", Collections.nCopies(indent, " "));
|
|
||||||
result.append(indentation).append("INSERT INTO ").append(insert.getTable()).append("\n");
|
|
||||||
|
|
||||||
// 컬럼 목록
|
|
||||||
if (insert.getColumns() != null) {
|
|
||||||
result.append(indentation).append("(")
|
|
||||||
.append(insert.getColumns().stream()
|
|
||||||
.map(Column::getColumnName)
|
|
||||||
.collect(Collectors.joining(", ")))
|
|
||||||
.append(")\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
result.append(indentation).append("VALUES\n");
|
|
||||||
// VALUES 절 포맷팅
|
|
||||||
if (insert.getItemsList() != null) {
|
|
||||||
result.append(indentation).append(" ").append(insert.getItemsList()).append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatUpdate(Update update, StringBuilder result, int indent) {
|
|
||||||
String indentation = String.join("", Collections.nCopies(indent, " "));
|
|
||||||
|
|
||||||
// UPDATE 절
|
|
||||||
result.append(indentation).append("UPDATE\n");
|
|
||||||
result.append(indentation).append(" ").append(update.getTable()).append("\n");
|
|
||||||
|
|
||||||
// SET 절
|
|
||||||
result.append(indentation).append("SET\n");
|
|
||||||
List<UpdateSet> updateSets = update.getUpdateSets();
|
|
||||||
for (int i = 0; i < updateSets.size(); i++) {
|
|
||||||
UpdateSet updateSet = updateSets.get(i);
|
|
||||||
result.append(indentation).append(" ")
|
|
||||||
.append(updateSet.getColumns().get(0))
|
|
||||||
.append(" = ")
|
|
||||||
.append(updateSet.getExpressions().get(0));
|
|
||||||
|
|
||||||
if (i < updateSets.size() - 1) {
|
|
||||||
result.append(",");
|
|
||||||
}
|
|
||||||
result.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// WHERE 절
|
|
||||||
if (update.getWhere() != null) {
|
|
||||||
result.append(indentation).append("WHERE\n");
|
|
||||||
result.append(indentation).append(" ").append(update.getWhere()).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ORDER BY 절 (일부 데이터베이스에서 지원)
|
|
||||||
if (update.getOrderByElements() != null) {
|
|
||||||
result.append(indentation).append("ORDER BY\n");
|
|
||||||
result.append(indentation).append(" ")
|
|
||||||
.append(update.getOrderByElements().stream()
|
|
||||||
.map(Object::toString)
|
|
||||||
.collect(Collectors.joining(", ")))
|
|
||||||
.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// LIMIT 절 (일부 데이터베이스에서 지원)
|
|
||||||
if (update.getLimit() != null) {
|
|
||||||
result.append(indentation).append("LIMIT ")
|
|
||||||
.append(update.getLimit()).append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatDelete(Delete delete, StringBuilder result, int indent) {
|
|
||||||
String indentation = String.join("", Collections.nCopies(indent, " "));
|
|
||||||
|
|
||||||
// DELETE 절
|
|
||||||
result.append(indentation).append("DELETE FROM\n");
|
|
||||||
result.append(indentation).append(" ").append(delete.getTable()).append("\n");
|
|
||||||
|
|
||||||
// WHERE 절
|
|
||||||
if (delete.getWhere() != null) {
|
|
||||||
result.append(indentation).append("WHERE\n");
|
|
||||||
result.append(indentation).append(" ").append(delete.getWhere()).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ORDER BY 절 (일부 데이터베이스에서 지원)
|
|
||||||
if (delete.getOrderByElements() != null) {
|
|
||||||
result.append(indentation).append("ORDER BY\n");
|
|
||||||
result.append(indentation).append(" ")
|
|
||||||
.append(delete.getOrderByElements().stream()
|
|
||||||
.map(Object::toString)
|
|
||||||
.collect(Collectors.joining(", ")))
|
|
||||||
.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// LIMIT 절 (일부 데이터베이스에서 지원)
|
|
||||||
if (delete.getLimit() != null) {
|
|
||||||
result.append(indentation).append("LIMIT ")
|
|
||||||
.append(delete.getLimit()).append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue