데이터베이스 따라잡기

Connection Pool과 ORM

meellon 2026. 7. 5. 05:30

학습 목표

Connection poolTCP·인증 비용을 줄이고 동시 요청제한하는 이유를 설명할 수 있다.

Pool 고갈(exhaustion)·connection leak·timeout 증상과 대응을 설명할 수 있다.

ORMSQL 생성·객체 매핑·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 · 연결 폭주전체 장애
  • /orders 200ms — 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 × poolSizemax_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+11번 부모 조회 + 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 HHH000104distinct · 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+11+N round-trip — JOIN·select in·batch
  • Lazy default · 필요 시 fetch · OSIV 주의
  • Bulkbatch·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