애플리케이션이 DB에 쿼리를 날리려면 먼저 연결(Connection)을 맺어야 한다. 그런데 이 과정이 생각보다 무겁다.
1. TCP 소켓 연결 (3-way handshake)
2. DB 인증 (아이디/비밀번호 검증)
3. DB 서버 쪽에서 세션용 메모리/프로세스 할당
4. (TLS를 쓰면) 암호화 핸드셰이크까지
이게 요청 한 번에 수 밀리초~수십 밀리초가 걸린다. 쿼리 자체는 1ms에 끝나는데 연결 맺는 데 20ms가 걸린다.
커넥션 풀 없이 매 요청마다 이렇게 하면:
요청 → 연결 생성(느림) → 쿼리 실행 → 연결 종료 → 응답
요청 → 연결 생성(느림) → 쿼리 실행 → 연결 종료 → 응답
... (매번 반복)
초당 수백~수천 요청이 들어오는 서버라면 연결 생성/해제만으로 서버와 DB가 모두 지쳐버린다.
커넥션 풀은 애플리케이션 시작 시 DB 연결을 여러 개 미리 만들어서 "풀(수영장/저장소)"에 담아두고, 필요할 때 빌려 쓰고 반납하는 방식
[애플리케이션] [커넥션 풀] [DB]
┌──────────────┐
요청 스레드 1 ──"빌려줘"──▶ │ 🔌 연결 #1 │ ◀━━ 미리 맺어둔
요청 스레드 2 ──"빌려줘"──▶ │ 🔌 연결 #2 │ 실제 연결들
요청 스레드 3 ──대기...──▶ │ 🔌 연결 #3 │ ━━━▶ PostgreSQL
│ (전부 사용중) │
└──────────────┘
1. 앱 시작 시: 풀이 연결을 N개(예: 10개) 미리 생성해서 보관
2. 요청이 들어오면: 스레드가 풀에서 유휴(idle) 연결을 하나 빌림 — 새로 만드는 게 아니라 이미 있는 걸 꺼내니 거의 0ms
3. 쿼리 실행 후: connection.close()를 호출하는데, 진짜로 끊는 게 아니라 풀에 반납됨 (프록시가 가로챔)
4. 다음 요청이 그 연결을 또 빌려 씀
연결이 10개인데 동시에 15개 요청이 들어오면, 나머지 5개는 대기. 누군가가 반납할 때까지 기다리다가, 제한 시간(HikariCP 기본 30초) 안에 못 빌리면 Connection is not available 예외가 터진다. 이게 실무에서 자주 보는 "커넥션 풀 고갈" 장애
원인은 보통:
커넥션 풀을 구현한 라이브러리. 다른 구현체(Tomcat JDBC, DBCP2 등)도 있지만 HikariCP가 가장 빠르고 가벼워서 Spring Boot 2.0부터 기본 커넥션 풀로 채택됨. spring-boot-starter-data-jpa만 넣어도 자동으로 딸려옴.
가장 확실한 하드 리밋. DB 서버 자체가 받아줄 수 있는 연결 수에 상한이 있다.
max_connections 기본 100max_connections 기본 151이걸 넘는 요청은 DB가 거부한다.
여기서 중요한 건 마이크로서비스가 여러 개면 합산.
user_service : 풀 10개
match_service : 풀 10개
chat_service : 풀 10개
feed_service : 풀 10개
assistant_service: 풀 10개
─────────────────────────
합계 50개가 DB에 연결됨
게다가 서비스를 스케일 아웃해서 인스턴스가 3대씩 뜨면? 50*3=150개
PostgresSQL 기본값 100을 이미 초과한다.
이런 경우 실무에서 배포 시 DB연결이 거부될 수 있다.
PostgreSQL은 연결 하나당 별도의 프로세스를 띄운다. 연결마다 수 MB의 메모리를 쓰고, 연결이 수백 개가 되면 DB 서버의 메모리와 CPU(컨텍스트 스위칭)가 낭비된다. 그래서 max_connections를 무작정 올리는 것도 답이 아니다.
이게 제일 직관에 반하는 부분. HikariCP 공식 문서가 강조하는 내용.
HikariCP 공식 문서가 강조하는 내용.
DB 서버의 CPU 코어가 8개라면, 실제로 동시에 처리할 수 있는 쿼리도 그 근처.
연결이 500개 있어봤자 8개만 일하고 나머지 492개는 DB 안에서 줄 서 있는 거. 오히려
HikariCP가 제시하는 유명한 공식:
pool_size = (CPU 코어 수 × 2) + 유효 디스크 수
예를 들어 DB 서버가 4코어 + SSD 1개면 9~10개 정도. 그래서 HikariCP 기본값이 10인 거.
실제 벤치마크에서 연결 2,000개 보다 96개짜리 풀이 더 높은 TPS를 낸 사례도 유명하다.
풀 크기만큼 앱 쪽에서도 소켓, 스레드 대기, 메모리를 쓴다. 그리고 애초에 톰캣 워커 스레드가 200개인데 풀이 500개면 의미가 없다(동시에 연결을 쓸 수 있는 주체가 최대 200개니까)
실무적인 순서는 아래와 같다.
1. 기본값(10)으로 시작 — 대부분의 서비스는 이걸로 충분
2. 모니터링 지표를 봄 — HikariCP는 active, idle, pending(대기 중인 스레드) 메트릭을 제공
3. pending이 자주 발생하면 그때 원인을 파악 — 풀이 작아서인지, 느린 쿼리/긴 트랜잭션이 연결을 오래 붙잡아서인지
4. 후자라면 풀을 늘리는 게 아니라 쿼리를 고쳐야 함 (풀 늘리기는 진통제일 뿐)
5. 늘릴 때도 서비스 수 × 인스턴스 수 × 풀 크기 < DB max_connections를 항상 계산
참고로 서비스/인스턴스가 정말 많아져서 연결 수가 감당이 안되면, PostgreSQL 앞단에 PgBouncer 같은 외부 커넥션 풀러를 두는 게 다음 단계의 해법. 여러 앱의 연결을 한 번 더 모아서 DB에는 적은 수의 연결만 유지해주는 중간 계층
정리하면: 설정은 자유지만, 상한은 DB의 max_connections이고, 최적값은 그보다 훨씬 작다.