5.5 KiB
kind, slug, title, topic, project, status, sourceRevision, basisVersion, rootTreeNode, evidenceCapturedOn, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | basisVersion | rootTreeNode | evidenceCapturedOn | assets | evidence | source | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CONCEPT | requires-new-connection-cost | REQUIRES_NEW의 커넥션 비용과 풀 사이징 제약 | transaction-deadline-and-pool | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | Spring Boot 4.0.8 · PostgreSQL 16 · 리비전 21234e38 | concept:requires-new-connection-cost | 2026-09-01 |
|
|
|
REQUIRES_NEW의 커넥션 비용과 풀 사이징 제약
새 트랜잭션은 새 커넥션을 요구하고 바깥 커넥션은 반납되지 않는다. 그래서 풀 크기 하한이 동시 스레드 수와 중첩 깊이의 곱에 묶인다.
관계
- REQUIRES_NEW가 바깥 커넥션을 핀한 채 새 커넥션을 딴다 이 제약이 실제로 나타나는 사례다.
- 호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파 풀 획득이 예산의 일부라는 점에서 연결된다.
- 풀 계약 레인이 실행되지 않아 포화 동작이 확인되지 않았다 이 제약의 검증에 대한 미해결 질문이다.
본문
REQUIRES_NEW는 바깥 트랜잭션의 리소스를 유지한 채 독립적인 inner transaction 리소스를 요구할 수 있다. 이 프로젝트는 그 concurrency model을 기준으로 maximumPoolSize >= (concurrent_threads × (1 + max_inNew_depth)) + 1을 보수적 sizing rule로 사용한다. Spring API 전체에 성립하는 보편 법칙으로 읽어서는 안 된다.
커넥션 비용이 곱셈이 되는 이유
:::evidence key="requires-new-connection-cost" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true" :::
레코드마다 inNew 를 도는 루프가 금지인 이유
모든 worker가 outer connection을 잡은 채 inner connection을 기다리고 추가 여유가 없을 때 pool exhaustion이 생기며, 대기가 서로의 반납을 전제로 하면 교착 형태로 진행할 수 있기 때문이다.
같은 곱셈 함정이 database-per-tenant에서 반복된다
각 tenant 풀은 개별적으로 합리적이고 그 합이 아니다 — 50 tenant × 10 = 서버 max_connections 100에 500 커넥션. 실패는 idle이던 것 포함 모든 tenant에 동시에 도착한다.
:::note
풀 계약 레인 미실행
:::
일시 중단은 커넥션을 놓지 않는다
새 트랜잭션 모드는 바깥 트랜잭션을 일시 중단한다. 그 일시 중단은 트랜잭션 경계에 대한 것이다.
바깥 커넥션은 유지된다. 나중에 재개해야 하기 때문이다.
그래서 하한이 생긴다
설정 파일이 그 제약을 수식으로 적는다.
maxPoolSize >= concurrent_threads * (1 + max_inNew_depth) + 1
이 프로젝트가 가정한 모델에서 중첩 깊이 1이면 worker 하나가 outer와 inner connection을 동시에 필요로 할 수 있다. 예를 들어 동시 worker 100을 최대 동시성으로 잡고 여유 1을 두는 이 규칙이라면 201을 산정한다. 실제 필요한 값은 transaction manager 동작과 workload의 동시성으로 검증해야 한다.
여유가 없으면 pool exhaustion과 교착 위험이 생긴다
모든 worker가 바깥 커넥션을 잡고 안쪽 커넥션을 기다리는 동시에 pool에 남은 connection이 없으면 획득 대기가 누적된다. 이 대기가 서로가 반납해야 할 connection을 기다리는 구조가 되면 acquisition timeout과 교착 형태로 이어질 수 있다. 실제 발생 여부와 임계점은 workload와 pool 설정으로 확인해야 한다.
풀 사이징의 다른 근거와 충돌한다
같은 주석 블록이 반대 방향의 근거도 적는다.
D1 (feature-database-connection-pool-contract): small-pool axiom + PostgreSQL formula
starting point (maximumPoolSize = cores * 2 + effective_spindle_count, adjust via load
test). Fixed-size pool recommended (minimumIdle = maximumPoolSize).
작은 풀이 낫다는 공리와 코어 수 기반 시작점이다. 그 값은 대개 동시 스레드 수보다 훨씬 작다.
:::warning
두 기준은 서로 다른 질문에 답한다. 일반적인 pool sizing은 불필요하게 큰 pool을 피하려는 기준이고, 중첩 transaction 규칙은 특정 동시성에서 추가 connection 수요가 생길 수 있음을 반영한다. 먼저 불필요한 REQUIRES_NEW 중첩을 줄이고, 남은 동시성 모델을 부하 테스트로 확인한 뒤 maximumPoolSize를 정한다.
:::
고정 크기 풀
minimumIdle = maximumPoolSize인 fixed-size 설정은 connection creation 지연을 피하고 응답성을 예측하기 쉽게 하려는 운영 선택이다. minimumIdle이 더 작다고 maximumPoolSize 용량 자체가 사라지는 것은 아니다. REQUIRES_NEW 안전 여유는 maximumPoolSize와 실제 concurrency model로 판단한다.
멀티테넌시에서의 확장
테넌트별 풀 예산 타입이 이 제약을 테넌트 단위로 다시 적용한다. 테넌트 하나가 풀 전체를 소진하는 것을 막으면서도 각 테넌트의 중첩 하한을 만족해야 한다.