DataSource 프록시 적용 및 MyBatis 설정 변경

dev
박성영 4 months ago
parent 3ea207657d
commit 77232f4bf3

@ -132,6 +132,10 @@ dependencies {
// ===== sqlPaser =====
implementation 'com.github.jsqlparser:jsqlparser:4.5'
// ===== DataSource Proxy =====
// datasource-proxy -
implementation 'net.ttddyy:datasource-proxy:1.8.1'
// ===== =====
// Lombok - (Getter, Setter, Builder )
compileOnly 'org.projectlombok:lombok'

@ -1,27 +1,88 @@
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.Profile;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
/**
* MyBatis Configuration
* MyBatis Configuration with DataSource Proxy
*
* This class configures MyBatis by adding custom interceptors.
* datasource-proxy include
*
*/
@Slf4j
@Configuration
@Profile({"local", "dev"}) // local과 dev 프로파일에서만 활성화
//@Profile({"local", "dev"}) // local과 dev 프로파일에서만 활성화
public class MyBatisConfig {
/**
* Customizes the MyBatis configuration by adding the query interceptor.
* 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
*
* @param interceptor The MyBatis query interceptor
* @return A ConfigurationCustomizer that adds the interceptor to MyBatis
* MyBatis
* datasource-proxy
*/
@Bean
public ConfigurationCustomizer mybatisConfigurationCustomizer(MyBatisQueryInterceptor interceptor) {
return configuration -> configuration.addInterceptor(interceptor);
public ConfigurationCustomizer mybatisConfigurationCustomizer() {
return configuration -> {
// 커스텀 쿼리 인터셉터 추가
configuration.addInterceptor(myBatisQueryInterceptor);
log.info("MyBatis Query Interceptor가 등록되었습니다 - 상세 쿼리 분석 활성화");
};
}
}

@ -13,7 +13,6 @@ 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.binding.MapperMethod;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
@ -24,39 +23,25 @@ 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.context.annotation.Profile;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* MyBatis
*
* MyBatis SQL .
* SQL, SQL( ), .
* local dev .
*
* :
* - SQL
* -
* -
* - include SQL
* - (String, Number, Boolean, Date, LocalDateTime )
* MyBatis Query Interceptor
*
* datasource-proxy
* -
* - include
* -
*/
@Component
@Profile({"local", "dev"}) // local과 dev 프로파일에서만 활성화
//@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})
@ -64,299 +49,105 @@ import java.util.stream.Collectors;
@Slf4j
public class MyBatisQueryInterceptor implements Interceptor {
/**
* .
*
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
return org.apache.ibatis.plugin.Plugin.wrap(target, this);
}
/**
* .
*
* @param properties
*/
@Override
public void setProperties(java.util.Properties properties) {
// 필요한 경우 속성 설정
}
/**
* MyBatis .
* include SQL .
*
* @param invocation MyBatis
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 로깅 레벨 확인으로 불필요한 처리 방지
boolean isDebugEnabled = log.isDebugEnabled();
boolean isInfoEnabled = log.isInfoEnabled();
if (!isInfoEnabled && !isDebugEnabled) {
// 로깅이 비활성화된 경우 바로 원래 쿼리 실행
return invocation.proceed();
}
try {
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();
// 쿼리 실행 시간 로깅
if (isDebugEnabled) {
log.debug("쿼리 실행 시간: {} ms", (endTime - startTime));
}
return result;
} catch (Exception e) {
// 인터셉터 내부 오류는 로깅하고 원래 쿼리 실행 (인터셉터 오류로 쿼리가 실패하지 않도록)
log.warn("쿼리 인터셉터 처리 중 오류 발생: {}", e.getMessage());
if (isDebugEnabled) {
log.debug("인터셉터 오류 상세 정보:", e);
}
return invocation.proceed();
}
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;
}
/**
* SQL .
* SQL '?' .
* include SQL .
* foreach null .
*/
private String getActualSql(BoundSql boundSql, Object parameter) {
String sql = boundSql.getSql();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
if (parameter == null) {
return sql;
}
// SQL에서 실제 파라미터 바인딩 위치 확인
Pattern pattern = Pattern.compile("\\?");
Matcher matcher = pattern.matcher(sql);
StringBuffer result = new StringBuffer();
int paramIndex = 0;
// IN 절 패턴 (대소문자 구분 없이)
Pattern inClausePattern = Pattern.compile("\\s+IN\\s*\\(\\s*\\?\\s*\\)", Pattern.CASE_INSENSITIVE);
Matcher inClauseMatcher = inClausePattern.matcher(sql);
Map<Integer, Boolean> inClausePositions = new HashMap<>();
// IN 절 위치 찾기
while (inClauseMatcher.find()) {
// 해당 위치의 ? 인덱스 찾기
String subSql = sql.substring(0, inClauseMatcher.end());
int questionMarkCount = 0;
for (int i = 0; i < subSql.length(); i++) {
if (subSql.charAt(i) == '?') {
questionMarkCount++;
}
}
// 인덱스는 0부터 시작하므로 -1
inClausePositions.put(questionMarkCount - 1, true);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings.isEmpty()) {
return sql;
}
while (matcher.find() && paramIndex < parameterMappings.size()) {
ParameterMapping parameterMapping = parameterMappings.get(paramIndex);
Object value = null;
try {
// include SQL 내부의 파라미터 처리 개선
if (parameter instanceof MapperMethod.ParamMap) {
MapperMethod.ParamMap<?> paramMap = (MapperMethod.ParamMap<?>) parameter;
String propertyName = parameterMapping.getProperty();
// 직접 키로 접근
if (paramMap.containsKey(propertyName)) {
value = paramMap.get(propertyName);
}
// param1, param2 등으로 접근
else if (propertyName.matches("param\\d+")) {
value = paramMap.get(propertyName);
}
// 중첩 객체 내부 필드 접근
else if (propertyName.contains(".")) {
value = getNestedParameterValue(paramMap, propertyName);
}
// VO 객체 내부 필드 접근
else {
for (Map.Entry<String, ?> entry : paramMap.entrySet()) {
if (entry.getValue() != null) {
Object nestedValue = getParameterValue(entry.getValue(), propertyName);
if (nestedValue != null) {
value = nestedValue;
break;
}
}
}
}
} else {
value = getParameterValue(parameter, parameterMapping.getProperty());
}
// IN 절 특별 처리
boolean isInClause = inClausePositions.containsKey(paramIndex);
// IN 절이고 컬렉션이 비어있거나 null인 경우 특별 처리
if (isInClause) {
if (value == null) {
// IN (NULL) 대신 IN (0)으로 대체 (항상 false가 되도록)
matcher.appendReplacement(result, "0");
log.debug("IN 절 NULL 파라미터 감지: 0으로 대체함 (파라미터: {})", parameterMapping.getProperty());
} else if (value instanceof Collection && ((Collection<?>) value).isEmpty()) {
// 빈 컬렉션인 경우 IN (0)으로 대체
matcher.appendReplacement(result, "0");
log.debug("IN 절 빈 컬렉션 감지: 0으로 대체함 (파라미터: {})", parameterMapping.getProperty());
} else if (value.getClass().isArray() && java.lang.reflect.Array.getLength(value) == 0) {
// 빈 배열인 경우 IN (0)으로 대체
matcher.appendReplacement(result, "0");
log.debug("IN 절 빈 배열 감지: 0으로 대체함 (파라미터: {})", parameterMapping.getProperty());
} else {
// 정상적인 컬렉션/배열인 경우 일반 처리
String replacement = formatParameterValue(value);
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
}
// 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 {
// 일반 파라미터 처리
String replacement = formatParameterValue(value);
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
value = getParameterValue(parameter, propertyName);
}
} catch (Exception e) {
log.debug("파라미터 값 추출 실패: {}, 기본값 사용", parameterMapping.getProperty());
matcher.appendReplacement(result, "?");
}
paramIndex++;
}
matcher.appendTail(result);
return result.toString();
}
/**
* .
*/
private Object getNestedParameterValue(MapperMethod.ParamMap<?> paramMap, String propertyPath) {
String[] parts = propertyPath.split("\\.");
Object current = null;
// 첫 번째 키로 객체 찾기
for (Map.Entry<String, ?> entry : paramMap.entrySet()) {
if (entry.getKey().equals(parts[0]) ||
(entry.getValue() != null && entry.getValue().getClass().getSimpleName().toLowerCase().contains(parts[0].toLowerCase()))) {
current = entry.getValue();
break;
}
}
// 중첩 필드 접근
for (int i = 1; i < parts.length && current != null; i++) {
current = getParameterValue(current, parts[i]);
}
return current;
}
String valueStr = value != null ? value.toString() : "null";
// SQL 인젝션 방지를 위한 문자열 이스케이프 처리
valueStr = valueStr.replace("'", "''");
/**
* SQL .
* null .
*/
private String formatParameterValue(Object value) {
if (value == null) {
return "NULL";
}
// 컬렉션 타입 처리 (List, Set, Array 등)
if (value instanceof Collection) {
Collection<?> collection = (Collection<?>) value;
if (collection.isEmpty()) {
// 빈 컬렉션인 경우 '0'을 반환하여 IN (0)으로 만듦
// 이는 항상 false가 되어 결과가 없음을 보장함 (어떤 ID도 0과 일치하지 않음)
return "0";
}
// 컬렉션 요소를 쉼표로 구분된 문자열로 변환
return collection.stream()
.map(this::formatParameterValue)
.collect(Collectors.joining(", "));
} else if (value.getClass().isArray()) {
// 배열 타입 처리
Object[] array = convertToObjectArray(value);
if (array.length == 0) {
// 빈 배열인 경우 '0'을 반환
return "0";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < array.length; i++) {
if (i > 0) {
sb.append(", ");
// 다양한 데이터 타입에 대한 처리
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);
}
sb.append(formatParameterValue(array[i]));
}
return sb.toString();
}
if (value instanceof String) {
return "'" + value.toString().replace("'", "''") + "'";
} else if (value instanceof Number || value instanceof Boolean) {
return value.toString();
} else if (value instanceof Date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return "'" + sdf.format((Date) value) + "'";
} else if (value instanceof java.time.LocalDateTime) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return "'" + ((java.time.LocalDateTime) value).format(formatter) + "'";
} else if (value instanceof java.time.LocalDate) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
return "'" + ((java.time.LocalDate) value).format(formatter) + "'";
} else {
return "'" + value.toString().replace("'", "''") + "'";
} catch (Exception e) {
log.warn("Failed to get actual SQL with parameters", e);
return sql;
}
}
/**
* Object[] .
*/
private Object[] convertToObjectArray(Object array) {
if (array instanceof Object[]) {
return (Object[]) array;
}
// 기본 타입 배열 처리
int length = java.lang.reflect.Array.getLength(array);
Object[] result = new Object[length];
for (int i = 0; i < length; i++) {
result[i] = java.lang.reflect.Array.get(array, i);
}
return result;
return actualSql;
}
private Object getParameterValue(Object parameter, String propertyName) {
@ -380,308 +171,113 @@ private Object getParameterValue(Object parameter, String propertyName) {
}
}
/**
* .
* include SQL .
*/
private Map<String, Object> extractDetailedParameters(Object parameter, List<ParameterMapping> parameterMappings) {
Map<String, Object> paramMap = new LinkedHashMap<>();
if (parameter == null || parameterMappings == null || parameterMappings.isEmpty()) {
Map<String, Object> paramMap = new HashMap<>();
if (parameter == null) {
return paramMap;
}
// 실제 사용되는 파라미터만 추출
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
Object value = null;
try {
if (parameter instanceof MapperMethod.ParamMap) {
MapperMethod.ParamMap<?> mapperParamMap = (MapperMethod.ParamMap<?>) parameter;
// 1. 직접 키로 접근
if (mapperParamMap.containsKey(propertyName)) {
value = mapperParamMap.get(propertyName);
}
// 2. 중첩 객체 내부 필드 접근 (include SQL에서 자주 발생)
else if (propertyName.contains(".")) {
value = getNestedParameterValue(mapperParamMap, propertyName);
}
// 3. VO 객체 내부 검색
else {
for (Map.Entry<String, ?> entry : mapperParamMap.entrySet()) {
if (entry.getValue() != null) {
Object nestedValue = getParameterValue(entry.getValue(), propertyName);
if (nestedValue != null) {
value = nestedValue;
break;
}
}
}
}
// 4. 전체 맵 내용도 포함 (디버깅용)
paramMap.putAll(mapperParamMap);
} else {
value = getParameterValue(parameter, propertyName);
extractObjectFields(parameter, paramMap);
}
if (value != null) {
paramMap.put(propertyName, value);
}
} catch (Exception e) {
log.debug("파라미터 추출 실패: {} - {}", propertyName, e.getMessage());
}
if (parameter instanceof Map) {
paramMap.putAll((Map<String, Object>) parameter);
} else {
// 객체의 필드 정보도 추출
paramMap.put("param", parameter);
extractObjectFields(parameter, paramMap);
}
return paramMap;
}
/**
* .
* include SQL .
*
* @param obj
* @param paramMap
*/
private void extractObjectFields(Object obj, Map<String, Object> paramMap) {
if (obj == null) {
return;
}
// 기본 타입이나 래퍼 타입은 처리하지 않음
if (obj instanceof String || obj instanceof Number || obj instanceof Boolean ||
obj instanceof java.util.Date || obj instanceof java.time.temporal.Temporal) {
return;
}
try {
Class<?> currentClass = obj.getClass();
// 시스템 클래스는 처리하지 않음
if (currentClass.getName().startsWith("java.") ||
currentClass.getName().startsWith("javax.") ||
currentClass.getName().startsWith("sun.")) {
return;
}
// 현재 클래스부터 상위 클래스까지 순회하면서 필드 추출
while (currentClass != null && !currentClass.getName().startsWith("java.")) {
while (currentClass != null) {
Field[] fields = currentClass.getDeclaredFields();
for (Field field : fields) {
try {
// static 또는 transient 필드는 건너뛰기
if (java.lang.reflect.Modifier.isStatic(field.getModifiers()) ||
java.lang.reflect.Modifier.isTransient(field.getModifiers())) {
// String 타입의 객체는 건너뛰기
if (obj instanceof String) {
continue;
}
// 시스템 클래스의 필드는 건너뛰기
if (field.getDeclaringClass().getName().startsWith("java.")) {
continue;
}
field.setAccessible(true);
Object value = field.get(obj);
// 값이 null이 아닌 경우만 맵에 추가
if (value != null) {
String fieldName = field.getName();
paramMap.put(fieldName, value);
// MANAGER_CD 특별 처리 (include SQL 내부에서도 적용)
if (fieldName.equalsIgnoreCase("currentUserId")) {
paramMap.put("MANAGER_CD", value);
paramMap.put("manager_cd", value);
}
// 중첩된 객체의 필드도 추출 (깊이 1까지만)
if (!(value instanceof Map) &&
!(value instanceof Collection) &&
!(value instanceof String) &&
!(value instanceof Number) &&
!(value instanceof Boolean) &&
!value.getClass().getName().startsWith("java.")) {
// 중첩된 객체의 필드를 추출하여 필드명.중첩필드명 형태로 저장
Class<?> nestedClass = value.getClass();
Field[] nestedFields = nestedClass.getDeclaredFields();
for (Field nestedField : nestedFields) {
try {
if (java.lang.reflect.Modifier.isStatic(nestedField.getModifiers()) ||
java.lang.reflect.Modifier.isTransient(nestedField.getModifiers())) {
continue;
}
nestedField.setAccessible(true);
Object nestedValue = nestedField.get(value);
if (nestedValue != null) {
String nestedFieldName = nestedField.getName();
paramMap.put(nestedFieldName, nestedValue);
}
} catch (IllegalAccessException | SecurityException e) {
// 중첩 필드 접근 실패는 무시하고 계속 진행
}
}
}
}
paramMap.put(field.getName(), value);
} catch (IllegalAccessException | SecurityException e) {
// 개별 필드 접근 실패는 무시하고 계속 진행
if (log.isDebugEnabled()) {
log.debug("필드 접근 실패: {} ({})", field.getName(), e.getMessage());
}
log.debug("필드 접근 실패: {} ({})", field.getName(), e.getMessage());
}
}
currentClass = currentClass.getSuperclass();
}
} catch (Exception e) {
log.warn("객체 필드 추출 중 오류 발생: {}", e.getMessage());
if (log.isDebugEnabled()) {
log.debug("상세 오류 정보:", e);
}
log.warn("객체 필드 추출 실패", e);
}
}
/**
* .
*
* @param mapperMethod
* @param originalSql SQL
* @param actualSql SQL ( )
* @param paramMap
*/
private void logDetailedQueryInfo(String mapperMethod, String originalSql, String actualSql, Map<String, Object> paramMap) {
// 로깅 레벨 확인으로 불필요한 문자열 연산 방지
if (!log.isInfoEnabled()) {
return;
}
StringBuilder logMessage = new StringBuilder();
logMessage.append("\n");
logMessage.append("┌─────────────── MyBatis 쿼리 상세 정보 ───────────────\n");
logMessage.append("│ 매퍼 메서드: ").append(mapperMethod).append("\n");
// 파라미터 정보 로깅 (너무 길면 축약)
String paramString = paramMap.toString();
if (paramString.length() > 1000) {
paramString = paramString.substring(0, 997) + "...";
}
logMessage.append("│ 파라미터: ").append(paramString).append("\n");
// 실제 실행 SQL 포맷팅 (local, dev 환경에서만)
logMessage.append("│ 실행 SQL:\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());
}
/**
* SQL .
*
* @param logMessage
* @param sql SQL
*/
private void formatSqlInLog(StringBuilder logMessage, String sql) {
try {
// SQL 키워드 하이라이트 및 들여쓰기
String formattedSql = formatSql(sql);
String[] lines = formattedSql.split("\n");
// SQL 키워드 하이라이트 및 들여쓰기
String formattedSql = formatSql(sql);
String[] lines = formattedSql.split("\n");
for (String line : lines) {
logMessage.append("│ ").append(line).append("\n");
}
} catch (Exception e) {
// 포맷팅 실패 시 원본 SQL 출력
logMessage.append("│ ").append(sql).append("\n");
log.debug("SQL 포맷팅 실패: {}", e.getMessage());
for (String line : lines) {
//logMessage.append("│ ").append(line).append("\n");
logMessage.append(line).append("\n");
}
}
/**
* SQL .
*
* @param sql SQL
* @return SQL
*/
private String formatSql(String sql) {
if (sql == null || sql.trim().isEmpty()) {
return "";
}
try {
// SQL 파서를 사용한 포맷팅
Statement statement = CCJSqlParserUtil.parse(sql);
return formatStatement(statement, 0);
} catch (JSQLParserException e) {
log.debug("SQL 파싱 실패. 기본 포맷팅으로 대체합니다.", e);
log.info("SQL 파싱 실패. 기본 포맷팅으로 대체합니다.");
// 파싱 실패 시 기본 포맷팅 사용
if (log.isDebugEnabled()) {
log.debug("SQL 파싱 실패. 기본 포맷팅으로 대체합니다: {}", e.getMessage());
}
// 기본 포맷팅: 키워드 대문자화 및 줄바꿈 추가
String formattedSql = sql.replaceAll("\\s+", " ").trim();
// 주요 SQL 키워드 앞에 줄바꿈 추가
formattedSql = formattedSql
.replaceAll("(?i)\\s+SELECT\\s+", "\nSELECT ")
.replaceAll("(?i)\\s+FROM\\s+", "\nFROM ")
.replaceAll("(?i)\\s+WHERE\\s+", "\nWHERE ")
.replaceAll("(?i)\\s+AND\\s+", "\n AND ")
.replaceAll("(?i)\\s+OR\\s+", "\n OR ")
.replaceAll("(?i)\\s+GROUP BY\\s+", "\nGROUP BY ")
.replaceAll("(?i)\\s+HAVING\\s+", "\nHAVING ")
.replaceAll("(?i)\\s+ORDER BY\\s+", "\nORDER BY ")
.replaceAll("(?i)\\s+LIMIT\\s+", "\nLIMIT ")
.replaceAll("(?i)\\s+OFFSET\\s+", "\nOFFSET ")
.replaceAll("(?i)\\s+UNION\\s+", "\nUNION\n")
.replaceAll("(?i)\\s+INSERT\\s+INTO\\s+", "\nINSERT INTO ")
.replaceAll("(?i)\\s+VALUES\\s+", "\nVALUES ")
.replaceAll("(?i)\\s+UPDATE\\s+", "\nUPDATE ")
.replaceAll("(?i)\\s+SET\\s+", "\nSET ")
.replaceAll("(?i)\\s+DELETE\\s+FROM\\s+", "\nDELETE FROM ")
.replaceAll("(?i)\\s+JOIN\\s+", "\nJOIN ")
.replaceAll("(?i)\\s+LEFT\\s+JOIN\\s+", "\nLEFT JOIN ")
.replaceAll("(?i)\\s+RIGHT\\s+JOIN\\s+", "\nRIGHT JOIN ")
.replaceAll("(?i)\\s+INNER\\s+JOIN\\s+", "\nINNER JOIN ")
.replaceAll("(?i)\\s+OUTER\\s+JOIN\\s+", "\nOUTER JOIN ");
return formattedSql;
return sql.replaceAll("\\s+", " ").trim();
}
}
/**
* SQL .
*
* @param statement SQL
* @param indent
* @return SQL
*/
private String formatStatement(Statement statement, int indent) {
if (statement == null) {
return "";
}
StringBuilder result = new StringBuilder();
String indentation = String.join("", Collections.nCopies(indent, " "));
try {
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);
} else {
// 지원하지 않는 문장 유형
result.append(statement.toString());
}
} catch (Exception e) {
// 포맷팅 중 오류 발생 시 원본 문장 반환
if (log.isDebugEnabled()) {
log.debug("SQL 문장 포맷팅 실패: {}", e.getMessage());
}
result.append(statement.toString());
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();

@ -47,6 +47,7 @@ logging:
level:
org.springframework: INFO
go.kr.project: DEBUG
go.kr.project.sql.query: OFF
egovframework: DEBUG
org.mybatis: INFO

@ -6,7 +6,7 @@
<div class="login">
<div class="login_head">
<ul>
<%--<li><img src="<c:url value="/img/h1_logo_main.png"/>"></li>--%>
<li><img src="<c:url value="/img/xit-logo/img.png"/>"></li>
<%--<li>로고 이미지</li>--%>
<li class="tit">불법건축물관리</li>
</ul>

Loading…
Cancel
Save