학습 목표
Connection pool이 TCP·인증 비용을 줄이고 동시 요청을 제한하는 이유를 설명할 수 있다.
Pool 고갈(exhaustion)·connection leak·timeout 증상과 대응을 설명할 수 있다.
ORM이 SQL 생성·객체 매핑·1차 캐시(session) 를 담당함을 설명할 수 있다.
N+1 쿼리가 어디서 생기는지 탐지하고 JOIN·fetch·batch 로 줄일 수 있다.
지연 로딩 vs 즉시 로딩·배치 fetch·bulk insert 트레이드오프를 설명할 수 있다.
문제 상황
- API 100 RPS — PostgreSQL
max_connections=100포화 · 새 연결 거부- 요청마다
DriverManager.getConnection()— TCP+SSL+auth 수 ms
- 요청마다
- HikariCP
maximumPoolSize=200— DB max 100 · 연결 폭주 후 전체 장애 /orders200ms — DB CPU 낮은데 쿼리 501번 (orders 1 + items 500)@OneToMany(fetch=LAZY)루프에서order.getItems()— N+1 은밀@ManyToOne(fetch=EAGER)전 엔티티 — cartesian JOIN 폭발@Transactional밖에서 lazy 접근 —LazyInitializationException/ 세션 닫힘
Part 4까지 엔진·분산을 봤다. 애플리케이션이 DB에 어떻게 붙는지 — 풀과 ORM — 가 실무 병목의 대부분이다.
1. Connection Pool이란
Connection pool — 미리 열어 둔 DB connection을 재사용하는 앱 측 풀.

| 매 요청 connect | Pool | |
|---|---|---|
| 비용 | TCP · TLS · auth · memory | borrow = 수 µs~ms |
| DB 부하 | connection 폭증 | 상한 (maxPoolSize) |
| 동시성 | max_connections 직접 소비 |
풀 크기 ≤ DB 한도 |
App thread → pool.borrow() → use connection → pool.return()
(wait if all busy)
| 상태 | |
|---|---|
| Idle | 풀에 대기 — 재사용 가능 |
| In-use | 스레드·코루틴이 점유 |
| Pending | 풀 고갈 — 대기 큐 |
# code/hikari-config.example
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=3000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.leak-detection-threshold=60000
| 설정 | 의미 |
|---|---|
maximum-pool-size |
동시 in-use 상한 |
connection-timeout |
borrow 대기 한도 — 초과 시 예외 |
max-lifetime |
오래된 connection 교체 (LB·failover) |
leak-detection |
return 안 한 borrow 로그 |
- 앞서 트랜잭션 — connection 하나 = TX 경계 (autocommit off)
- PgBouncer — 프록시 풀 — 앱 풀 × 인스턴스 >
max_connections완화
2. Pool 크기와 고갈
Pool sizing — 너무 크면 DB 과부하, 너무 작으면 대기·timeout.

Rule of thumb (web): pool ≈ (core_count * 2) + effective_spindle_count
Reality: measure · DB max_connections · replica routing
| 증상 | 원인 |
|---|---|
Connection is not available |
pool exhausted |
| DB connection count = max | 풀 합 > 한도 |
| 느린 API + DB idle | pool wait — CPU DB 밖 |
| 연결 monotonic 증가 | leak — finally close 누락 |
| 대응 | |
|---|---|
| Sizing | instances × poolSize ≤ max_connections − admin |
| Timeout | fail fast · circuit breaker |
| Leak fix | try-finally · @Transactional 범위 |
| PgBouncer | transaction pooling — prepared stmt 주의 |
| Read replica | read 풀 분리 (복제 편) |
- 장시간 TX — connection 점유 — 외부 HTTP TX 밖 (데드락 편)
- Serverless — 풀 per instance 폭발 — RDS Proxy·PgBouncer
3. ORM 개요
ORM — 테이블 row ↔ 객체 · SQL 생성 · 변경 추적.
| 계층 | 역할 |
|---|---|
| Mapping | @Entity · @Column · FK 관계 |
| Session / Persistence Context | 1차 캐시 · dirty checking |
| Query | JPQL · Criteria · QueryDSL |
| TX boundary | @Transactional — session 생명주기 |
Repository.save(order) → session tracks change → flush → INSERT/UPDATE SQL
- ORM ≠ SQL 대체 — 생성된 SQL·플랜 확인 (실행 계획 편)
- Raw SQL — 복잡 report · bulk — 혼용 정상
4. N+1 문제
N+1 — 1번 부모 조회 + N번 연관 lazy load.

# code/n-plus-one.example — SQLAlchemy style (anti-pattern)
orders = session.execute(select(Order).limit(100)).scalars().all()
for order in orders:
print(order.items) # lazy → SELECT * FROM order_items WHERE order_id = ? ×100
# 1 + 100 queries
| 탐지 | |
|---|---|
| 로그 | 같은 SQL 다른 bind 반복 |
| APM | span 수백 · DB round-trip |
| Hibernate | statistics · sessionFactory.getStatistics() |
해결
# code/fetch-join.example
from sqlalchemy.orm import selectinload, joinedload
# JOIN fetch — single query, wide result
orders = session.execute(
select(Order).options(joinedload(Order.items)).limit(100)
).unique().scalars().all()
# SELECT IN batch — 2 queries (orders + items IN (...))
orders = session.execute(
select(Order).options(selectinload(Order.items)).limit(100)
).scalars().all()
| 전략 | 쿼리 수 | 적합 |
|---|---|---|
| JOIN fetch | 1 | 1:1·소량 collection |
| SELECT IN (batch) | 2 | large collection · pagination |
| @BatchSize | 1 + ceil(N/batch) | Hibernate lazy 완화 |
| 수동 2-step | 2 | 명시 제어 |
-- equivalent: one JOIN
SELECT o.*, i.*
FROM orders o
LEFT JOIN order_items i ON i.order_id = o.id
WHERE o.user_id = 42;
- 앞서 쿼리 처리 — 조인 한 번 vs Nested Loop N번 — 네트워크 비용 더 큼
5. Fetch 전략

| Lazy | Eager JOIN | Batch / SELECT IN | |
|---|---|---|---|
| 로드 시점 | 접근 시 | 즉시 JOIN | 접근 시 IN batch |
| 쿼리 | N+1 위험 | 1 · cartesian | 소수 |
| 메모리 | 점진 | 한 번에 | 중간 |
| 기본 | @OneToMany 권장 |
남용 금지 | selectinload 실무 |
Lazy + loop → N+1
Eager on root entity → giant JOIN all relations
SelectIn / BatchSize → balanced default
| 규칙 | |
|---|---|
| 기본 LAZY | @OneToMany · @ManyToMany |
| 필요한 곳만 | fetch join · EntityGraph · selectinload |
| Open Session In View | 뷰까지 lazy — 숨은 N+1 · 끄고 서비스에서 fetch |
| DTO projection | SELECT new Dto(...) · 필요 컬럼만 |
- Pagination + JOIN collection — Hibernate
HHH000104— distinct · batch 설계
6. 배치 쿼리
Bulk — row-by-row save() 대신 배치 flush.
# code/batch-insert.example
from sqlalchemy import insert
rows = [{"user_id": u, "score": s} for u, s in data]
session.execute(insert(EventLog), rows) # executemany
# Hibernate: jdbc.batch_size=50, order_inserts=true
| row-by-row | batch / COPY | |
|---|---|---|
| round-trip | N | 1~few |
| WAL | 많음 | 적음 |
| ORM 이벤트 | 동작 | bypass (native) |
| PostgreSQL | INSERT loop | COPY · multi-row INSERT |
INSERT INTO event_log (user_id, score) VALUES
(1, 10), (2, 20), (3, 30); -- single statement
- 대량 마이그레이션 — ORM 끄고
COPY/ loader - 앞서 MVCC — 대량 INSERT — vacuum·인덱스 부하
7. Read replica와 풀
| Write pool | Read pool | |
|---|---|---|
| Target | Primary | Replica |
| Size | 작게 — TX 점유 | 크게 — stateless read |
| 주의 | strong read | lag · stale (복제 편) |
@Service
writeRepo → primaryPool (size 10)
readRepo → replicaPool (size 30)
8. 체크리스트
| 항목 | 확인 |
|---|---|
| Pool | instances × poolSize ≤ DB max? |
| Leak | borrow = return · leak detection? |
| N+1 | 로그·APM query count? |
| Fetch | 루프 안 lazy · OSIV 끔? |
| TX | 짧게 · 외부 I/O 밖? |
| Bulk | 10k+ row — batch·COPY? |
| Plan | ORM SQL EXPLAIN 확인? |
9. 정리
- Connection pool — 재사용·상한 — 고갈·leak 운영 이슈
- ORM — 편의 + 숨은 쿼리 — 생성 SQL 검증
- N+1 — 1+N round-trip — JOIN·select in·batch
- Lazy default · 필요 시 fetch · OSIV 주의
- Bulk — batch·COPY — 대량 ORM 지양
다음에 다룰 것
- 캐시와 DB
- cache-aside, TTL, invalidation
해당 내용은 Database System Concepts, 7/E (Avraham Silberschatz, Henry F. Korth, S. Sudarshan), HikariCP · SQLAlchemy · Hibernate 공식 문서 의 내용을 기반으로 합니다.
'데이터베이스 따라잡기' 카테고리의 다른 글
| CDC와 이벤트 (0) | 2026.07.07 |
|---|---|
| 캐시와 DB (0) | 2026.07.06 |
| NoSQL 개요 (0) | 2026.07.04 |
| 분산 트랜잭션 개요 (0) | 2026.07.03 |
| 파티셔닝과 샤딩 (0) | 2026.07.02 |