로그 변경 완료

datasource-proxy
dev
박성영 4 months ago
parent fe25fb496e
commit fa3a7f34d5

@ -1,6 +1,9 @@
package egovframework.config;
import com.zaxxer.hikari.HikariDataSource;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.util.TablesNamesFinder;
import net.ttddyy.dsproxy.listener.logging.DefaultQueryLogEntryCreator;
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener;
@ -117,39 +120,126 @@ public class DataSourceProxyConfig {
for (QueryInfo queryInfo : queryInfoList) {
String query = queryInfo.getQuery();
List<List<ParameterSetOperation>> parametersList = queryInfo.getParametersList();
// Mapper 경로 및 메서드명 추출
String mapperInfo = extractMapperInfo();
logger.info(" ========================== Mapper: {} ========================== ", mapperInfo);
// 디버깅: 원본 파라미터 정보 로그
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");
}
}
// 쿼리 실행 시간 및 기본 정보
long executionTime = execInfo.getElapsedTime();
logger.debug("실행 시간: {}ms", executionTime);
// 파라미터 값 추출
List<Object> parameterValues = extractParameterValues(parametersList);
// 디버깅: 추출된 파라미터 값들 로그
logger.info("[INFO] 추출된 파라미터 값들: {}", parameterValues);
// 파라미터 개수 정보
int questionMarkCount = countQuestionMarks(query);
logger.debug("파라미터 정보: ? 플레이스홀더 {}개, 바인딩 값 {}개", questionMarkCount, parameterValues.size());
// 디버깅: SQL에서 ? 개수 카운트
int questionMarkCount = 0;
for (int i = 0; i < query.length(); i++) {
if (query.charAt(i) == '?') {
questionMarkCount++;
// 파라미터 값들 전체 출력 (인덱스 포함)
if (!parameterValues.isEmpty()) {
logger.debug("파라미터 값 목록 (총 {}개):", parameterValues.size());
for (int i = 0; i < parameterValues.size(); i++) {
Object param = parameterValues.get(i);
String paramValue;
if (param == null) {
paramValue = "NULL";
} else if (param instanceof String) {
paramValue = "'" + param.toString() + "'";
} else {
paramValue = param.toString();
}
// 파라미터 인덱스와 함께 출력
logger.debug(" (parameter {}) = {}", i + 1, paramValue);
}
}
logger.info("[INFO] SQL에서 ? 플레이스홀더 개수: {}, 파라미터 개수: {}", questionMarkCount, parameterValues.size());
// 파라미터 바인딩된 SQL 생성
String boundQuery = bindParameters(query, parameterValues);
// 로그 출력
logger.info("\n========== 파라미터 바인딩된 SQL ==========\n{}\n==========================================", boundQuery);
// SQL 포맷팅 적용 (jsqlparser 사용)
String formattedQuery = formatSql(boundQuery);
logger.info("\n{}\n", formattedQuery);
}
}
/**
* SQL ?
* ?
*
* @param query SQL
* @return ?
*/
private int countQuestionMarks(String query) {
int count = 0;
boolean inBlockComment = false; // /* */ 블록 주석 내부인지 여부
boolean inLineComment = false; // -- 라인 주석 내부인지 여부
boolean inStringLiteral = false; // ' ' 문자열 리터럴 내부인지 여부
for (int i = 0; i < query.length(); i++) {
char currentChar = query.charAt(i);
char nextChar = (i + 1 < query.length()) ? query.charAt(i + 1) : '\0';
// 문자열 리터럴 처리 (홑따옴표로 둘러싸인 문자열)
if (currentChar == '\'' && !inBlockComment && !inLineComment) {
inStringLiteral = !inStringLiteral;
continue;
}
// 블록 주석 시작 /* 검사
if (!inStringLiteral && !inLineComment && currentChar == '/' && nextChar == '*') {
inBlockComment = true;
i++; // 다음 문자도 건너뛰기
continue;
}
// 블록 주석 끝 */ 검사
if (!inStringLiteral && inBlockComment && currentChar == '*' && nextChar == '/') {
inBlockComment = false;
i++; // 다음 문자도 건너뛰기
continue;
}
// 라인 주석 시작 -- 검사
if (!inStringLiteral && !inBlockComment && currentChar == '-' && nextChar == '-') {
inLineComment = true;
i++; // 다음 문자도 건너뛰기
continue;
}
// 라인 주석 끝 (줄바꿈) 검사
if (inLineComment && (currentChar == '\n' || currentChar == '\r')) {
inLineComment = false;
continue;
}
// ? 플레이스홀더 카운트 (주석이나 문자열 리터럴 내부가 아닌 경우에만)
if (currentChar == '?' && !inBlockComment && !inLineComment && !inStringLiteral) {
count++;
}
}
return count;
}
/**
* MyBatis Interceptor Mapper
* SqlLoggingInterceptor ThreadLocal
*
* @return Mapper (: go.kr.project.login.mapper.LoginMapper.selectMenusByRoleIds)
*/
private String extractMapperInfo() {
try {
// SqlLoggingInterceptor에서 제공하는 정확한 Mapper 정보 사용
String mapperInfo = SqlLoggingInterceptor.getCurrentMapperInfo();
return mapperInfo != null ? mapperInfo : "Unknown Mapper";
} catch (Exception e) {
logger.warn("Mapper 정보 추출 실패: {}", e.getMessage());
return "Unknown Mapper";
}
}
@ -180,6 +270,679 @@ public class DataSourceProxyConfig {
return new java.util.ArrayList<>(parameterMap.values());
}
/**
* jsqlparser SQL
* SQL (SELECT, UPDATE, INSERT, DELETE)
*
* @param sql SQL
* @return SQL
*/
private String formatSql(String sql) {
try {
// SQL 파싱
Statement statement = CCJSqlParserUtil.parse(sql);
// 포맷팅된 SQL 반환 (들여쓰기와 줄바꿈 적용)
String formattedSql = statement.toString();
// SQL 타입별 정교한 포맷팅 적용
String sqlUpper = formattedSql.toUpperCase().trim();
if (sqlUpper.startsWith("SELECT")) {
// SELECT 절 포맷팅
formattedSql = formatSelectColumns(formattedSql);
} else if (sqlUpper.startsWith("UPDATE")) {
// UPDATE SET 절 포맷팅
formattedSql = formatUpdateSetColumns(formattedSql);
} else if (sqlUpper.startsWith("INSERT")) {
// INSERT INTO VALUES 절 포맷팅
formattedSql = formatInsertValuesColumns(formattedSql);
}
// 기본적인 SQL 키워드 포맷팅 적용
formattedSql = applyBasicSqlFormatting(formattedSql);
// 시작 부분의 불필요한 줄바꿈 제거
formattedSql = formattedSql.replaceAll("^\\s*\\n+", "");
return formattedSql;
} catch (Exception e) {
// SQL 파싱 실패 시 원본 SQL 반환
logger.warn("SQL 파싱 실패, 원본 SQL 반환: {}", e.getMessage());
return sql;
}
}
/**
* SQL
*
* @param sql SQL
* @return SQL
*/
private String applyBasicSqlFormatting(String sql) {
return sql
.replaceAll("(?i)\\bSELECT\\b", "\nSELECT")
.replaceAll("(?i)\\bFROM\\b", "\nFROM")
.replaceAll("(?i)\\bINNER JOIN\\b", "\n INNER JOIN")
.replaceAll("(?i)\\bLEFT OUTER JOIN\\b", "\n LEFT OUTER JOIN")
.replaceAll("(?i)\\bLEFT JOIN\\b", "\n LEFT JOIN")
.replaceAll("(?i)\\bRIGHT JOIN\\b", "\n RIGHT JOIN")
.replaceAll("(?i)\\bWHERE\\b", "\nWHERE")
.replaceAll("(?i)\\bAND\\b(?=\\s+[A-Za-z])", "\n AND")
.replaceAll("(?i)\\bOR\\b(?=\\s+[A-Za-z])", "\n OR")
.replaceAll("(?i)\\bORDER BY\\b", "\nORDER BY")
.replaceAll("(?i)\\bGROUP BY\\b", "\nGROUP BY")
.replaceAll("(?i)\\bHAVING\\b", "\nHAVING")
.replaceAll("(?i)\\bLIMIT\\b", "\nLIMIT")
.replaceAll("(?i)\\bUNION\\b", "\nUNION")
.replaceAll("(?i)\\bUPDATE\\b", "\nUPDATE")
.replaceAll("(?i)\\bSET\\b", "\nSET")
.replaceAll("(?i)\\bINSERT INTO\\b", "\nINSERT INTO")
.replaceAll("(?i)\\bVALUES\\b", "\nVALUES")
.replaceAll("(?i)\\bDELETE FROM\\b", "\nDELETE FROM");
}
/**
* UPDATE SET =
* SQL , CASE WHEN ,
* : UPDATE table SET col1=val1, col2=val2 UPDATE table\nSET\n col1=val1\n , col2=val2
*
* @param sql SQL
* @return UPDATE SET SQL
*/
private String formatUpdateSetColumns(String sql) {
try {
StringBuilder result = new StringBuilder();
String[] lines = sql.split("\n");
for (String line : lines) {
String trimmedLine = line.trim();
// SET으로 시작하는 줄을 찾아서 컬럼 포맷팅 적용
if (trimmedLine.toUpperCase().startsWith("SET ")) {
String setPart = trimmedLine.substring(4); // "SET " 제거
// WHERE 절이 같은 줄에 있는지 확인
String wherePart = "";
int whereIndex = findWhereClause(setPart);
if (whereIndex > 0) {
wherePart = setPart.substring(whereIndex);
setPart = setPart.substring(0, whereIndex);
}
// 정교한 컬럼=값 쌍 분리 (함수, CASE WHEN, 괄호 고려)
List<String> setColumns = parseSetColumns(setPart);
// SET 절 시작
result.append("SET");
// 각 컬럼=값 쌍을 개별 줄로 추가
for (int i = 0; i < setColumns.size(); i++) {
String setColumn = setColumns.get(i).trim();
if (!setColumn.isEmpty()) {
if (i == 0) {
// 첫 번째 컬럼은 들여쓰기만
result.append("\n ").append(setColumn);
} else {
// 두 번째부터는 쉼표와 함께
result.append("\n , ").append(setColumn);
}
}
}
// WHERE 절이 있으면 추가
if (!wherePart.isEmpty()) {
result.append(wherePart);
}
} else {
// SET이 아닌 줄은 그대로 추가
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
}
return result.toString();
} catch (Exception e) {
// 포맷팅 실패 시 원본 SQL 반환
logger.warn("UPDATE SET 컬럼 포맷팅 실패, 원본 SQL 반환: {}", e.getMessage());
return sql;
}
}
/**
* INSERT INTO VALUES
* SQL , CASE WHEN ,
* : INSERT INTO table (col1, col2) VALUES (val1, val2) INSERT INTO table\n (\n col1\n , col2\n )\nVALUES\n (\n val1\n , val2\n )
*
* @param sql SQL
* @return INSERT VALUES SQL
*/
private String formatInsertValuesColumns(String sql) {
try {
StringBuilder result = new StringBuilder();
String[] lines = sql.split("\n");
for (String line : lines) {
String trimmedLine = line.trim();
// INSERT INTO로 시작하는 줄을 찾아서 컬럼 목록 포맷팅 적용
if (trimmedLine.toUpperCase().startsWith("INSERT INTO ")) {
// INSERT INTO table_name (columns) VALUES (values) 형태 파싱
String insertPart = trimmedLine.substring(12); // "INSERT INTO " 제거
// 테이블명과 나머지 부분 분리
int openParenIndex = insertPart.indexOf('(');
if (openParenIndex > 0) {
String tableName = insertPart.substring(0, openParenIndex).trim();
String remainingPart = insertPart.substring(openParenIndex);
// VALUES 절 위치 찾기
int valuesIndex = findValuesClause(remainingPart);
if (valuesIndex > 0) {
String columnsPart = remainingPart.substring(0, valuesIndex);
String valuesPart = remainingPart.substring(valuesIndex);
// INSERT INTO table_name 시작
result.append("INSERT INTO ").append(tableName);
// 컬럼 목록 포맷팅
String formattedColumns = formatInsertColumns(columnsPart);
result.append(formattedColumns);
// VALUES 절 포맷팅
String formattedValues = formatInsertValues(valuesPart);
result.append(formattedValues);
} else {
// VALUES 절이 없는 경우 (서브쿼리 INSERT 등)
result.append(trimmedLine);
}
} else {
// 괄호가 없는 경우 그대로 추가
result.append(trimmedLine);
}
} else {
// INSERT INTO가 아닌 줄은 그대로 추가
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
}
return result.toString();
} catch (Exception e) {
// 포맷팅 실패 시 원본 SQL 반환
logger.warn("INSERT VALUES 컬럼 포맷팅 실패, 원본 SQL 반환: {}", e.getMessage());
return sql;
}
}
/**
* SELECT
* SQL , CASE WHEN ,
* : SELECT AA, BB, CC SELECT\n AA\n , BB\n , CC
*
* @param sql SQL
* @return SELECT SQL
*/
private String formatSelectColumns(String sql) {
try {
// SELECT 절을 찾아서 포맷팅
StringBuilder result = new StringBuilder();
String[] lines = sql.split("\n");
for (String line : lines) {
String trimmedLine = line.trim();
// SELECT로 시작하는 줄을 찾아서 컬럼 포맷팅 적용
if (trimmedLine.toUpperCase().startsWith("SELECT ")) {
String selectPart = trimmedLine.substring(7); // "SELECT " 제거
// FROM 절이 같은 줄에 있는지 확인
String fromPart = "";
int fromIndex = findFromClause(selectPart);
if (fromIndex > 0) {
fromPart = selectPart.substring(fromIndex);
selectPart = selectPart.substring(0, fromIndex);
}
// 정교한 컬럼 분리 (함수, CASE WHEN, 괄호 고려)
List<String> columns = parseSelectColumns(selectPart);
// SELECT 절 시작
result.append("SELECT");
// 각 컬럼을 개별 줄로 추가
for (int i = 0; i < columns.size(); i++) {
String column = columns.get(i).trim();
if (!column.isEmpty()) {
if (i == 0) {
// 첫 번째 컬럼은 들여쓰기만
result.append("\n ").append(column);
} else {
// 두 번째부터는 쉼표와 함께
result.append("\n , ").append(column);
}
}
}
// FROM 절이 있으면 추가
if (!fromPart.isEmpty()) {
result.append(fromPart);
}
} else {
// SELECT가 아닌 줄은 그대로 추가
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
}
return result.toString();
} catch (Exception e) {
// 포맷팅 실패 시 원본 SQL 반환
logger.warn("SELECT 컬럼 포맷팅 실패, 원본 SQL 반환: {}", e.getMessage());
return sql;
}
}
/**
* FROM
* FROM SELECT FROM
*
* @param selectPart SELECT
* @return FROM , -1
*/
private int findFromClause(String selectPart) {
int parenthesesDepth = 0;
boolean inStringLiteral = false;
String upperCaseSelect = selectPart.toUpperCase();
for (int i = 0; i < selectPart.length() - 4; i++) {
char currentChar = selectPart.charAt(i);
// 문자열 리터럴 처리
if (currentChar == '\'' && (i == 0 || selectPart.charAt(i - 1) != '\\')) {
inStringLiteral = !inStringLiteral;
continue;
}
if (!inStringLiteral) {
// 괄호 깊이 추적
if (currentChar == '(') {
parenthesesDepth++;
} else if (currentChar == ')') {
parenthesesDepth--;
}
// 괄호 밖에서만 FROM 절 검색
if (parenthesesDepth == 0 && upperCaseSelect.substring(i).startsWith(" FROM ")) {
return i;
}
}
}
return -1;
}
/**
* SELECT
* , CASE WHEN , ,
*
* @param selectPart SELECT (SELECT )
* @return
*/
private List<String> parseSelectColumns(String selectPart) {
List<String> columns = new java.util.ArrayList<>();
StringBuilder currentColumn = new StringBuilder();
int parenthesesDepth = 0;
boolean inStringLiteral = false;
boolean inCaseStatement = false;
int caseDepth = 0;
for (int i = 0; i < selectPart.length(); i++) {
char currentChar = selectPart.charAt(i);
String remaining = selectPart.substring(i);
String upperRemaining = remaining.toUpperCase();
// 문자열 리터럴 처리 (이스케이프 문자 고려)
if (currentChar == '\'' && (i == 0 || selectPart.charAt(i - 1) != '\\')) {
inStringLiteral = !inStringLiteral;
currentColumn.append(currentChar);
continue;
}
if (!inStringLiteral) {
// CASE WHEN 구문 시작 검사
if (upperRemaining.startsWith("CASE ") && parenthesesDepth == 0) {
inCaseStatement = true;
caseDepth++;
currentColumn.append("CASE ");
i += 4; // "CASE" 길이만큼 건너뛰기
continue;
}
// 중첩된 CASE 구문 처리
if (inCaseStatement && upperRemaining.startsWith("CASE ")) {
caseDepth++;
}
// END 키워드 검사 (CASE 구문 종료)
if (inCaseStatement && upperRemaining.startsWith("END") &&
(i + 3 >= selectPart.length() || !Character.isLetterOrDigit(selectPart.charAt(i + 3)))) {
caseDepth--;
if (caseDepth == 0) {
inCaseStatement = false;
}
currentColumn.append("END");
i += 2; // "END" 길이만큼 건너뛰기
continue;
}
// 괄호 깊이 추적
if (currentChar == '(') {
parenthesesDepth++;
currentColumn.append(currentChar);
continue;
} else if (currentChar == ')') {
parenthesesDepth--;
currentColumn.append(currentChar);
continue;
}
// 컬럼 구분자 쉼표 처리 (괄호 밖이고 CASE 구문 밖에서만)
if (currentChar == ',' && parenthesesDepth == 0 && !inCaseStatement) {
// 현재 컬럼을 목록에 추가
String column = currentColumn.toString().trim();
if (!column.isEmpty()) {
columns.add(column);
}
// 다음 컬럼을 위해 초기화
currentColumn = new StringBuilder();
continue;
}
}
// 일반 문자 추가
currentColumn.append(currentChar);
}
// 마지막 컬럼 추가
String lastColumn = currentColumn.toString().trim();
if (!lastColumn.isEmpty()) {
columns.add(lastColumn);
}
return columns;
}
/**
* WHERE
* WHERE UPDATE WHERE
*
* @param setPart SET
* @return WHERE , -1
*/
private int findWhereClause(String setPart) {
int parenthesesDepth = 0;
boolean inStringLiteral = false;
String upperCaseSet = setPart.toUpperCase();
for (int i = 0; i < setPart.length() - 5; i++) {
char currentChar = setPart.charAt(i);
// 문자열 리터럴 처리
if (currentChar == '\'' && (i == 0 || setPart.charAt(i - 1) != '\\')) {
inStringLiteral = !inStringLiteral;
continue;
}
if (!inStringLiteral) {
// 괄호 깊이 추적
if (currentChar == '(') {
parenthesesDepth++;
} else if (currentChar == ')') {
parenthesesDepth--;
}
// 괄호 밖에서만 WHERE 절 검색
if (parenthesesDepth == 0 && upperCaseSet.substring(i).startsWith(" WHERE ")) {
return i;
}
}
}
return -1;
}
/**
* SET =
* , CASE WHEN , ,
*
* @param setPart SET (SET )
* @return =
*/
private List<String> parseSetColumns(String setPart) {
List<String> setColumns = new java.util.ArrayList<>();
StringBuilder currentSetColumn = new StringBuilder();
int parenthesesDepth = 0;
boolean inStringLiteral = false;
boolean inCaseStatement = false;
int caseDepth = 0;
for (int i = 0; i < setPart.length(); i++) {
char currentChar = setPart.charAt(i);
String remaining = setPart.substring(i);
String upperRemaining = remaining.toUpperCase();
// 문자열 리터럴 처리 (이스케이프 문자 고려)
if (currentChar == '\'' && (i == 0 || setPart.charAt(i - 1) != '\\')) {
inStringLiteral = !inStringLiteral;
currentSetColumn.append(currentChar);
continue;
}
if (!inStringLiteral) {
// CASE WHEN 구문 시작 검사
if (upperRemaining.startsWith("CASE ") && parenthesesDepth == 0) {
inCaseStatement = true;
caseDepth++;
currentSetColumn.append("CASE ");
i += 4; // "CASE" 길이만큼 건너뛰기
continue;
}
// 중첩된 CASE 구문 처리
if (inCaseStatement && upperRemaining.startsWith("CASE ")) {
caseDepth++;
}
// END 키워드 검사 (CASE 구문 종료)
if (inCaseStatement && upperRemaining.startsWith("END") &&
(i + 3 >= setPart.length() || !Character.isLetterOrDigit(setPart.charAt(i + 3)))) {
caseDepth--;
if (caseDepth == 0) {
inCaseStatement = false;
}
currentSetColumn.append("END");
i += 2; // "END" 길이만큼 건너뛰기
continue;
}
// 괄호 깊이 추적
if (currentChar == '(') {
parenthesesDepth++;
currentSetColumn.append(currentChar);
continue;
} else if (currentChar == ')') {
parenthesesDepth--;
currentSetColumn.append(currentChar);
continue;
}
// 컬럼=값 구분자 쉼표 처리 (괄호 밖이고 CASE 구문 밖에서만)
if (currentChar == ',' && parenthesesDepth == 0 && !inCaseStatement) {
// 현재 컬럼=값 쌍을 목록에 추가
String setColumn = currentSetColumn.toString().trim();
if (!setColumn.isEmpty()) {
setColumns.add(setColumn);
}
// 다음 컬럼=값 쌍을 위해 초기화
currentSetColumn = new StringBuilder();
continue;
}
}
// 일반 문자 추가
currentSetColumn.append(currentChar);
}
// 마지막 컬럼=값 쌍 추가
String lastSetColumn = currentSetColumn.toString().trim();
if (!lastSetColumn.isEmpty()) {
setColumns.add(lastSetColumn);
}
return setColumns;
}
/**
* VALUES
* VALUES INSERT VALUES
*
* @param insertPart INSERT
* @return VALUES , -1
*/
private int findValuesClause(String insertPart) {
int parenthesesDepth = 0;
boolean inStringLiteral = false;
String upperCaseInsert = insertPart.toUpperCase();
for (int i = 0; i < insertPart.length() - 6; i++) {
char currentChar = insertPart.charAt(i);
// 문자열 리터럴 처리
if (currentChar == '\'' && (i == 0 || insertPart.charAt(i - 1) != '\\')) {
inStringLiteral = !inStringLiteral;
continue;
}
if (!inStringLiteral) {
// 괄호 깊이 추적
if (currentChar == '(') {
parenthesesDepth++;
} else if (currentChar == ')') {
parenthesesDepth--;
}
// 괄호 밖에서만 VALUES 절 검색
if (parenthesesDepth == 0 && upperCaseInsert.substring(i).startsWith(" VALUES ")) {
return i;
}
}
}
return -1;
}
/**
* INSERT
* : (col1, col2, col3) \n (\n col1\n , col2\n , col3\n )
*
* @param columnsPart
* @return
*/
private String formatInsertColumns(String columnsPart) {
try {
// 괄호 제거
String columnsOnly = columnsPart.trim();
if (columnsOnly.startsWith("(") && columnsOnly.endsWith(")")) {
columnsOnly = columnsOnly.substring(1, columnsOnly.length() - 1);
}
// 컬럼들을 파싱 (함수, CASE WHEN, 괄호 고려)
List<String> columns = parseSelectColumns(columnsOnly);
StringBuilder result = new StringBuilder();
result.append("\n (");
// 각 컬럼을 개별 줄로 추가
for (int i = 0; i < columns.size(); i++) {
String column = columns.get(i).trim();
if (!column.isEmpty()) {
if (i == 0) {
// 첫 번째 컬럼은 들여쓰기만
result.append("\n ").append(column);
} else {
// 두 번째부터는 쉼표와 함께
result.append("\n , ").append(column);
}
}
}
result.append("\n )");
return result.toString();
} catch (Exception e) {
// 포맷팅 실패 시 원본 반환
logger.warn("INSERT 컬럼 포맷팅 실패, 원본 반환: {}", e.getMessage());
return columnsPart;
}
}
/**
* INSERT VALUES
* : VALUES (val1, val2, val3) \nVALUES\n (\n val1\n , val2\n , val3\n )
*
* @param valuesPart VALUES
* @return VALUES
*/
private String formatInsertValues(String valuesPart) {
try {
String valuesOnly = valuesPart.trim();
if (valuesOnly.toUpperCase().startsWith("VALUES ")) {
valuesOnly = valuesOnly.substring(7); // "VALUES " 제거
}
// 괄호 제거
if (valuesOnly.startsWith("(") && valuesOnly.endsWith(")")) {
valuesOnly = valuesOnly.substring(1, valuesOnly.length() - 1);
}
// 값들을 파싱 (함수, CASE WHEN, 괄호 고려)
List<String> values = parseSelectColumns(valuesOnly);
StringBuilder result = new StringBuilder();
result.append("\nVALUES\n (");
// 각 값을 개별 줄로 추가
for (int i = 0; i < values.size(); i++) {
String value = values.get(i).trim();
if (!value.isEmpty()) {
if (i == 0) {
// 첫 번째 값은 들여쓰기만
result.append("\n ").append(value);
} else {
// 두 번째부터는 쉼표와 함께
result.append("\n , ").append(value);
}
}
}
result.append("\n )");
return result.toString();
} catch (Exception e) {
// 포맷팅 실패 시 원본 반환
logger.warn("INSERT VALUES 포맷팅 실패, 원본 반환: {}", e.getMessage());
return valuesPart;
}
}
/**
* SQL ?
* ?

@ -0,0 +1,117 @@
package egovframework.config;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Properties;
/**
* MyBatis Interceptor SQL Mapper
*
* MappedStatement namespace(Mapper ) id()
* ThreadLocal DataSourceProxy .
*
* @author XIT Framework
*/
@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})
})
public class SqlLoggingInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(SqlLoggingInterceptor.class);
/**
* ThreadLocal Mapper
* DataSourceProxy Mapper
*/
private static final ThreadLocal<String> CURRENT_MAPPER_INFO = new ThreadLocal<>();
/**
* Mapper
*
* @return Mapper . (: go.kr.project.login.mapper.LoginMapper.selectMenusByRoleIds)
*/
public static String getCurrentMapperInfo() {
String mapperInfo = CURRENT_MAPPER_INFO.get();
return mapperInfo != null ? mapperInfo : "Unknown Mapper";
}
/**
* Mapper
*
* @param mapperInfo Mapper
*/
public static void setCurrentMapperInfo(String mapperInfo) {
CURRENT_MAPPER_INFO.set(mapperInfo);
}
/**
* Mapper
*/
public static void clearCurrentMapperInfo() {
CURRENT_MAPPER_INFO.remove();
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
// MappedStatement에서 Mapper 정보 추출
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
String statementId = mappedStatement.getId();
// statementId는 "namespace.methodId" 형태 (예: go.kr.project.login.mapper.LoginMapper.selectMenusByRoleIds)
String mapperInfo = statementId;
// ThreadLocal에 Mapper 정보 저장
setCurrentMapperInfo(mapperInfo);
logger.debug("MyBatis Interceptor - Mapper 정보 설정: {}", mapperInfo);
// 실제 쿼리 실행
return invocation.proceed();
} catch (Exception e) {
logger.warn("MyBatis Interceptor에서 Mapper 정보 추출 실패: {}", e.getMessage());
throw e;
} finally {
// 쿼리 실행 완료 후 ThreadLocal 정리 (메모리 누수 방지)
// 단, DataSourceProxy에서 사용할 수 있도록 바로 정리하지 않고 약간의 지연 후 정리
scheduleCleanup();
}
}
/**
* ThreadLocal
* DataSourceProxy Mapper
*/
private void scheduleCleanup() {
// 별도 스레드에서 약간의 지연 후 ThreadLocal 정리
new Thread(() -> {
try {
Thread.sleep(100); // 100ms 대기 (DataSourceProxy에서 사용할 시간 제공)
clearCurrentMapperInfo();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
clearCurrentMapperInfo();
}
}).start();
}
@Override
public Object plugin(Object target) {
// 모든 Executor에 대해 인터셉터 적용
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 설정 프로퍼티가 있다면 여기서 처리
// 현재는 특별한 설정이 필요없음
}
}

@ -14,14 +14,51 @@ spring:
username: root
password: xit5811807
hikari:
maximum-pool-size: 10
minimum-idle: 5
# ==========================================
# 커넥션 풀 크기 설정 (4코어 32GB 서버 기준)
# ==========================================
# 동시에 사용할 수 있는 최대 커넥션 수
# 권장값: (코어수 × 2) + (동시사용자 × 0.1) = (4 × 2) + (300 × 0.1) = 38 → 40
# 이 값을 초과하면 새로운 커넥션 요청이 대기 상태가 됨
maximum-pool-size: 40
# 풀에서 유지할 최소 유휴 커넥션 수
# 권장값: maximum-pool-size의 25% (40 × 0.25 = 10)
# 갑작스러운 부하 증가 시 빠른 응답을 위해 미리 준비된 커넥션 유지
minimum-idle: 10
# ==========================================
# 커넥션 타임아웃 설정
# ==========================================
# 커넥션을 얻기 위한 최대 대기 시간 (밀리초)
# 권장값: 30초 - 네트워크 지연이나 데이터베이스 부하 시 적절한 대기 시간
connection-timeout: 30000
idle-timeout: 600000
# 커넥션 유효성 검사 타임아웃 (밀리초)
# 권장값: 60초 - 유효성 검사가 너무 오래 걸리지 않도록 제한
validation-timeout: 60000
# ==========================================
# 커넥션 생명주기 관리
# ==========================================
# 커넥션의 최대 생명 시간 (밀리초)
# 권장값: 30분 - 데이터베이스 연결이 너무 오래 유지되지 않도록 제한
max-lifetime: 1800000
validation-timeout: 60000 #60초
# 유휴 커넥션을 제거하기 위한 최소 대기 시간 (밀리초)
# 권장값: 10분 - 메모리 절약과 커넥션 재사용의 균형점
idle-timeout: 600000
# ==========================================
# 커넥션 누수 방지 (수백명 동시 사용 환경, 배치가 있으므로 제외 함.)
# ==========================================
# 커넥션 누수 감지 임계값 (밀리초)
# 권장값: 300초 (5분) - 일반적인 쿼리 실행 시간보다 충분히 큰 값
# 너무 짧으면 정상적인 긴 쿼리도 누수로 감지될 수 있음
# 너무 길면 커넥션 누수 시 메모리 부족 가능성
# leak-detection-threshold: 300000
#hikari.connection-test-query 는 JDBC 4.0 이상을 사용하면 설정하지 않는 것을 권장. (공식문서)
#connection-test-query: select 1
auto-commit: false
# Server configuration
server:
@ -47,7 +84,8 @@ logging:
level:
org.springframework: INFO
go.kr.project: DEBUG
go.kr.project.sql: DEBUG # datasource-proxy 파라미터 바인딩된 쿼리 로그
go.kr.project.sql: INFO # datasource-proxy
go.kr.project.sql.binding: INFO # datasource-proxy 파라미터 바인딩된 쿼리 로그
egovframework: DEBUG
org.mybatis: INFO

@ -14,14 +14,51 @@ spring:
username: root
password: xit5811807
hikari:
maximum-pool-size: 10
minimum-idle: 5
# ==========================================
# 커넥션 풀 크기 설정 (4코어 32GB 서버 기준)
# ==========================================
# 동시에 사용할 수 있는 최대 커넥션 수
# 권장값: (코어수 × 2) + (동시사용자 × 0.1) = (4 × 2) + (300 × 0.1) = 38 → 40
# 이 값을 초과하면 새로운 커넥션 요청이 대기 상태가 됨
maximum-pool-size: 40
# 풀에서 유지할 최소 유휴 커넥션 수
# 권장값: maximum-pool-size의 25% (40 × 0.25 = 10)
# 갑작스러운 부하 증가 시 빠른 응답을 위해 미리 준비된 커넥션 유지
minimum-idle: 10
# ==========================================
# 커넥션 타임아웃 설정
# ==========================================
# 커넥션을 얻기 위한 최대 대기 시간 (밀리초)
# 권장값: 30초 - 네트워크 지연이나 데이터베이스 부하 시 적절한 대기 시간
connection-timeout: 30000
idle-timeout: 600000
# 커넥션 유효성 검사 타임아웃 (밀리초)
# 권장값: 60초 - 유효성 검사가 너무 오래 걸리지 않도록 제한
validation-timeout: 60000
# ==========================================
# 커넥션 생명주기 관리
# ==========================================
# 커넥션의 최대 생명 시간 (밀리초)
# 권장값: 30분 - 데이터베이스 연결이 너무 오래 유지되지 않도록 제한
max-lifetime: 1800000
validation-timeout: 60000 #60초
# 유휴 커넥션을 제거하기 위한 최소 대기 시간 (밀리초)
# 권장값: 10분 - 메모리 절약과 커넥션 재사용의 균형점
idle-timeout: 600000
# ==========================================
# 커넥션 누수 방지 (수백명 동시 사용 환경, 배치가 있으므로 제외 함.)
# ==========================================
# 커넥션 누수 감지 임계값 (밀리초)
# 권장값: 300초 (5분) - 일반적인 쿼리 실행 시간보다 충분히 큰 값
# 너무 짧으면 정상적인 긴 쿼리도 누수로 감지될 수 있음
# 너무 길면 커넥션 누수 시 메모리 부족 가능성
# leak-detection-threshold: 300000
#hikari.connection-test-query 는 JDBC 4.0 이상을 사용하면 설정하지 않는 것을 권장. (공식문서)
#connection-test-query: select 1
auto-commit: false
# Server configuration
server:

@ -10,18 +10,55 @@ spring:
enabled: true
datasource:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mariadb://localhost:53306/ibmsdb?allowMultiQueries=true
url: jdbc:mariadb://211.119.124.117:53306/ibmsdb?characterEncoding=UTF-8&allowMultiQueries=true
username: root
password: xit5811807
hikari:
maximum-pool-size: 10
minimum-idle: 5
# ==========================================
# 커넥션 풀 크기 설정 (4코어 32GB 서버 기준)
# ==========================================
# 동시에 사용할 수 있는 최대 커넥션 수
# 권장값: (코어수 × 2) + (동시사용자 × 0.1) = (4 × 2) + (300 × 0.1) = 38 → 40
# 이 값을 초과하면 새로운 커넥션 요청이 대기 상태가 됨
maximum-pool-size: 40
# 풀에서 유지할 최소 유휴 커넥션 수
# 권장값: maximum-pool-size의 25% (40 × 0.25 = 10)
# 갑작스러운 부하 증가 시 빠른 응답을 위해 미리 준비된 커넥션 유지
minimum-idle: 10
# ==========================================
# 커넥션 타임아웃 설정
# ==========================================
# 커넥션을 얻기 위한 최대 대기 시간 (밀리초)
# 권장값: 30초 - 네트워크 지연이나 데이터베이스 부하 시 적절한 대기 시간
connection-timeout: 30000
idle-timeout: 600000
# 커넥션 유효성 검사 타임아웃 (밀리초)
# 권장값: 60초 - 유효성 검사가 너무 오래 걸리지 않도록 제한
validation-timeout: 60000
# ==========================================
# 커넥션 생명주기 관리
# ==========================================
# 커넥션의 최대 생명 시간 (밀리초)
# 권장값: 30분 - 데이터베이스 연결이 너무 오래 유지되지 않도록 제한
max-lifetime: 1800000
validation-timeout: 60000 #60초
# 유휴 커넥션을 제거하기 위한 최소 대기 시간 (밀리초)
# 권장값: 10분 - 메모리 절약과 커넥션 재사용의 균형점
idle-timeout: 600000
# ==========================================
# 커넥션 누수 방지 (수백명 동시 사용 환경, 배치가 있으므로 제외 함.)
# ==========================================
# 커넥션 누수 감지 임계값 (밀리초)
# 권장값: 300초 (5분) - 일반적인 쿼리 실행 시간보다 충분히 큰 값
# 너무 짧으면 정상적인 긴 쿼리도 누수로 감지될 수 있음
# 너무 길면 커넥션 누수 시 메모리 부족 가능성
# leak-detection-threshold: 300000
#hikari.connection-test-query 는 JDBC 4.0 이상을 사용하면 설정하지 않는 것을 권장. (공식문서)
#connection-test-query: select 1
auto-commit: false
# Server configuration
server:
@ -47,9 +84,17 @@ logging:
level:
org.springframework: INFO
go.kr.project: DEBUG
go.kr.project.sql: DEBUG # datasource-proxy 파라미터 바인딩된 쿼리 로그
egovframework: DEBUG
go.kr.project.sql: WARN # datasource-proxy
go.kr.project.sql.binding: DEBUG # datasource-proxy 파라미터 바인딩된 쿼리 로그
egovframework: INFO
org.mybatis: INFO
# 실 운영, 안정화 이후 적용
# org.springframework: WARN
# go.kr.project: INFO
# go.kr.project.sql: WARN # datasource-proxy
# go.kr.project.sql.binding: INFO # datasource-proxy 파라미터 바인딩된 쿼리 로그
# egovframework: WARN
# org.mybatis: INFO
# File upload configuration
file:

@ -17,7 +17,7 @@ Globals:
# Common application properties
spring:
profiles:
active: local
active: prd
application:
name: IBMS-NEW
mvc:

@ -17,4 +17,11 @@
<typeAliases>
<!-- Type aliases will be defined in the type-aliases-package in application.yml -->
</typeAliases>
<!-- MyBatis Interceptors for SQL logging and Mapper information extraction -->
<plugins>
<plugin interceptor="egovframework.config.SqlLoggingInterceptor">
<!-- SQL 로깅 및 정확한 Mapper 정보 추출을 위한 인터셉터 -->
</plugin>
</plugins>
</configuration>
Loading…
Cancel
Save