JDBC로 실행되는 SQL에 자동으로 프로젝트 정보 주석 남기기
이 글은 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 독립을 꿈꾸며 광복절 다음 날…
긴 글 읽어주셔서 고맙습니다.