JDBC API를 사용하여 DB와 연결하기 위해 Connection 객체를 생성하는 작업은 비용이 많이 드는 작업 중 하나이다.
이러한 문제를 해결하기 위해 애플리케이션 로딩 시점에 Connection 객체를 미리 생성하고,
애플리케이션에서 데이터베이스에 연결이 필요할 경우 미리 준비된 Connection 객체를 사용하여 애플리케이션의 성능을 향상하는 커넥션 풀 (Connection Pool)이 등장하였다.
Connection Pool
Java에서 JDBC의 Connection Pooling을 관리하는 라이브러리
빠르고 안정적인 성능으로 많은 Spring Boot 및 Java 기반 애플리케이션에서 사용된다
커넥션 풀이란 (Connection Pool)
1) 커넥션 풀 (Connection Pool) (1) 커넥션 풀이란 JDBC API를 사용하여 데이터베이스와 연결하기 위해 Connection 객체를 생성하는 작업은 비용이 굉장히 많이 드는 작업 중 하나이다. Connection 객체를 생성
shuu.tistory.com
이 글에서는 대표적인 커넥션 풀 오픈소스인 HikariCP에 대해 정리하려고 한다.
HikariCP
HikariCP는 Java에서 각각의 커넥션 풀을 연결하는 방법을 추상화한 DataSource 인터페이스를 구현한 구현체이다.
다른 구현체들에 비해 압도적인 성능을 발휘하여 Spring Boot 2.0 이후에는 HikariCP를 기본 DBCP로 채택하여 사용한다
어떻게 HikariCP는 이런 압도적인 성능을 낼 수 있었을까?
1. Byte Code 단순화
초기 Hikari는 JDK Dynamic Proxy를 사용하여 최적화하려 했지만, JDK Dynamic Proxy는 리플렉션 기반이라
프록시 클래스를 생성하는 과정에서 불필요한 바이트코드가 추가적으로 생성되는 문제점이 존재했다.
Hikari는 이 문제를 해결하기 위해 Javassist를 사용해 필요한 바이트코드만 작성해 문제를 해결하여 속도를 향상시켰다.
- Javassist : Java 바이트 코드를 조작하는 라이브러리
2. ConcurrentBag(custom Collection)사용
HikariCP는 Read/Write의 동시 작업 효율성을 향상시키기 위해 ConcurrentBag이라는 thread-safe한 컬렉션을 사용한다.
일반적인 풀(pool)은 BlockingQueue를 사용하는데, HikariCP는 ConcurrentBag을 사용하여 성능을 최적화한다
특징
- 스레드 로컬 저장소(Thread-Local Storage)
- 각각의 스레드를 다른 공간(Thread-Local Storage)에 저장해 경합을 최소화 하여 thread safe를 보장한다
- 각각의 스레드가 따로 관리되기 때문에 스레드가 데이터를 가져오거나 추가할 때, 다른 스레드가 작업을 수행하는 것을 차단하지 않는다.
- 커넥션 사용이 완료되면 같은 스레드가 재사용 할 수 있도록 ThreadLocal cache 에 저장한다
- Queue-stealing
- 일반적으로 Connection을 ThreadLocal cache에 저장하여 같은 스레드에서 같은 커넥션을 재사용하도록 한다
- 하지만 대기중인 스레드가 있다면 스레드는 커넥션이 캐시에 저장되기 이전에 가로채(stealing) 사용한다
- 즉시 직접 전달(Direct hand-off)받아 사용하기에 스레드와 커넥션의 유휴시간이 줄어들어 성능이 향상된다
- 직접 전달(Direct hand-off)
- 요청 스레드가 커넥션을 즉시 사용할 수 있도록 가능한 한 직접 전달하는 방법
- BlockingQueue는 블로킹(blocking)되어 지연이 발생할 수 있지만, ConcurrentBag 은 스레드별로 따로 저장되므로 블로킹을 피할 수 있다.
- 반환된 커넥션을 대기 중인 스레드에게 바로 넘겨줌으로써 큐에서 기다릴 필요 없이 즉시 재사용할 수 있다
- 커넥션 정리(Connection Eviction)
- ConcurrentBag는 필요 이상의 커넥션이 풀에 남아 있는 경우, 유휴(idle) 커넥션을 정리하는 기능 제공한다
- 일정 시간 동안 사용되지 않은 커넥션을 닫아서 메모리 사용을 최적화하며, 설정한 minimumIdle 개수만큼은 유지한다.
- 원본 객체의 동작을 변경하지 않고, 성능 측정, 로깅, 트랜잭션 관리 등 투명하게 처리하는 디자인 패턴
- 프록시 객체는 중간에서 호출을 가로채 추가적인 작업을 처리하기 위한 객체이다프록시(proxy) 패턴프록시(proxy) 패턴
- HikariCP는 상태 추적, 성능 측정, 로깅, 트랜잭션 관리 등 여러 작업들을 효율적으로 처리하기 위해 커넥션 풀에서 제공하는 JDBC 커넥션 객체를 프록시 객체로 감싸 메서드 호출 시 프록시 객체가 가로채서 추가적인 작업을 처리한 후, 실제 커넥션 객체에 전달하거나 값을 반환한다.
- 요청을 가로채 추가 작업을 하고 호출한 메서드를 실행 O
- Statement의 executeQuery() 메서드를 호출 시, 프록시 객체가 쿼리 실행 시간 측정, 성능 로깅, 트랜잭션 관리 등 작업을 수행
- 이후 executeQuery() 메서드를 호출하여 데이터베이스 쿼리 실행
- 요청을 가로채 추가 작업만 진행하고 호출한 메서드를 실행 X
- Connection 객체의 close() 메서드가 호출 시, 커넥션을 풀에 반환하는 작업만 수행
- 원래의 close() 메서드는 실행되지 않으며, 커넥션이 종료되지 않고 풀에 반환만 이루어짐
- 요청을 가로채 추가 작업을 하고 호출한 메서드를 실행 O
- FastList 사용
- Hikari가 만든 custom list로 ArrayList를 상속받아 구현된 자료구조이다
- ArrayList의 메서드를 사용할 수 없으며, 유연성이 떨어져 대부분의 경우 ArrayList가 유리하다
이외에도 위의 특징들로 인해
CPU time splice(커넥션을 관리하는 데 드는 CPU 시간)최적화,
HikariCP Artifact 사이즈 최소화 등 여러 이점을 얻는다
3. Connection 시, BlockingQueue 와 ConcurrentBag 차이
BlockingQueue
- 스레드 A가 커넥션을 요청 → 사용 가능한 커넥션이 없으면 큐에서 대기
- 스레드 B가 커넥션을 반납 시 → 커넥션 풀의 BlockingQueue에 추가
- 스레드 A는 큐에서 커넥션을 가져오기 위해 대기 상태에 들어감
- 락이 풀리면 커넥션을 가져와서 사용 → BlockingQueue 내부에서 스레드 간 경합과 컨텍스트 스위칭 발생
ConcurrentBag
- 스레드 A가 커넥션을 요청
- ThreadLocal 캐시에 사용 가능한 커넥션이 있는지 확인 → 없으면 ConcurrentBag에서 가져옴
- 만약 ConcurrentBag에서도 사용 가능한 커넥션이 없으면 대기 상태에 들어감
- 스레드 B가 커넥션을 반환
- 대기 중인 스레드가 없는 경우: 같은 스레드가 다시 사용하도록 ThreadLocal Cache에 우선 저장
- 이후 요청 시 ThreadLocal Cache에서 즉시 가져와 대기 시간 없이 사용
- 일정 시간 동안 ThreadLocal Cache에 저장된 커넥션을 같은 스레드가 다시 사용하지 않으면 ConcurrentBag의 공유 풀(Pool)에 반환
- 반환된 커넥션은 ConcurrentBag의 공유 리스트(shared list)에 추가
- 이후 새로운 스레드가 커넥션을 요청하면, 이 공유 리스트에서 가져가서 사용
- 공유 리스트에 추가되면 Direct Hand-off 방식과 비교했을 때 약간의 지연이 발생
- 대기 중인 다른 스레드(A)가 있으면 즉시 가져가도록 허용 (Queue-Stealing)
- 스레드 A가 즉시 커넥션을 가져와 사용
- Queue-Stealing 덕분에 즉시 할당받아 사용하여 대기 시간이 줄어들고, 전체적인 시스템 처리량이 증가
'DB' 카테고리의 다른 글
[Redis] 레디스 자료형 8가지 (0) | 2025.06.24 |
---|---|
FETCH JOIN (0) | 2025.03.18 |
Java - DB 연결2: ORM (0) | 2023.06.26 |
Java - DB 연결1: JDBC (0) | 2023.06.26 |
Python mongoDB Join 사용 코드 (0) | 2023.06.09 |