MyBatis 실행된 SQL 문자열을 가져오는 방법

최정환·2025년 1월 18일
0

MyBatis 실행된 SQL 문자열을 가져오는 방법

ISMS 진행하며 어드민에서 자료를 조회할 때 조회한 자료를 저장하는 것이 정석적인 접근입니다.
하지만 리소스 부족으로 인해 사용자가 실행한 SQL 쿼리를 문자열로 저장하는 방식으로 대체해야 하는 상황이 발생했습니다.

SQL 쿼리를 그대로 저장하려면 MyBatis에서 실행된 쿼리문을 완성된 형태로 가져오는 방법이 필요했지만 이 과정이 쉽지 않았습니다. 그러던 중 아래의 코드를 활용하여 문제를 해결할 수 있었고 이를 공유하고자 합니다.

1. 문제 상황: 실행된 SQL 쿼리 문자열 가져오기 어려움

MyBatis는 매퍼 파일에서 쿼리를 실행할 때 "?"로 표현된 파라미터와 실제 값을 런타임에 매핑합니다. 그러나 실행된 쿼리 문자열(즉, 파라미터가 채워진 완성된 SQL)을 바로 가져오는 기능은 제공되지 않습니다.
이에 따라 직접적으로 실행된 쿼리 문자열을 반환하는 메서드를 작성해야 했습니다.

2. 문제 해결: MyBatis의 BoundSql을 활용한 쿼리 생성

MyBatis의 BoundSql 객체를 활용하면 실행된 쿼리 템플릿과 파라미터 매핑 정보를 가져올 수 있습니다. 이를 이용해 "?"를 실제 값으로 치환한 완성된 SQL 문자열을 만들어 냅니다.


다음은 SQL 쿼리문을 문자열로 반환하는 메서드를 만들어 봅니다.

/**
 * MyBatis 쿼리를 실행하지 않고, SQL 쿼리문을 문자열로 반환하는 메서드
 * @param id MyBatis 매퍼의 SQL ID (쿼리명)
 * @param param 쿼리에 전달될 파라미터 객체 (DTO 또는 Map)
 * @return String 완성된 SQL 쿼리문
 */
public String getSql(String id, Object param) {
    BoundSql boundSql;

    // 파라미터가 Map일 경우 그대로 사용, DTO일 경우 Map으로 변환
    if (param instanceof Map) {
        boundSql = sqlSession.getConfiguration().getMappedStatement(id).getBoundSql(param);
    } else {
        Map<String, Object> paramMap = convertDtoToMap(param);
        boundSql = sqlSession.getConfiguration().getMappedStatement(id).getBoundSql(paramMap);
    }

    // SQL 템플릿과 파라미터 매핑 정보 가져오기
    String sql = boundSql.getSql();
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    Map<String, Object> params = convertDtoToMap(param);

    // ?를 실제 값으로 치환 (SQL 완성)
    for (ParameterMapping mapping : parameterMappings) {
        String propertyName = mapping.getProperty();
        Object value = params.get(propertyName);
        sql = sql.replaceFirst("\\?", "'" + value + "'");
    }

    System.out.println("Generated SQL: " + sql);
    return sql;
}

DTO를 Map으로 변환하는 메서드

파라미터가 DTO 객체일 경우 내부 필드와 값을 추출해 Map<String, Object>로 변환해야 합니다. 아래는 이를 수행하는 메서드입니다.

/**
 * DTO 객체를 Map<String, Object>로 변환하는 메서드
 * @param dto 변환할 DTO 객체
 * @return Map<String, Object> DTO의 필드명과 값을 가진 맵 객체
 */
public Map<String, Object> convertDtoToMap(Object dto) {
    Map<String, Object> map = new HashMap<>();
    Field[] fields = dto.getClass().getDeclaredFields();  // DTO의 모든 필드 가져오기

    try {
        for (Field field : fields) {
            field.setAccessible(true);  // private 필드 접근 허용
            map.put(field.getName(), field.get(dto));  // 필드명과 값을 Map에 추가
        }
    } catch (IllegalAccessException e) {
        throw new RuntimeException("DTO to Map 변환 중 오류 발생", e);
    }
    return map;
}

3. 동작 원리

  1. 쿼리 템플릿과 파라미터 매핑 정보 가져오기
    • MyBatis의 getMappedStatement를 통해 SQL 템플릿과 매핑 정보를 가져옵니다.
  2. DTO 객체를 Map으로 변환
    • DTO의 모든 필드와 값을 Map 형태로 변환합니다.
  3. ?를 실제 값으로 치환
    • 파라미터 매핑 정보를 순회하며 ?를 실제 값으로 대체하여 완성된 SQL을 생성합니다.

4. 실제 사용 예시

MyBatis 매퍼 파일 예시

<select id="getUserById" parameterType="map" resultType="User">
    SELECT * FROM users WHERE id = #{id} AND status = #{status}
</select>

호출 코드

Map<String, Object> params = new HashMap<>();
params.put("id", 123);
params.put("status", "ACTIVE");

String sql = getSql("getUserById", params); // mapper에서 사용하는 메서드 id 문자열, ? 매핑을 위한 param
System.out.println(sql);
// 출력 결과: SELECT * FROM users WHERE id = '123' AND status = 'ACTIVE'

5. 느낀 점 및 활용 방안

이 방법을 통해 사용자가 실행한 쿼리를 문자열로 가져오는 문제가 해결되었습니다. 이 코드는 다음과 같은 상황에서 유용합니다:

  1. 사용자 활동 로깅: 실행된 SQL을 기록하여 감사 로그로 저장.
  2. 문제 디버깅: 실행된 SQL 쿼리를 확인하여 디버깅 효율성 향상.
  3. 보안 강화: 실행된 쿼리를 검증하여 악의적인 쿼리를 방지.

개인적인 생각

이런 작업은 일반적인 상황에서 필요하지 않다
실행된 SQL 쿼리를 저장하는 것은 대부분의 프로젝트에서 과도한 작업이 될 가능성이 큽니다.
• SQL 쿼리 자체를 저장하기보다는, 조회된 결과를 저장하는 것이 정석적이고 효율적입니다.
• SQL 쿼리 저장은 프로젝트 리소스를 소모시키고 추가 작업을 발생시킬 가능성이 높습니다. ex) 파일서버 or noSql 디비 도입

6. 결론

이번 경험은 MyBatis의 내부 동작을 이해하고 BoundSql 객체를 활용해 실행된 쿼리 문자열을 생성하는 방법을 알게 된 계기였습니다. 이 코드가 저처럼 실행된 SQL을 기록하려는 분들에게 도움이 되기를 바랍니다. 🙌

0개의 댓글