@ -13,6 +13,7 @@ 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 ;
@ -27,17 +28,32 @@ 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 Query Interceptor
* MyBatis 쿼 리 인 터 셉 터
*
* This interceptor logs detailed information about SQL queries executed by MyBatis ,
* including the original SQL , actual SQL with parameters , and parameter values .
* 이 인 터 셉 터 는 MyBatis 에 서 실 행 되 는 SQL 쿼 리 를 가 로 채 서 상 세 정 보 를 로 깅 합 니 다 .
* 원 본 SQL , 실 제 실 행 될 SQL ( 파 라 미 터 값 포 함 ) , 파 라 미 터 값 등 을 로 그 에 기 록 합 니 다 .
* local 과 dev 프 로 파 일 에 서 만 활 성 화 됩 니 다 .
*
* 주 요 기 능 :
* - SQL 쿼 리 로 깅
* - 파 라 미 터 값 추 출 및 로 깅
* - 쿼 리 실 행 시 간 측 정
* - include SQL 내 부 파 라 미 터 처 리
* - 다 양 한 데 이 터 타 입 지 원 ( String , Number , Boolean , Date , LocalDateTime 등 )
* /
@Component
@Profile ( { "local" , "dev" } ) // local과 dev 프로파일에서만 활성화
@ -48,105 +64,205 @@ import java.util.stream.Collectors;
@Slf4j
public class MyBatisQueryInterceptor implements Interceptor {
/ * *
* 인 터 셉 터 를 대 상 객 체 에 연 결 합 니 다 .
*
* @param target 대 상 객 체
* @return 프 록 시 객 체
* /
@Override
public Object intercept ( Invocation invocation ) throws Throwable {
MappedStatement ms = ( MappedStatement ) invocation . getArgs ( ) [ 0 ] ;
Object parameter = invocation . getArgs ( ) [ 1 ] ;
BoundSql boundSql = ms . getBoundSql ( parameter ) ;
String sql = boundSql . getSql ( ) ;
List < ParameterMapping > parameterMappings = boundSql . getParameterMappings ( ) ;
// 실제 실행될 쿼리 추출 (MyBatis 내부에서 처리된 결과)
String actualSql = getActualSql ( boundSql , parameter ) ;
// 파라미터 정보 추출
Map < String , Object > paramMap = extractDetailedParameters ( parameter , parameterMappings ) ;
logDetailedQueryInfo ( ms . getId ( ) , sql , actualSql , paramMap ) ;
// 원래 쿼리 실행
long startTime = System . currentTimeMillis ( ) ;
Object result = invocation . proceed ( ) ;
long endTime = System . currentTimeMillis ( ) ;
// 쿼리 실행 시간 로깅
log . debug ( "Query execution time: {} ms" , ( endTime - startTime ) ) ;
public Object plugin ( Object target ) {
return org . apache . ibatis . plugin . Plugin . wrap ( target , this ) ;
}
/ * *
* 인 터 셉 터 속 성 을 설 정 합 니 다 .
*
* @param properties 속 성
* /
@Override
public void setProperties ( java . util . Properties properties ) {
// 필요한 경우 속성 설정
}
return result ;
/ * *
* 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 ( ) ;
}
}
/ * *
* 실 제 실 행 될 SQL 쿼 리 를 생 성 합 니 다 .
* 원 본 SQL 의 '?' 플 레 이 스 홀 더 를 실 제 파 라 미 터 값 으 로 대 체 합 니 다 .
* include SQL 내 부 의 파 라 미 터 도 올 바 르 게 처 리 합 니 다 .
* /
private String getActualSql ( BoundSql boundSql , Object parameter ) {
String sql = boundSql . getSql ( ) ;
if ( parameter = = null ) {
return sql ;
}
List < ParameterMapping > parameterMappings = boundSql . getParameterMappings ( ) ;
if ( parameterMappings . isEmpty ( ) ) {
if ( parameterMappings = = null | | parameterMappings . isEmpty ( ) ) {
return sql ;
}
// SQL 복사본 생성
String actualSql = sql ;
try {
// 파라미터 값 추출
for ( ParameterMapping parameterMapping : parameterMappings ) {
String propertyName = parameterMapping . getProperty ( ) ;
Object value = null ;
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 ;
// SQL에서 실제 파라미터 바인딩 위치 확인
Pattern pattern = Pattern . compile ( "\\?" ) ;
Matcher matcher = pattern . matcher ( sql ) ;
StringBuffer result = new StringBuffer ( ) ;
int paramIndex = 0 ;
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 , propertyName ) ;
value = getParameterValue ( parameter , p arameterMapping. getProperty ( ) ) ;
}
String replacement = formatParameterValue ( value ) ;
matcher . appendReplacement ( result , Matcher . quoteReplacement ( replacement ) ) ;
} catch ( Exception e ) {
log . debug ( "파라미터 값 추출 실패: {}, 기본값 사용" , parameterMapping . getProperty ( ) ) ;
matcher . appendReplacement ( result , "?" ) ;
}
paramIndex + + ;
}
matcher . appendTail ( result ) ;
return result . toString ( ) ;
}
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 ) ;
}
/ * *
* 중 첩 된 파 라 미 터 값 을 추 출 합 니 다 .
* /
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 ;
}
} catch ( Exception e ) {
log . warn ( "Failed to get actual SQL with parameters" , e ) ;
return sql ;
}
// 중첩 필드 접근
for ( int i = 1 ; i < parts . length & & current ! = null ; i + + ) {
current = getParameterValue ( current , parts [ i ] ) ;
}
return current ;
}
return actualSql ;
/ * *
* 파 라 미 터 값 을 SQL 용 문 자 열 로 포 맷 팅 합 니 다 .
* /
private String formatParameterValue ( Object value ) {
if ( value = = null ) {
return "NULL" ;
}
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 ( "'" , "''" ) + "'" ;
}
}
private Object getParameterValue ( Object parameter , String propertyName ) {
@ -170,113 +286,308 @@ private Object getParameterValue(Object parameter, String propertyName) {
}
}
/ * *
* 파 라 미 터 객 체 에 서 상 세 정 보 를 추 출 하 여 맵 으 로 반 환 합 니 다 .
* include SQL 내 부 의 파 라 미 터 도 올 바 르 게 추 출 합 니 다 .
* /
private Map < String , Object > extractDetailedParameters ( Object parameter , List < ParameterMapping > parameterMappings ) {
Map < String , Object > paramMap = new HashMap < > ( ) ;
if ( parameter = = null ) {
Map < String , Object > paramMap = new Linked HashMap< > ( ) ;
if ( parameter = = null | | parameterMappings = = null | | parameterMappings . isEmpty ( ) ) {
return paramMap ;
}
if ( parameter instanceof Map ) {
paramMap . putAll ( ( Map < String , Object > ) parameter ) ;
} else {
// 객체의 필드 정보도 추출
paramMap . put ( "param" , parameter ) ;
extractObjectFields ( parameter , 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 ( ) ) ;
}
}
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 ( ) ;
while ( currentClass ! = null ) {
// 시스템 클래스는 처리하지 않음
if ( currentClass . getName ( ) . startsWith ( "java." ) | |
currentClass . getName ( ) . startsWith ( "javax." ) | |
currentClass . getName ( ) . startsWith ( "sun." ) ) {
return ;
}
// 현재 클래스부터 상위 클래스까지 순회하면서 필드 추출
while ( currentClass ! = null & & ! currentClass . getName ( ) . startsWith ( "java." ) ) {
Field [ ] fields = currentClass . getDeclaredFields ( ) ;
for ( Field field : fields ) {
try {
// String 타입의 객체는 건너뛰기
if ( obj instanceof String ) {
// static 또는 transient 필드는 건너뛰기
if ( java . lang . reflect . Modifier . isStatic ( field . getModifiers ( ) ) | |
java . lang . reflect . Modifier . isTransient ( field . getModifiers ( ) ) ) {
continue ;
}
// 시스템 클래스의 필드는 건너뛰기
if ( field . getDeclaringClass ( ) . getName ( ) . startsWith ( "java." ) ) {
continue ;
}
field . setAccessible ( true ) ;
Object value = field . get ( obj ) ;
paramMap . put ( field . getName ( ) , value ) ;
// 값이 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 ) {
// 중첩 필드 접근 실패는 무시하고 계속 진행
}
}
}
}
} catch ( IllegalAccessException | SecurityException e ) {
// 개별 필드 접근 실패는 무시하고 계속 진행
log . debug ( "필드 접근 실패: {} ({})" , field . getName ( ) , e . getMessage ( ) ) ;
if ( log . isDebugEnabled ( ) ) {
log . debug ( "필드 접근 실패: {} ({})" , field . getName ( ) , e . getMessage ( ) ) ;
}
}
}
currentClass = currentClass . getSuperclass ( ) ;
}
} catch ( Exception e ) {
log . warn ( "객체 필드 추출 실패" , e ) ;
log . warn ( "객체 필드 추출 중 오류 발생: {}" , e . getMessage ( ) ) ;
if ( log . isDebugEnabled ( ) ) {
log . debug ( "상세 오류 정보:" , 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 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" ) ;
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" ) ;
formatSqlInLog ( logMessage , actualSql ) ;
logMessage . append ( "└──────────────────────────────────────────────────────" ) ;
log . info ( logMessage . toString ( ) ) ;
}
/ * *
* SQL 쿼 리 를 로 그 메 시 지 에 포 맷 팅 하 여 추 가 합 니 다 .
*
* @param logMessage 로 그 메 시 지 빌 더
* @param sql 포 맷 팅 할 SQL 쿼 리
* /
private void formatSqlInLog ( StringBuilder logMessage , String sql ) {
// SQL 키워드 하이라이트 및 들여쓰기
String formattedSql = formatSql ( sql ) ;
String [ ] lines = formattedSql . split ( "\n" ) ;
try {
// 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" ) ;
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 ( ) ) ;
}
}
/ * *
* 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 파싱 실패. 기본 포맷팅으로 대체합니다." ) ;
// 파싱 실패 시 기본 포맷팅 사용
return sql . replaceAll ( "\\s+" , " " ) . trim ( ) ;
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 ;
}
}
/ * *
* 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 ( "" , java . util . 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 ) ;
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 ( ) ) ;
}
return result . toString ( ) ;