커넥션과 커넥션 풀(Feat. ThreadPool, HikariCP)

최민길(Gale)·2023년 9월 14일
1

Spring Boot 적용기

목록 보기
46/46

안녕하세요 이번 시간에는 커넥션과 커넥션 풀에 대해, 그리고 적절한 커넥션 풀과 Spring Boot에서 이를 어떤 식으로 적용하면 좋을지에 대해 알아보겠습니다.

먼저 커넥션이란 DB와 어플리케이션 간의 통신 경로로 다음과 같은 역할을 수행합니다.
1. 통신 경로 설정
2. 데이터 전송
3. 신뢰성 확보
4. 연결 유지 및 종료
5. 상태 관리

커넥션은 생성 시 많은 비용이 발생합니다. 그 이유는 TCP 통신 때문으로, 연결 시작 시 3-way handshake, 연결 종료 시 4-way handshake 과정을 거치기 때문에 리소스가 많이 소요되기 때문입니다.

이로 인해 커넥션 풀이 사용됩니다. 커넥션 풀은 커넥션을 관리하고 재사용하는데 사용되는 구성 요소로 커넥션을 미리 생성하여 연결이 종료된 커넥션을 재활용하는 방식으로 커넥션의 생성 및 제거 비용을 절약할 수 있습니다.

커넥션은 DB와 어플리케이션 간의 통신 경로이기 때문에 DB와 어플리케이션 양쪽에서 각각 생성되어 관리되어 각 컴포넌트들이 자체적으로 연결 관리를 수행하여 성능과 확장성을 향상시킬 수 있습니다. 따라서 커넥션 풀 역시 DB와 어플리케이션 양쪽에서 존재할 수 있습니다.

커넥션 풀은 일반적으로 다음의 동작 방식을 따릅니다.
1. 스레드가 커넥션을 요청하면 커넥션 풀이 유휴 커넥션을 찾아서 반환합니다.
2. 만약 가능한 커넥션이 존재하지 않으면 스레드가 HandOffQueue를 polling하여 다른 스레드가 커넥션을 반환할 때까지 대기합니다.
2-1. 이 때 지정한 시간까지 대기 후 만료 시 Exception을 던집니다.
3. 다른 스레드가 커넥션을 반환하면 커넥션 풀이 HandOffQueue에 반환된 커넥션을 추가합니다.
4. HandOffQueue를 polling하고 있던 스레드가 커넥션을 획득하여 태스크를 수행합니다.

여기서 중요한 점은 다른 스레드가 커넥션을 반환할 때까지 대기한다는 점입니다. 즉 커넥션 풀 크기가 작으면 커넥션을 획득하기 위해 대기하는 스레드가 많아져 성능 저하가 발생합니다. 하지만 커넥션 풀의 크기를 지나치게 늘리면 남는 유휴 커넥션들이 메모리 공간을 잡아먹어 리소스 낭비가 발생합니다. 따라서 적절한 커넥션 풀의 크기를 설정하는 것이 중요합니다.

또 한가지 중요한 점은 스레드가 커넥션을 요청한다는 점입니다. 즉 스레드가 커넥션을 사용하는 주체이기 때문에 커넥션 풀을 설정할 때 시스템의 스레드 개수도 같이 고려해야 합니다. 시스템의 스레드 풀에서 작업을 수행하는 스레드가 사용하는 커넥션 외의 남는 커넥션은 리소스를 낭비하기 때문입니다. CPU 하나의 코어는 하나의 작업을 처리하기 때문에 이상적인 스레드 수는 CPU 코어 수와 같다고 볼 수 있습니다. 만약 코어 수보다 스레드가 지나치게 많이 생성된다면 스레드 컨텍스트 스위칭이 발생하여 성능 저하가 발생할 수 있습니다.

그럼 코어 수보다 작거나 같게 스레드를 설정해야 최고의 퍼포먼스가 나올까요? 그렇지 않습니다. 스레드 컨텍스트 스위칭으로 인한 오버헤드보다 디스크 I/O 또는 네트워크 접속 시의 처리 속도가 크게 느리기 때문에 디스크 또는 네트워크 작업으로 스레드가 block되는 시간에 다른 스레드의 작업을 처리할 수 있습니다. 따라서 스레드 풀 내의 스레드 수는 코어 수보다 약간 높게 설정해주는 것이 좋습니다.

그렇다면 이론상으로 적절한 크기의 커넥션 풀을 어떻게 설정할까요? HikariCP와 PostgreSQL에서는 다음과 같이 커넥션 풀의 크기를 제안합니다.

커넥션 풀의 크기 = (코어 수 x 2) + effective_spindle_count

여기서 코어 수에 2를 곱하는 이유는 위에서 설명한 디스크 I/O 또는 네트워크 작업으로 스레드가 block되는 시간에 다른 스레드의 작업을 할당해주기 위함입니다. 그렇다면 effective_spindle_count이 무엇일까요? effective_spindle_count이란 회전하는 디스크의 수를 의미하며 하나의 spindel은 DB가 관리할 수 있는 동시 I/O 요청의 개수로 볼 수 있으며 이를 토대로 값을 보정해줍니다.

저는 AWS RDS에서 제공하는 AWS Proxy를 사용하기 때문에 커넥션 풀의 크기를 수동으로 설정할 필요가 없습니다. AWS Proxy의 경우 자동으로 커넥션 풀을 지원하기 때문에 시스템에서 적절한 풀의 크기를 설정해줍니다.

그럼 WAS에서의 커넥션 풀에 대해 알아보겠습니다. Spring Boot의 경우 HikariCP라고 하는 Java를 위한 고성능 JDBC 커넥션 풀 라이브러리를 디폴트 커넥션 풀로 사용합니다. HikariCP의 경우 커넥션을 지연 초기화하여 어플리케이션 시작 시 커넥션을 미리 생성하지 않아 시작 및 종료 시간을 단축하며, 필요한 커넥션만 생성하고 유휴 상태로 관리하기 때문에 커넥션 생성 시 불필요한 오버헤드가 발생하지 않습니다.

아래는 HikariCP 옵션을 설정하는 방법입니다. application.properties에 들어가 다음의 코드를 추가해줍니다. 물론 옵션은 자신이 원하는대로 충분히 바꿔가며 사용합니다. 저같은 경우 HikariCP 설정은 로컬 환경에서 테스트 시 디폴트로 진행하였습니다. 로컬 PC의 경우 8코어이지만 서버는 2코어로 성능 차이가 많이 발생해 추후 같은 환경에서 테스트를 진행해보려고 합니다.

# HikariCP 설정
spring.datasource.hikari.maximum-pool-size=10 # 커넥션 풀 최대 크기, 디폴트로 10개로 사이즈가 설정
spring.datasource.hikari.minimum-idle=5 # 커넥션 풀의 최소 유휴 커넥션 수, 디폴트로 maximum-pool-size로 설정되어 있어 최적의 성능과 응답성을 요구한다면 패스
spring.datasource.hikari.idle-timeout=30000 # 유휴 커넥션 최대 유지 시간

# Prepared Statement 설정
spring.datasource.hikari.cachePrepStmts=true # prepared statement의 캐싱 여부
spring.datasource.hikari.prepStmtCacheSize=250 # 하나의 커넥션에 캐싱할 prepared statement 개수
spring.datasource.hikari.prepStmtCacheSqlLimit=2048 # 캐싱할 prepared statement의 최대 길이

여기서 Prepared Statement 설정을 추가해주는 것을 추천합니다. Prepared Statement란 SQL 쿼리의 실행 계획을 미리 준비하고 캐시에 저장하는 SQL 실행 방법으로 초기에 PREPARE로 쿼리를 세팅한 후 EXECUTE로 실행하는 방식으로 동작합니다. 동일한 쿼리가 여러 번 실행될 때 실행 계획을 다시 생성하지 않고 캐시된 실행 계획을 사용하기 때문에 쿼리를 빠르게 실행할 수 있습니다.

-- 쿼리 준비
PREPARE stmt FROM 'SELECT * FROM employees WHERE department = ?';

-- 쿼리 실행
SET @department = 'IT';
EXECUTE stmt USING @department;

-- 쿼리 해제
DEALLOCATE PREPARE stmt;

Prepared Statement는 JPA 내부에서 자체적으로 사용하기 때문에 JPA를 사용하신다면 prepared statement를 따로 작성하거나 관리할 필요가 없습니다. 이는 반대로 말하면 Prepared Statement 설정이 되어 있다면 성능 향상이 가능하다는 뜻이기도 합니다.

아래는 Jmeter를 이용하여 API를 200번 동시 요청한 결과입니다. 위의 결과는 Prepared Statement 설정을 하지 않았을 때, 밑의 결과는 Prepared Statement 설정을 했을 때의 Latency 값을 나타냅니다. 테스트 결과 Prepared Statement 설정을 한 경우 약 18% Latency가 감소한 것을 확인할 수 있습니다.

그럼 지금까지 결론을 요약해보겠습니다. 커넥션 풀의 크기는 (코어 수 x 2) + effective_spindle_count로 설정하며, DB의 경우 커넥션 하나 당 하나의 스레드가 사용되기 때문에 스레드 풀의 크기 역시 커넥션 풀의 크기와 유사하게 설정하면 될 것으로 추정할 수 있습니다. 이는 실제 적용 시 테스트를 통해 안정적인 값을 찾아내야하며 이론상 값이라는 점을 유의해주시면 감사하겠습니다. 또한 DB가 아닌 경우 스레드 풀의 스레드가 커넥션을 사용하는 것 외에 다른 작업도 수행할 수 있으므로 더 크게 설정해야 한다는 점도 유의해주시면 될 것 같습니다. 그럼 이상으로 포스팅 마치도록 하겠습니다.

참고 자료
https://colour-my-memories-blue.tistory.com/15
https://hyuntaeknote.tistory.com/12

profile
저는 상황에 맞는 최적의 솔루션을 깊고 정확한 개념의 이해를 통한 다양한 방식으로 해결해오면서 지난 3년 동안 신규 서비스를 20만 회원 서비스로 성장시킨 Software Developer 최민길입니다.

0개의 댓글