커넥션 풀
커넥션이란 WAS와 DB 사이의 연결을 의미하고, 다음과 같이 진행된다.
- 데이터베이스 드라이버를 사용해 데이터베이스 커넥션 열기
- 데이터를 읽고 쓰기 위해 TCP 소켓 열기
- TCP 소켓을 사용해 데이터 통신
- 데이터베이스 커넥션 닫기
- TCP 소켓 닫기
사용자로부터 요청이 들어올 때마다 이렇게 데이터베이스를 연결하고, 해제하는 것은 굉장히 비효율적으로 보인다. 이걸 커넥션 풀을 이용해 해결할 수 있다.
커넥션 풀은 WAS가 실행되면서 DB와 미리 커넥션을 해놓은 객체들을 pool에 저장해두었다가, 클라이언트 요청이 오면 커넥션을 빌려주고, 처리가 끝나면 다시 커넥션을 반납받아 pool에 저장하는 방식이다.
장점
- 커넥션 객체를 생성하고 해제하는 과정은 시간이 오래 걸리고 자원을 많이 소모하기 때문에 커넥션 풀을 사용하면 미리 생성된 커넥션을 재사용할 수 있어 성능이 향상된다.
- 커넥션 수를 제한할 수 있어 과도한 접속으로 인한 서버 자원 고갈 방지가 가능하다.
- DB 접속 모듈을 공통화하여 DB 서버 환경이 바뀔 경우 쉬운 유지 보수가 가능하다.
커넥션 풀을 무조건 크게 만들면 성능이 좋아질까?
- WAS와 DB와의 연결은 쓰레드가 담당하고, 쓰레드 또한 쓰레드 풀을 가지고 있다. 쓰레드 풀의 크기보다 커넥션 풀의 크기가 더 커지게 되면 메모리상에서 남은 커넥션들은 작업을 하지 못하고 놀게 되기 때문에 메모리만 차지하게 된다.
- 그렇다고 무작정 쓰레드 풀의 크기를 늘린다고 해결되지 않는다. 쓰레드의 증가는 Context switching으로 인한 한계가 존재한다.
커넥션 풀의 크기는 얼마가 적당할까?
- MySQL의 공식문서에서는 600명의 유저에 대해 15~20개의 크기가 적당하고, 그 이상부터는 적절하게 부하 테스트를 진행하며 적당한 값을 직접 찾는 것을 추천하고 있다.
- Hikari CP의 공식문서에 따르면 connections = ((core_count * 2) + effective_spindle_count)를 추천하고 있다.
- core_count는 서버 환경에서의 CPU 코어 개수를 의미한다. core_count * 2를 하는 이유는 context switching으로 인한 오버헤드를 고려하더라도 데이터베이스에서 Disk I/O 혹은 DRAM이 처리하는 속도보다 CPU 속도가 월등히 빠르기 때문에 쓰레드가 Disk와 같은 작업에서 블로킹되는 시간에 다른 쓰레드의 작업을 처리할 수 있는 여유가 생기게 된다. 이러한 여유 정도에 따라 멀티 쓰레드 작업을 수행할 수 있고, Hikari CP가 제시한 공식에서는 계수를 2로 선정하여 쓰레드 개수를 지정했다.
- effective_spindle_count는 하드디스크와 관련이 있다. 하드디스크 하나는 spindle 하나를 가진다. 이에 따라 spindle 수는 기본적으로 DB 서버가 관리할 수 있는 동시 I/O 요청 수를 말한다. 디스크가 4개 있는 경우 시스템은 동시에 4개의 I/O 요청을 처리할 수 있다. 해당 공식에서는 디스크의 효율을 고려하여 spindle_count를 더해준 것으로 보인다.
커넥션 풀의 라이브러리들
커넥션 풀의 라이브러리에는 대표적으로 Commons DBCP, tomcat-jdbc-pool, HikariCP 가있다.
Commons DBCP
- Apache에서 제공하는 커넥션 풀 라이브러리.
- 동작 방식
- PoolableConnection 타입의 커넥션을 생성하고 생성한 커넥션에 ConnectionEventListener를 등록한다. ConnectionEventListener에는 애플리케이션이 사용한 커넥션을 풀로 반환하기 위해 JDBC 드라이버가 호출할 수 있는 콜백 메소드가 있다.
- 이렇게 생성된 커넥션은 commons-pool의 addObject() 메소드로 커넥션 풀에 추가된다. 이때 commons-pool은 내부적으로 현재 시간을 담고 있는 타임스탬프와 추가된 커넥션의 레퍼런스를 한 쌍으로 하는 ObjectTimestampPair라는 자료구조를 생성하고 LIFO형태의 CursorableLinkedList로 관리한다.
- 옵션
- initialSize - 최초 커넥션을 맺을 때 pool에 생성되는 커넥션의 개수
- maxActive : 동시에 사용할 수 있는 최대 커넥션의 개수
- maxIdle : 커넥션 풀에 반납할 때 최대로 유지될 수 있는 커넥션의 개수
- minIdle : 최소한으로 유지할 커넥션의 개수
- maxWait : 사용가능한 커넥션 객체가 없는 경우, pool이 예외를 던지기 전 연결이 반환 될 때까지 대기하는 최대 시간. 기본값은 무한정이다.
- 속성을 설정하지 않아도 일반적인 상황에서는 큰 문제가 되지 않지만 사용자가 갑자기 급증하거나 DBMS에 장애가 발생했을 때 장애를 더 크게 확산시킬 수 있어 주의해야한다.
- 커넥션 개수와 관련된 속성 및 조건
- maxActive ≥ initialSize
- 동시에 사용할 수 있는 최대 커넥션의 개수가 10이고 최초 커넥션을 맺을 때 생성되는 개수가 20개라면 initialSize 값이 최대 커넥션 개수인 maxActive보다 커서 논리적으로 오류가 있는 설정이다.
- maxIdle ≥ minIdle
- 최대로 유지할 커넥션의 개수가 최소한으로 유지될 커넥션의 개수보다 많아야 한다.
- initialSize = maxActive = maxIdle = minIdle
- 위 네 가지의 설정은 동일한 값으로 통일해도 무방하다. 커넥션 개수와 관련된 가상 중요한 성능 요소는 일반적으로 커넥션의 최대 개수이기 때문에 위 항목의 설정 값 차이는 성능을 좌우하지 않는다.
- 유효성 검사 쿼리와 Evictor 쓰레드 관련 설정으로 애플리케이션의 안정성을 높일 수 있다.
- JDBC 커넥션의 유효성은 validationQuery옵션에 설정된 쿼리를 실행해 확인할 수 있다.다음과 같은 세 가지 테스트 옵션으로 유효성을 검사한다.
- testOnBorrow : 커넥션 풀에서 커넥션을 얻어올 때 테스트 실행
- testOnReturn : 커넥션 풀로 커넥션을 반환할 때 테스트 실행
- testWhileIdle : Evictor 쓰레드가 실행될 때 커넥션 풀 안에 있는 유휴 상태의 커넥션을 대상으로 테스트 실행
- 검증에 지나치게 자원을 소모하지 않게 testOnBorrow, testOnReturn 옵션은 fasle로 설정하고 오랫동안 대기 상태였던 커넥션이 끊어지는 현상을 막기 위해 testWhileIdle 옵션은 true로 설정하는 것이 좋다.
- Evictor 쓰레드는 DBCP 내부에서 커넥션 자원을 정리하는 구성 요소이며 별도의 쓰레드로 실행된다.
- Evictor 쓰레드의 역할은 크게 3가지이다.
- 커넥션 풀 내의 유휴 상태의 커넥션 중에서 오랫동안 사용되지 않은 커넥션을 추출해 제거한다.
- 커넥션에 대해 추가로 유효성 검사를 수행해 문제가 있을 경우 해당 커넥션을 제거한다.
- 앞의 두 작업 이후 남아있는 커넥션의 개수가 minIdle 속성값보다 작으면 minIdle 속성값만큼 커넥션을 생성해 유지한다.
tomcat-jdbc-pool
- tomcat에 내장되어 사용되고 있다.
- Commons DBCP 라이브러리를 바탕으로 만들어져 있다.
- 스프링 부트 2.0 하위 버전에서 사용하는 기본 DBCP이다.
HikariCP
- 스프링 부트 2.0부터 기본으로 사용하는 커넥션 풀이다.
- 왜 스프링부트에서 HikariCP를 선택했을까?
- HikariCP는 성능 최적화에 초점을 맞춰 개발된 라이브러리로, 가볍고 빠른 속도를 제공하기 때문이다.
- 아래 사진과 같이 HikariCP팀에서 공개한 벤치마크를 보면 다른 커넥션풀 라이브러리보다 확실히 빠른 것을 알 수 있다.

- 동작 방식
- 쓰레드가 커넥션을 요청하면 커넥션 풀의 방식에 따라 유휴 커넥션을 찾아 반환한다. 이전에 사용했던 커넥션이 존재하는지 확인하고, 이를 우선적으로 반환하는 특징이 있다.
- 가능한 커넥션이 존재하지 않으면, handOffQueue를 Polling하면서 다른 쓰레드가 커넥션을 반납하기를 기다리고, 지정한 timeout 시간까지 대기하다가 시간이 만료되면 예외를 던진다.
- 최종적으로 사용한 커넥션을 반납하면 커넥션 풀이 커넥션 사용 내역을 기록하고 handOffQueue에서 커넥션을 받으려고 기다리는 쓰레드가 있다면 handOffQueue에 커넥션을 삽입한다. handOffQueue를 Polling하던 쓰레드는 커넥션을 획득하고 작업을 이어나간다.
- 옵션
- minimum-idle : 유지 가능한 최소 커넥션 개수
- maximum-pool-size : 유지 가능한 최대 커넥션 개수
- idle-timeout : 커넥션이 풀에서 사용하지 않는 상태로 남을 수 있는 최대 시간
- max-lifetime : 커넥션의 최대 유지 가능 시간
- connection-timeout : 풀에서 커넥션을 구할때 대기 시간
- HikariCP에서 커넥션 풀 크기 산정시 주의점
- Pool-locking 현상으로 인한 데드락 상태의 발생 가능성을 고려해야 한다.
- 다음은 간단한 예시이다.
- Thread-1이 작업을 수행하기 위해 2개의 커넥션이 필요하고, 커넥션풀의 사이즈는 1이라고 가정.
- Thread-1이 작업을 수행하기 위해 커넥션을 받아온다.
- 커넥션풀에 존재하는 커넥션은 1개이므로 1개만 Thread-1에게 건네준다.
- Thread-1은 1개의 커넥션으로 작업을 수행할 수 없으므로 다른 쓰레드로부터 1개의 커넥션이 반납될동안 기다린다.
- 하지만 반납될 커넥션이 존재하지 않는다. Thread-1은 자기자신이 커넥션을 반납하는 것을 기다리는 상황이고 데드락을 의미한다.
- HikariCP 공식 문서에서는 pool size = Tn * (Cm - 1) + 1 공식대로 최대 풀 크기를 설정하면 데드락을 피할 수 있다고 하고 있다.
- Tn : 전체 쓰레드 개수
- Cm : 하나의 Task에서 동시에 필요한 커넥션 수
참고