이 글은 Java기반에서 JDBC와 이를 기반으로 한 Persistence Framework를 이용해 SQL을 실행할 수 있는 개발자를 대상으로 모든 JDBC를 통해 실행되는 SQL 구문에 애플리케이션 정보를 주석으로 넣는 법을 설명합니다.

왜 SQL 쿼리에 프로젝트 이름을 남기고 싶나?

보통 Monolithic 아키텍처로 프로젝트를 진행하게 되면 모든 데이터를 하나의 데이터베이스에 다 넣는 방식으로 개발을 하게 됩니다. 서비스가 계속 Monolithic으로 유지 가능한 수준이라면 상관없지만 운 좋게도 폭발적으로 성장하여 버틸 수 없게 되면 마이크로서비스 아키텍처로 하나씩 분리를 하게 됩니다.

이때 한 번에 모든 데이터베이스를 각각의 마이크로서비스에서 한 번에 나눠서 가져가면 좋겠지만 공통 데이터베이스에서 완전히 벗어나기에는 상당히 오랜 시간이 걸립니다.

이 긴 고통의 시간 동안에 매우 많은 문제가 발생하게 됩니다.

여러 프로젝트에서 하나의 공통 DB에 쿼리를 날리다 보니 어느 팀의 누군가가 인덱스 안 타는 쿼리를 잘못 짜거나 혹은 대규모의 데이터를 읽어들이는 Batch 성 작업을 수행했을 때 공통 DB는 CPU 100%를 치게 되고 고객들은 하염없이 기다려야 하는 일이 매우 자주 발생하게 됩니다.

이때 빠르게 어떤 프로젝트의 쿼리가 문제를 일으켰는지 찾아내면 좋습니다만, 이게 그리 쉽지가 않습니다. IP 주소가 있더라도 그 IP의 서버에 가서 어느 팀의 무슨 프로젝트 서버인지를 확인하는 등의 복잡도가 추가됩니다.

그래서 모든 SQL의 맨 앞에 어느 팀의 무슨 프로젝트에서 온 요청인지를 주석으로 남겨두면 느린 쿼리 로그를 보자마자 조금이라도 빠르게 해당 팀을 찾아가 문제를 해결할 수 있지 않을까요?

사실 이에 관한 글을 몇 년 전에 쓴 적이 있습니다. JDBC SQL 구문에 클라이언트 정보 남기기 하지만 이 글은 Hibernate 그리고 MySQL JDBC 드라이버에 국한한 기법이었습니다.

배달의 민족은 최소 두 가지 이상의 데이터베이스를 사용하며 Persistence Framework도 Hibernate, jOOQ, QueryDSL, MyBatis, Spring JDBCTemplate 등 다양하게 사용 중입니다.

따라서 좀 더 보편적인 해결책이 필요합니다.

구현의 기본 아이디어

Java의 데이터베이스 접속과 실행에 관한 API들(JDBC)은 DataSource, Connection, Statement, PreparedStatement 등의 인터페이스로 잘 추상화돼 있습니다.

실행되는 SQL 구문이 결정되는 순간은 Connection.prepareStatement("SQL 구문") 이때거나, 혹은 Statement.execute("SQL 구문"), Statement.executeQuery("SQL 구문"), Statement.executeUpdate("SQL 구문") 이 정도입니다(사실 더 있지만…).

Java의 모든 Connection Pool은 DataSource 인터페이스를 구현하고 있습니다.

여기서 간단한 아이디어가 도출됩니다.

  • DataSourceProxy & ConnectionProxy
    • Connection Pool DataSource를 감싸는 DataSourceProxy 를 만들고, 애플리케이션에는 DataSourceProxy를 노출합니다.
    • 여기서 getConnection()이 호출되면 실제 데이터소스가 리턴해준 Connection 객체를 감싸고 있는 ConnectionProxy 객체를 리턴해줍니다.
  • PreparedStatement
    • ConnectionProxy.prepareStatement("SQL 구문") 호출이 일어나면 실제로는 SQL에 애플리케이션 정보를 추가한 Connection.prepareStatement("/* 프로젝트 정보 주석 */ SQL 구문") 을 호출하고 그 결과를 반환합니다.
    • 이렇게 되면 그 결과로 나온 PreparedStatement 객체는 DB에 프로젝트 정보 주석이 들어가 있는 SQL을 전송하게 됩니다.
  • Statement
    • ConnectionProxy.createStatement() 호출이 일어났다면 실제 Statement 객체를 감싸는 StatementProxy 객체를 리턴해줍니다.
    • 이후 StatementProxy.execute/executeQuery/executeUpdate("SQL 구문") 이 호출되면 실제로는 Statement.execute/executeQuery/executeUpdate("/* 프로젝트 정보 주석 */ SQL 구문") 을 호출합니다.
    • 이렇게 되면 Statement 객체는 DB에 프로젝트 정보 주석이 들어가 있는 SQL을 전송하게 됩니다.

처음부터 모든 Proxy를 직접 구현해도 상관은 없겠으나 좀 더 쉽게 가는 방법을 찾아보았습니다.

Tomcat JDBC Connection Pool

Tomcat JDBC Connection Pool은 Tomcat과 함께 개발되고 있는 커넥션 풀로 HikariCP와 함께 요즘 가장 많이 사용되며(SpringBoot 1.x의 기본 커넥션 풀) 성능도 준수한 편입니다.

이 Connection Pool에는 JDBC Interceptor라는 개념이 있습니다. 커넥션풀에서 자체적으로 DataSourceProxy와 ConnectionProxy를 제공해주고 SQL 실행을 가로채서 slow query 로그를 남기는 등의 일을 할 수 있습니다.

문서를 보면 기본적으로 유용한 Interceptor 들을 몇 가지 제공해주고 있습니다.

저는 일을 간단히 끝내고자 Tomcat JDBC Connection Pool을 사용하고 SQL 구문을 가로채어 설정한 프로젝트 이름을 주석으로 맨 앞에 넣어주는 JDBC Interceptor 를 만들었습니다.

해당 소스코드는 woowabros/tomcat-jdbc-pool-sql-caller-info-comment라는 github 저장소에 공개해 두었습니다.

실제 코드는 파일 한 개이므로 사용하실 분들은 SqlCallerInfoCommentInterceptor.java 파일을 복사하여 자신의 프로젝트에 넣고 Tomcat JDBC Connection Pool을 만들어주시면 됩니다.

Spring Boot에서 YML로 설정한다면 다음과 같겠네요. baemin_in_woowabros 대신 자신이 넣고 싶은 정보를 넣습니다.

spring:
  datasource:
    url: .....
    type: org.apache.tomcat.jdbc.pool.DataSource
    tomcat:
      ....
      jdbc-interceptors: in.woowa.tomcat.jdbc.pool.interceptor.SqlCallerInfoCommentInterceptor(projectName=baemin_in_woowabros)

혹은 Java Code로 직접 설정한다면

import org.apache.tomcat.jdbc.pool.DataSource;

DataSource dataSource = new DataSource();
//... 기타 설정
dataSource.setJdbcInterceptors("in.woowa.tomcat.jdbc.pool.interceptor.SqlCallerInfoCommentInterceptor(projectName=baemin_in_woowabros)");

위와 같이 설정하면 모든 SQL 구문은 맨 앞에 /* baemin_in_woowabros */ SELECT .... 형태로 주석이 붙은 상태로 전송됩니다.

혹시나 몰라 SQL Injection에 대비하여 영문자/숫자/밑줄 등만 가능하게 하였습니다. 하지만 이 부분은 원하는 대로 코드를 변경해서 한글을 넣게 하셔도 무방합니다.

간단한 코드 설명

tomcat connection pool의 JDBC Interceptor는 org.apache.tomcat.jdbc.pool.JdbcInterceptor를 상속해서 구현해야 합니다만, 또 귀찮으므로 최대한 많이 구현된 기본 구현체인 org.apache.tomcat.jdbc.pool.interceptor.StatementDecoratorInterceptor를 상속하였습니다.

StatementDecoratorInterceptor는 기본적으로 DataSource Proxy와 Connection Proxy까지는 돼 있기 때문에 Connection.prepareStatement 메서드와 일반 Statement 생성시 SqlChangeStatementProxy라는 객체를 생성해주고 그 안에서 Statement.execute 등...이 호출될 때 SQL을 가로채어 바꿔치기하게 하였습니다.

마무리

주석으로 프로젝트를 넣는 방법은 사용법이 너무나 간단하지만, 실제로 문제가 발생했을 때 상당한 도움을 줄 것으로 생각됩니다.

여러 애플리케이션이 사용하는 공통 DB가 있다면 Java를 사용하지 않는 프로젝트라도 이런 기능을 만들면 좋을 것 같고, 그게 안 되더라도 직접 주석으로 DBA가 알아보고 바로 연락할 수 있는 정도의 주석을 SQL 앞단에 남겨주는 습관을 지니는 것이 좋을 것 같습니다.

귀찮은 일을 줄이려고 Tomcat JDBC Connection Pool의 JdbcInterceptor를 사용하게 했더니 설정은 참 쉽지만, 코드를 이해하는 것은 오히려 더 어려워진 것 같습니다. 어디까지가 JdbcInterceptor가 해주는 것이고 어디부터가 직접 구현한 것인지 경계가 명확히 안 드러나 보여서 두 코드를 다 이해해야만 하게 되었습니다. 저도 막상 글을 쓰려고 코드를 다시 보다 보니 매우 헷갈립니다.

DataSource 부터 모두 직접 Proxy를 만들어보는 것도 좋은 공부가 될 것 같고, 오히려 코드가 더 간결해질 것 같습니다. 또한 특정 Connection Pool에 의존하지도 않고요.

저는 안 했지만 누군가는 하실 거라 믿으며… / 어서 빨리 DB 독립을 꿈꾸며 광복절 다음 날…

긴 글 읽어주셔서 고맙습니다.