|
|
|
|
@ -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 쿼리의 ? 플레이스홀더를 실제 파라미터 값으로 치환
|
|
|
|
|
* 주석 안에 있는 ? 는 치환하지 않음
|
|
|
|
|
|