package egovframework.config; import lombok.extern.slf4j.Slf4j; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.delete.Delete; import net.sf.jsqlparser.statement.insert.Insert; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectItem; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.update.UpdateSet; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import java.lang.reflect.Field; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * MyBatis Query Interceptor * * This interceptor logs detailed information about SQL queries executed by MyBatis, * including the original SQL, actual SQL with parameters, and parameter values. */ @Component @Profile({"local", "dev"}) // local과 dev 프로파일에서만 활성화 @Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) @Slf4j public class MyBatisQueryInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; Object parameter = invocation.getArgs()[1]; BoundSql boundSql = ms.getBoundSql(parameter); String sql = boundSql.getSql(); List parameterMappings = boundSql.getParameterMappings(); // 실제 실행될 쿼리 추출 (MyBatis 내부에서 처리된 결과) String actualSql = getActualSql(boundSql, parameter); // 파라미터 정보 추출 Map paramMap = extractDetailedParameters(parameter, parameterMappings); logDetailedQueryInfo(ms.getId(), sql, actualSql, paramMap); // 원래 쿼리 실행 long startTime = System.currentTimeMillis(); Object result = invocation.proceed(); long endTime = System.currentTimeMillis(); // 쿼리 실행 시간 로깅 log.debug("Query execution time: {} ms", (endTime - startTime)); return result; } private String getActualSql(BoundSql boundSql, Object parameter) { String sql = boundSql.getSql(); if (parameter == null) { return sql; } List parameterMappings = boundSql.getParameterMappings(); if (parameterMappings.isEmpty()) { return sql; } // SQL 복사본 생성 String actualSql = sql; try { // 파라미터 값 추출 for (ParameterMapping parameterMapping : parameterMappings) { String propertyName = parameterMapping.getProperty(); Object value = 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; } else { value = getParameterValue(parameter, propertyName); } String valueStr = value != null ? value.toString() : "null"; // SQL 인젝션 방지를 위한 문자열 이스케이프 처리 valueStr = valueStr.replace("'", "''"); // 다양한 데이터 타입에 대한 처리 if (value instanceof String) { // 문자열 타입은 따옴표로 감싸기 actualSql = actualSql.replaceFirst("\\?", "'" + valueStr + "'"); } else if (value instanceof Number) { // 숫자 타입 (Integer, Long, Double 등)은 그대로 사용 actualSql = actualSql.replaceFirst("\\?", valueStr); } else if (value instanceof Boolean) { // Boolean 타입은 그대로 사용 actualSql = actualSql.replaceFirst("\\?", valueStr); } else if (value instanceof java.util.Date) { // 표준 타임스탬프 포맷 사용 java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); actualSql = actualSql.replaceFirst("\\?", "'" + sdf.format(value) + "'"); } else if (value instanceof java.time.LocalDateTime) { // Java 8 LocalDateTime 처리 actualSql = actualSql.replaceFirst("\\?", "'" + value.toString().replace('T', ' ') + "'"); } else { // 기타 타입은 null이 아닌 경우 따옴표로 감싸기 actualSql = actualSql.replaceFirst("\\?", value != null ? "'" + valueStr + "'" : valueStr); } } } catch (Exception e) { log.warn("Failed to get actual SQL with parameters", e); return sql; } return actualSql; } private Object getParameterValue(Object parameter, String propertyName) { try { // 현재 클래스부터 상위 클래스까지 순회하면서 필드 찾기 Class currentClass = parameter.getClass(); while (currentClass != null) { try { Field field = currentClass.getDeclaredField(propertyName); field.setAccessible(true); return field.get(parameter); } catch (NoSuchFieldException e) { // 현재 클래스에서 필드를 찾지 못하면 상위 클래스로 이동 currentClass = currentClass.getSuperclass(); } } throw new NoSuchFieldException(propertyName); } catch (Exception e) { log.warn("필드 값 추출 실패: {} ({})", propertyName, e.getMessage()); return null; } } private Map extractDetailedParameters(Object parameter, List parameterMappings) { Map paramMap = new HashMap<>(); if (parameter == null) { return paramMap; } if (parameter instanceof Map) { paramMap.putAll((Map) parameter); } else { // 객체의 필드 정보도 추출 paramMap.put("param", parameter); extractObjectFields(parameter, paramMap); } return paramMap; } private void extractObjectFields(Object obj, Map paramMap) { try { Class currentClass = obj.getClass(); while (currentClass != null) { Field[] fields = currentClass.getDeclaredFields(); for (Field field : fields) { try { // String 타입의 객체는 건너뛰기 if (obj instanceof String) { continue; } // 시스템 클래스의 필드는 건너뛰기 if (field.getDeclaringClass().getName().startsWith("java.")) { continue; } field.setAccessible(true); Object value = field.get(obj); paramMap.put(field.getName(), value); } catch (IllegalAccessException | SecurityException e) { // 개별 필드 접근 실패는 무시하고 계속 진행 log.debug("필드 접근 실패: {} ({})", field.getName(), e.getMessage()); } } currentClass = currentClass.getSuperclass(); } } catch (Exception e) { log.warn("객체 필드 추출 실패", e); } } private void logDetailedQueryInfo(String mapperMethod, String originalSql, String actualSql, Map paramMap) { StringBuilder logMessage = new StringBuilder(); logMessage.append("\n"); logMessage.append("┌─────────────── MyBatis Query Details ───────────────\n"); logMessage.append("│ Mapper Method: ").append(mapperMethod).append("\n"); logMessage.append("│ Parameters: ").append(paramMap).append("\n"); // 원본 SQL 포맷팅, prd, 운영 //logMessage.append("│ Original SQL:\n"); //formatSqlInLog(logMessage, originalSql); // 실제 실행 SQL 포맷팅, local, dev 로컬 개발 logMessage.append("│ Actual SQL:\n"); formatSqlInLog(logMessage, actualSql); logMessage.append("└──────────────────────────────────────────────────────"); log.info(logMessage.toString()); } private void formatSqlInLog(StringBuilder logMessage, String sql) { // SQL 키워드 하이라이트 및 들여쓰기 String formattedSql = formatSql(sql); String[] lines = formattedSql.split("\n"); for (String line : lines) { //logMessage.append("│ ").append(line).append("\n"); logMessage.append(line).append("\n"); } } private String formatSql(String sql) { try { // SQL 파서를 사용한 포맷팅 Statement statement = CCJSqlParserUtil.parse(sql); return formatStatement(statement, 0); } catch (JSQLParserException e) { log.debug("SQL 파싱 실패. 기본 포맷팅으로 대체합니다.", e); log.info("SQL 파싱 실패. 기본 포맷팅으로 대체합니다."); // 파싱 실패 시 기본 포맷팅 사용 return sql.replaceAll("\\s+", " ").trim(); } } private String formatStatement(Statement statement, int indent) { StringBuilder result = new StringBuilder(); String indentation = String.join("", 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); } return result.toString(); } private void formatSelect(Select select, StringBuilder result, int indent) { String indentation = String.join("", Collections.nCopies(indent, " ")); PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); // SELECT 절 result.append(indentation).append("SELECT\n"); formatSelectItems(plainSelect.getSelectItems(), result, indent + 2); // FROM 절 if (plainSelect.getFromItem() != null) { result.append(indentation).append("FROM\n"); result.append(indentation).append(" ").append(plainSelect.getFromItem()).append("\n"); } // JOIN 절 if (plainSelect.getJoins() != null) { for (Join join : plainSelect.getJoins()) { result.append(indentation).append(join.isLeft() ? "LEFT JOIN " : "JOIN ") .append(join.getRightItem()).append("\n"); if (join.getOnExpression() != null) { result.append(indentation).append(" ON ").append(join.getOnExpression()).append("\n"); } } } // WHERE 절 if (plainSelect.getWhere() != null) { result.append(indentation).append("WHERE\n"); result.append(indentation).append(" ").append(plainSelect.getWhere()).append("\n"); } // GROUP BY 절 if (plainSelect.getGroupBy() != null) { result.append(indentation).append("GROUP BY\n"); result.append(indentation).append(" ") .append(plainSelect.getGroupBy().getGroupByExpressions().stream() .map(Object::toString) .collect(Collectors.joining(", "))) .append("\n"); } // HAVING 절 if (plainSelect.getHaving() != null) { result.append(indentation).append("HAVING\n"); result.append(indentation).append(" ").append(plainSelect.getHaving()).append("\n"); } // ORDER BY 절 if (plainSelect.getOrderByElements() != null) { result.append(indentation).append("ORDER BY\n"); result.append(indentation).append(" ") .append(plainSelect.getOrderByElements().stream() .map(Object::toString) .collect(Collectors.joining(", "))) .append("\n"); } } private void formatSelectItems(List items, StringBuilder result, int indent) { String indentation = String.join("", Collections.nCopies(indent, " ")); for (int i = 0; i < items.size(); i++) { result.append(indentation).append(items.get(i)); if (i < items.size() - 1) { result.append(","); } result.append("\n"); } } private void formatInsert(Insert insert, StringBuilder result, int indent) { String indentation = String.join("", Collections.nCopies(indent, " ")); result.append(indentation).append("INSERT INTO ").append(insert.getTable()).append("\n"); // 컬럼 목록 if (insert.getColumns() != null) { result.append(indentation).append("(") .append(insert.getColumns().stream() .map(Column::getColumnName) .collect(Collectors.joining(", "))) .append(")\n"); } result.append(indentation).append("VALUES\n"); // VALUES 절 포맷팅 if (insert.getItemsList() != null) { result.append(indentation).append(" ").append(insert.getItemsList()).append("\n"); } } private void formatUpdate(Update update, StringBuilder result, int indent) { String indentation = String.join("", Collections.nCopies(indent, " ")); // UPDATE 절 result.append(indentation).append("UPDATE\n"); result.append(indentation).append(" ").append(update.getTable()).append("\n"); // SET 절 result.append(indentation).append("SET\n"); List updateSets = update.getUpdateSets(); for (int i = 0; i < updateSets.size(); i++) { UpdateSet updateSet = updateSets.get(i); result.append(indentation).append(" ") .append(updateSet.getColumns().get(0)) .append(" = ") .append(updateSet.getExpressions().get(0)); if (i < updateSets.size() - 1) { result.append(","); } result.append("\n"); } // WHERE 절 if (update.getWhere() != null) { result.append(indentation).append("WHERE\n"); result.append(indentation).append(" ").append(update.getWhere()).append("\n"); } // ORDER BY 절 (일부 데이터베이스에서 지원) if (update.getOrderByElements() != null) { result.append(indentation).append("ORDER BY\n"); result.append(indentation).append(" ") .append(update.getOrderByElements().stream() .map(Object::toString) .collect(Collectors.joining(", "))) .append("\n"); } // LIMIT 절 (일부 데이터베이스에서 지원) if (update.getLimit() != null) { result.append(indentation).append("LIMIT ") .append(update.getLimit()).append("\n"); } } private void formatDelete(Delete delete, StringBuilder result, int indent) { String indentation = String.join("", Collections.nCopies(indent, " ")); // DELETE 절 result.append(indentation).append("DELETE FROM\n"); result.append(indentation).append(" ").append(delete.getTable()).append("\n"); // WHERE 절 if (delete.getWhere() != null) { result.append(indentation).append("WHERE\n"); result.append(indentation).append(" ").append(delete.getWhere()).append("\n"); } // ORDER BY 절 (일부 데이터베이스에서 지원) if (delete.getOrderByElements() != null) { result.append(indentation).append("ORDER BY\n"); result.append(indentation).append(" ") .append(delete.getOrderByElements().stream() .map(Object::toString) .collect(Collectors.joining(", "))) .append("\n"); } // LIMIT 절 (일부 데이터베이스에서 지원) if (delete.getLimit() != null) { result.append(indentation).append("LIMIT ") .append(delete.getLimit()).append("\n"); } } }