애플리케이션이 데이터베이스(DB)와 통신할 때 가장 많은 비용이 발생하는 지점은 어디일까요? 많은 개발자가 쿼리 최적화에 집중하지만, 실제 성능 병목의 상당 부분은 ‘커넥션 생성’ 과정에서 발생합니다. TCP 3-Way Handshake와 DB의 인증 및 세션 설정 과정은 매우 무겁기 때문입니다. 이 문제를 해결하기 위한 기술인 커넥션 풀(Connection Pool)에 대해 깊이 있게 탐구합니다.
커넥션 풀의 정의와 탄생 배경
커넥션 풀은 애플리케이션 시작 시 미리 정해진 개수의 DB 연결을 생성하여 ‘풀(Pool)’에 담아두고, 필요할 때마다 이를 빌려주고 다시 반납받는 재사용 기법입니다.
과거의 단순한 방식은 요청이 올 때마다 DB 연결을 맺고 종료하는 것이었습니다. 하지만 현대적인 고트래픽 서비스 환경에서 매 요청마다 TCP 연결을 새로 맺는 것은 리소스 고갈을 야기합니다. DB 서버는 동시에 맺을 수 있는 연결 수에 제한이 있으며, 빈번한 연결 생성과 삭제는 서버의 CPU와 메모리 자원을 급격히 소모시킵니다. 따라서 연결을 ‘유지’하고 ‘재사용’하는 구조는 확장 가능한 시스템을 위한 필수 전략입니다.
핵심 원리와 구조적 이해
커넥션 풀을 이해하기 위해 ‘은행의 창구’ 비유를 사용해 보겠습니다.
- 풀(Pool): 은행의 대기실에 마련된 10개의 창구.
- 연결(Connection): 창구에서 업무를 보는 직원.
- 요청: 고객(애플리케이션 스레드)이 업무를 보기 위해 창구로 이동.
고객이 오면 직원을 새로 채용하는 것이 아니라, 대기 중인 빈 창구로 안내합니다. 창구가 모두 찼다면 고객은 자리가 날 때까지 대기합니다. 여기서 중요한 점은 직원이 업무를 마쳐도 퇴근(연결 종료)시키지 않고, 다음 고객을 위해 자리를 정돈(세션 초기화)한 뒤 대기 상태로 돌려놓는다는 것입니다.
[애플리케이션 스레드] -> [커넥션 풀 관리자] -> [유휴(Idle) 커넥션 리스트]
|
└─ [DB 서버]
코드 예제로 보는 구현 기초
파이썬의 psycopg2 라이브러리를 활용한 간단한 커넥션 풀 개념 구현입니다. 실제 실무에서는 SQLAlchemy의 QueuePool 같은 검증된 라이브러리를 사용해야 합니다.
import queue
# 1. 간단한 커넥션 풀 시뮬레이션
class SimpleConnectionPool:
def __init__(self, size):
self.pool = queue.Queue(maxsize=size)
# 초기 커넥션 생성
for _ in range(size):
self.pool.put("DB_CONNECTION_INSTANCE")
def acquire(self):
# 유휴 커넥션이 없으면 블로킹 대기
return self.pool.get(block=True)
def release(self, conn):
# 작업 완료 후 풀로 반환
self.pool.put(conn)
# 사용 예시
pool = SimpleConnectionPool(5)
conn = pool.acquire()
# ... 쿼리 실행 ...
pool.release(conn)
JavaScript(Node.js) 환경에서 pg 라이브러리를 사용하여 풀을 설정하는 방식입니다.
const { Pool } = require('pg');
// 2. pg 라이브러리의 커넥션 풀 설정
const pool = new Pool({
max: 20, // 최대 연결 수
idleTimeoutMillis: 30000, // 유휴 상태 유지 시간
connectionTimeoutMillis: 2000, // 연결 대기 시간
});
async function queryDB() {
const client = await pool.connect(); // 풀에서 빌림
try {
const res = await client.query('SELECT NOW()');
console.log(res.rows[0]);
} finally {
client.release(); // 반드시 반납!
}
}
실무 적용 사례와 설계 전략
실무에서 커넥션 풀 크기를 정할 때는 ‘DB 서버의 CPU 코어 수’와 ‘디스크 입출력 속도’를 고려해야 합니다. 무작정 풀 사이즈를 늘리는 것은 정답이 아닙니다. 오히려 컨텍스트 스위칭 비용을 증가시켜 성능을 저하시킬 수 있습니다.
일반적인 공식인 connections = ((core_count * 2) + effective_spindle_count)는 좋은 시작점입니다. 여기서 core_count는 DB 서버의 CPU 코어 수를 의미합니다. 실무에서는 모니터링 툴을 통해 ‘Active Connection’과 ‘Wait Time’을 추적하며 적정 수치를 찾아가는 튜닝 과정이 반드시 필요합니다.
자주 하는 실수와 해결법
가장 흔한 실수는 연결을 반납하지 않는 것입니다. 이를 ‘Connection Leak’이라고 합니다.
- 증상: 애플리케이션 운영 중 서서히 성능이 저하되다가 결국 "Too many connections" 에러와 함께 DB 접근이 차단됨.
- 해결법:
try-finally구문을 활용하여 어떤 예외 상황에서도release()가 호출되도록 강제합니다.- 커넥션 풀 라이브러리가 제공하는 ‘Leak Detection’ 기능을 활성화합니다. (예: HikariCP의
leakDetectionThreshold) - 커넥션의 유효 기간을 설정하여, 네트워크 불안정으로 인해 좀비 상태가 된 연결을 주기적으로 정리합니다.
더 공부할 리소스 추천
- HikariCP 공식 위키: 자바 진영의 표준 커넥션 풀 라이브러리이지만, 문서 내용 자체가 커넥션 풀 설계의 정석입니다.
- PostgreSQL 문서 - Performance Optimization: DB 엔진이 커넥션 요청을 어떻게 처리하는지 깊이 있는 기술적 내용을 제공합니다.
- Database Internals (Alex Petrov 저): 데이터베이스 시스템의 근본적인 아키텍처를 이해하는 데 큰 도움이 됩니다.