From fe25fb496e5a564af8d76a63219fec3b05472623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=98=81?= Date: Wed, 20 Aug 2025 13:43:30 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EB=B3=80=EA=B2=BD,=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A7=84=ED=96=89=EC=A4=91=20dat?= =?UTF-8?q?asource-proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 +- .../config/DataSourceProxyConfig.java | 279 +++++++++++ .../egovframework/config/MyBatisConfig.java | 88 ---- .../config/MyBatisQueryInterceptor.java | 450 ------------------ src/main/resources/application-dev.yml | 3 +- src/main/resources/application-local.yml | 3 +- src/main/resources/application-prd.yml | 5 +- src/main/resources/logback-spring.xml | 20 +- .../mybatis/mapper/ma30/Ma30Mapper_maria.xml | 2 +- 9 files changed, 309 insertions(+), 547 deletions(-) create mode 100644 src/main/java/egovframework/config/DataSourceProxyConfig.java delete mode 100644 src/main/java/egovframework/config/MyBatisConfig.java delete mode 100644 src/main/java/egovframework/config/MyBatisQueryInterceptor.java diff --git a/build.gradle b/build.gradle index a3d4bb6..8ce6c85 100644 --- a/build.gradle +++ b/build.gradle @@ -132,9 +132,9 @@ dependencies { // ===== sqlPaser ===== implementation 'com.github.jsqlparser:jsqlparser:4.5' - // ===== DataSource Proxy ===== - // datasource-proxy - 쿼리 로깅 및 모니터링을 위한 데이터소스 프록시 - implementation 'net.ttddyy:datasource-proxy:1.8.1' + // ===== datasource-proxy ===== + // 파라미터 바인딩된 SQL 쿼리 로깅을 위한 datasource-proxy + implementation 'net.ttddyy:datasource-proxy:1.10.1' // ===== 개발 도구 의존성 ===== // Lombok - 반복 코드 생성 도구 (Getter, Setter, Builder 등 자동 생성) diff --git a/src/main/java/egovframework/config/DataSourceProxyConfig.java b/src/main/java/egovframework/config/DataSourceProxyConfig.java new file mode 100644 index 0000000..37b159a --- /dev/null +++ b/src/main/java/egovframework/config/DataSourceProxyConfig.java @@ -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 queryInfoList) { + // 쿼리 실행 전 처리 (필요시 구현) + } + + @Override + public void afterQuery(ExecutionInfo execInfo, List queryInfoList) { + // 쿼리 실행 후 파라미터가 바인딩된 SQL 출력 + for (QueryInfo queryInfo : queryInfoList) { + String query = queryInfo.getQuery(); + List> 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 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 extractParameterValues(List> parametersList) { + java.util.Map parameterMap = new java.util.TreeMap<>(); + + if (parametersList != null && !parametersList.isEmpty()) { + // 첫 번째 파라미터 세트를 사용 (일반적으로 PreparedStatement는 하나의 파라미터 세트를 가짐) + List 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 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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/egovframework/config/MyBatisConfig.java b/src/main/java/egovframework/config/MyBatisConfig.java deleted file mode 100644 index bf869c9..0000000 --- a/src/main/java/egovframework/config/MyBatisConfig.java +++ /dev/null @@ -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가 등록되었습니다 - 상세 쿼리 분석 활성화"); - }; - } -} \ No newline at end of file diff --git a/src/main/java/egovframework/config/MyBatisQueryInterceptor.java b/src/main/java/egovframework/config/MyBatisQueryInterceptor.java deleted file mode 100644 index c07c094..0000000 --- a/src/main/java/egovframework/config/MyBatisQueryInterceptor.java +++ /dev/null @@ -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 parameterMappings = boundSql.getParameterMappings(); - - // 실제 실행될 쿼리 추출 (MyBatis 내부에서 처리된 결과) - String actualSql = getActualSql(boundSql, parameter); - - // 파라미터 정보 추출 - Map 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 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 extractDetailedParameters(Object parameter, List parameterMappings) { - Map paramMap = new HashMap<>(); - - if (parameter == null) { - return paramMap; - } - - if (parameter instanceof Map) { - paramMap.putAll((Map) parameter); - } else { - // 객체의 필드 정보도 추출 - paramMap.put("param", parameter); - extractObjectFields(parameter, paramMap); - } - - return paramMap; - } - - private void extractObjectFields(Object obj, Map 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 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 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 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"); - } -} -} \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 9967fad..7b14a66 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -46,7 +46,8 @@ logging: max-history: 30 level: org.springframework: INFO - go.kr.project: INFO + go.kr.project: DEBUG + go.kr.project.sql: DEBUG # datasource-proxy 파라미터 바인딩된 쿼리 로그 egovframework: DEBUG org.mybatis: INFO diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index f6fb33a..20a6079 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -47,7 +47,8 @@ logging: level: org.springframework: INFO go.kr.project: DEBUG - go.kr.project.sql.query: OFF + go.kr.project.sql: INFO # datasource-proxy + go.kr.project.sql.binding: INFO # datasource-proxy 파라미터 바인딩된 쿼리 로그 egovframework: DEBUG org.mybatis: INFO diff --git a/src/main/resources/application-prd.yml b/src/main/resources/application-prd.yml index 6564822..a9dbcc3 100644 --- a/src/main/resources/application-prd.yml +++ b/src/main/resources/application-prd.yml @@ -46,8 +46,9 @@ logging: max-history: 30 level: org.springframework: INFO - go.kr.project: INFO - egovframework: INFO + go.kr.project: DEBUG + go.kr.project.sql: DEBUG # datasource-proxy 파라미터 바인딩된 쿼리 로그 + egovframework: DEBUG org.mybatis: INFO # File upload configuration diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index db44a6b..e2d9e14 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -50,6 +50,12 @@ + + + + + + @@ -59,6 +65,12 @@ + + + + + + @@ -68,7 +80,13 @@ - + + + + + + + diff --git a/src/main/resources/mybatis/mapper/ma30/Ma30Mapper_maria.xml b/src/main/resources/mybatis/mapper/ma30/Ma30Mapper_maria.xml index b81b11c..dcbf9da 100644 --- a/src/main/resources/mybatis/mapper/ma30/Ma30Mapper_maria.xml +++ b/src/main/resources/mybatis/mapper/ma30/Ma30Mapper_maria.xml @@ -259,7 +259,7 @@ LEFT OUTER JOIN UNLAW_ACT_D_DAEPYO D ON (A.ORG_CD=D.ORG_CD AND A.AREA_TYPE=D.AREA_TYPE AND A.MNG_YY=D.MNG_YY AND A.MNG_NO=D.MNG_NO) LEFT OUTER JOIN UNLAW_POS_D_DAEPYO E ON (D.ORG_CD=E.ORG_CD AND D.AREA_TYPE=E.AREA_TYPE AND D.MNG_YY=E.MNG_YY AND D.MNG_NO=E.MNG_NO AND D.ACT_NO=E.ACT_NO) WHERE A.DEL_YN = 0 - AND A.ORG_CD = #{currentUserOrgCd} /* TODO : 일산서구코드인데 필요한가? */ + AND A.ORG_CD = #{currentUserOrgCd} /* 쿼리 주석안에 ? 가 들어있어... */