DataSource 객체에 프록시를 하여 쿼리에 대한 상세 정보를 조회할 수 있는 라이브러리.

설정 방법

build.gradle.kts

implementation("net.ttddyy:datasource-proxy:1.10")

application.yaml

net.ttddyy.dsproxy.listener: DEBUG
spring:
  datasource:
    hikari:
      jdbc-url: jdbc:mysql://localhost:3306/mydb # 사용하시는 DB URL
      driver-class-name: com.mysql.cj.jdbc.Driver # DB에 맞는 드라이버
      username: root
      password: mypassword
      maximum-pool-size: 10

구현 방식

방법 1) 직접 DataSource 등록

DataSource를 프록시로 감싸 ProxyDataSourceBuilder를 통해 커스텀

@Configuration  
@EnableTransactionManagement  
public class DatabaseConfig {  
  
    @Value("${matchuri.query-monitor.enabled}")  
    public boolean queryMonitorEnabled;  
  
    @Bean  
    @ConfigurationProperties(prefix = "spring.datasource.hikari")  
    public HikariConfig hikariConfig(DataSourceProperties dataSourceProperties) {  
        HikariConfig hikariConfig = new HikariConfig();  
        hikariConfig.setJdbcUrl(dataSourceProperties.determineUrl());  
        hikariConfig.setUsername(dataSourceProperties.determineUsername());  
        hikariConfig.setPassword(dataSourceProperties.determinePassword());  
        hikariConfig.setDriverClassName(dataSourceProperties.determineDriverClassName());  
        return hikariConfig;  
    }  
  
    @Bean(name = "dataSource")  
    public DataSource dataSource(HikariConfig hikariConfig) {  
  
        DataSource originalDataSource = new HikariDataSource(hikariConfig);  
        if (!queryMonitorEnabled) return originalDataSource;  
  
        Formatter formatter = FormatStyle.BASIC.getFormatter();  
  
        // 기존 Hikari DataSource를 감싸는 Proxy DataSource 생성  
        return ProxyDataSourceBuilder.create(originalDataSource)  
                // 실행된 SELECT/INSERT/UPDATE/DELETE 등의 개수를 집계  
                .countQuery()  
                // 실행된 JDBC 쿼리 정보를 SLF4J의 DEBUG 레벨로 출력  
                .logQueryBySlf4j(SLF4JLogLevel.DEBUG)  
                // Proxy DataSource에 이름 부여  
                .name("matchuri-query-log")  
                // SQL을 Hibernate Formatter로 보기 좋게 포맷  
                .formatQuery(formatter::format)  
                // Query 로그를 여러 줄 형태로 출력  
                .multiline()  
                // 일정 시간보다 오래 걸린 쿼리를 별도로 WARN으로 기록  
                .logSlowQueryBySlf4j(2, TimeUnit.MINUTES, SLF4JLogLevel.WARN)  
                // 설정한 Proxy DataSource 생성  
                .buildProxy();  
    }  
}

방법 2) BeanPostProcessor를 활용하여 프록싱

BeanPostProcessor를 활용하여 Spring이 만들어준 Bean을 가로채기

@Configuration(proxyBeanMethods = false)  
@ConditionalOnProperty(name = "app.query.logging.enabled", havingValue = "true")  
public class DataSourceProxyConfig {  
  
    private static final String LOGGER_NAME = "org.example.knockin.query";  
  
    @Bean  
    static BeanPostProcessor dataSourceProxyBeanPostProcessor() {  
        return new BeanPostProcessor() {  
            @Override  
            public Object postProcessAfterInitialization(@NotNull Object bean, @NotNull String beanName) {  
                if (!(bean instanceof DataSource dataSource) || bean instanceof ProxyDataSource) {  
                    return bean;  
                }  
  
                return ProxyDataSourceBuilder.create(dataSource)  
                        .name(beanName)  
                        .logQueryBySlf4j(SLF4JLogLevel.INFO, LOGGER_NAME)  
                        .countQuery()  
                        .build();  
            }  
        };  
    }  
}

로깅 정보

  • Name: 이 쿼리를 실행한 ProxyDataSource의 이름
  • Connection: datasource-proxy가 식별을 위해 부여한 JDBC Connection 식별 번호
  • Time: JDBC 쿼리 실행에 걸린 시간(ms)
  • Success: JDBC 쿼리 실행의 성공 여부
  • Type: 사용된 JDBC Statement 타입. Prepared는 PreparedStatement 사용을 의미
  • Batch: JDBC Batch 실행 여부
  • QuerySize: 이번 실행 컨텍스트에 포함된 SQL 개수
  • BatchSize: Batch 실행에 포함된 파라미터 세트 개수. Batch가 아니면 보통 0
  • Query: 실제 실행된 Prepared SQL
  • Params: SQL의 ?에 바인딩된 실제 파라미터 값

ApiQueryCountFilter

해당 필터는 HTTP API 요청 하나가 처리되는 동안 발생한 SQL 쿼리 개수와 JDBC 실행 시간 집계하여 로깅한다.

import jakarta.servlet.FilterChain;  
import jakarta.servlet.ServletException;  
import jakarta.servlet.http.HttpServletRequest;  
import jakarta.servlet.http.HttpServletResponse;  
import java.io.IOException;  
import lombok.extern.slf4j.Slf4j;  
import net.ttddyy.dsproxy.QueryCount;  
import net.ttddyy.dsproxy.QueryCountHolder;  
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;  
import org.springframework.stereotype.Component;  
import org.springframework.web.filter.OncePerRequestFilter;  
  
@Slf4j  
@Component  
@ConditionalOnProperty(name = "matchuri.query-monitor.enabled", havingValue = "true")  
public class ApiQueryCountFilter extends OncePerRequestFilter {  
  
    @Override  
    protected void doFilterInternal(  
            HttpServletRequest request,  
            HttpServletResponse response,  
            FilterChain filterChain  
    ) throws ServletException, IOException {  
        QueryCountHolder.clear();  
  
        try {  
            filterChain.doFilter(request, response);  
        } finally {  
            QueryCount queryCount = QueryCountHolder.getGrandTotal();  
            log.info(  
                    "API_QUERY_BEFORE method={} uri={} status={} total={} select={} insert={} update={} delete={} other={} jdbcMs={}",  
                    request.getMethod(),  
                    request.getRequestURI(),  
                    response.getStatus(),  
                    queryCount.getTotal(),  
                    queryCount.getSelect(),  
                    queryCount.getInsert(),  
                    queryCount.getUpdate(),  
                    queryCount.getDelete(),  
                    queryCount.getOther(),  
                    queryCount.getTime()  
            );  
            QueryCountHolder.clear();  
        }  
    }  
}

오버헤드

운영환경에서도 DataSource Proxy를 적용하는 것은 심각한 성능 저하와 보안 이슈를 유발할 수 있다.
그렇기에 local/prod별 적정한 환경분리가 필요하다.

Hibernate 로깅과 비교

Hibernate 로깅(Statistics 포함)은 ORM 애플리케이션 계층에서 동작하고, DataSource Proxy는 JDBC 드라이버 계층에서 동작한다.

비교 항목Hibernate Logging / StatisticsDataSource Proxy (e.g., net.ttddyy)
파라미터 바인딩쿼리에는 ?로 표시되고, 하단에 별도 로그로 값 출력쿼리 내 ? 위치에 실제 값을 삽입하여 완성된 SQL 출력
쿼리 복붙 및 테스트불가능 (개발자가 ?에 값을 일일이 대입해야 함)즉시 가능 (로그를 복사해서 DataGrip/DBeaver에서 바로 실행)
로깅 커버리지JPA/Hibernate를 통해 실행된 쿼리만 로깅JdbcTemplate, MyBatis 등 모든 JDBC 통신 로깅
실행 시간 측정 기준ORM 내부 쿼리 생성 및 매핑 시간 위주실제 네트워크를 타고 DB에 다녀온 정확한 소요 시간
Slow Query 탐지전체 통계(Statistics) 위주의 확인특정 시간(예: 3초) 초과 쿼리만 필터링하여 경고 로그(WARN) 발생 가능
  • 압도적인 디버깅 생산성 (파라미터 인라인 매핑) 가장 큰 이유입니다. Hibernate의 TRACE 레벨 로깅을 켜면 파라미터가 출력되긴 하지만, 쿼리와 파라미터가 분리되어 나옵니다. 파라미터가 10개만 넘어가도 디버깅 시 쿼리를 조립하는 데 엄청난 시간이 낭비됩니다. Proxy는 완성된 쿼리를 뱉어주므로 즉시 DB 툴에 붙여넣어 실행 계획(Explain)을 확인할 수 있습니다.

  • 하이버네이트의 ‘거짓말’ 회피 Hibernate는 쿼리를 생성하고 캐시에 올리는 시점의 정보를 로깅합니다. 실제 DB로 쿼리가 날아가지 않았는데도(예: 트랜잭션 롤백, 지연 쓰기 등) 쿼리 로그가 찍히는 경우가 발생하여 혼란을 줍니다. Proxy는 실제 DB 커넥션을 타고 넘어가는 순간을 가로채므로 “진짜 DB에 실행된 쿼리”만 정확하게 보여줍니다.

  • 멀티 모듈 / 혼합 기술 스택에서의 통합 모니터링 실무에서는 JPA 하나만 쓰기보다 복잡한 통계 쿼리를 위해 JdbcTemplate이나 MyBatis를 혼용하는 경우가 많습니다. Hibernate 로깅은 JPA 외의 기술로 날아가는 쿼리는 전혀 잡지 못합니다. Proxy를 씌우면 애플리케이션에서 나가는 모든 DB 요청을 하나의 포맷으로 일관되게 수집할 수 있습니다.

  • 실제 네트워크 비용이 포함된 실행 시간(Slow Query) 측정 Hibernate Statistics가 제공하는 쿼리 실행 시간은 하이버네이트 내부의 처리 시간입니다. 반면 DataSource Proxy는 애플리케이션과 DB 사이의 네트워크 지연, Connection Pool 병목까지 포함된 ‘실제 체감 실행 시간’을 측정할 수 있어 실질적인 병목 탐지에 훨씬 유리합니다.

P6Spy와 비교

비교 항목P6SpyDataSource Proxy (net.ttddyy.dsproxy)
주요 설정 방식주로 spy.properties 같은 별도 외부 설정 파일 의존Java 코드(@Configuration, Builder) 기반의 유연한 제어
인지도 및 레퍼런스역사가 깊고 가장 많이 쓰이는 표준적인 라이브러리 (레퍼런스 압도적)상대적으로 모던하며, 객체지향적인 설정을 선호하는 개발자들이 사용
확장 및 커스터마이징정해진 옵션과 포매터 안에서 설정 변경커스텀 리스너(Listener), 파라미터 변환 로직 등을 Java 코드로 직접 구현하기 매우 용이함

AOP, Dynamic Proxy를 활용한 카운팅

추가적인 의존성 없이 구현하는 방식을 설명하는 블로그 글을 찾았다. 나중에 알아보자…

출처 및 참고자료

GitHub - jdbc-observations/datasource-proxy: Provide listener framework for JDBC interactions and query executions via proxy.Provide listener framework for JDBC interactions and query executions via proxy. - jdbc-observations/datasource-proxygithub.com [springboot] Datasource Proxy로 SQL query logging 하기1. Hibernate 기본 쿼리 로그 기본적으로 JPA를 사용하면 쿼리가 어떻게 찍히는지 확인이 필요하다. application.yml에 hibernate 옵션을 넣어주면 가독성 좋은 query를 찍어보며 개발할 수 있다. 아래는 쿼리찍히는 예시 2. Datasource Proxy 운영 환경에서 유용한 확장 기능을 제공하는 Datasource proxy(https://github.com/jdbc-observations/datasource-proxy)를 사용하면 더 풍부한 SQL query를 찍을 수 있다. [대표적으로 쓸만한 기능] slow query가 발생하면 INFO대신 WARN이나 ERROR로 로그를 찍수 있다. 쿼리 호출 앞뒤로 어떤 작업 하도록 설정 할 수 있다. hibernate에서는 ?표로..yeoon.tistory.com

모니터링 솔루션 개선하기