Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f032.md
T
DongHyeonkaandClaude Fable 5.1 b25357c48a docs(clean-architecture-backend-template): fold analysis into final and re-select one topic
- analysis/·source-index·state.json 을 final/document.md 제2부·제3부로 접었다. SSOT 는 하나다
- 파일럿 — commit-ambiguity-as-a-result 를 새 기준으로 재선별. 후보 14 → 글감 5
  (PROMOTE 5 · MERGE_INTO 3 · KEEP_IN_SSOT 4 · 보류 2). 기록 5건을 다시 썼고 그림 1개를
  techviz 로 만들었다
- 재선별이 잡은 것: 제1부 §6.2·§11.1 이 자기 §13.2 와 어긋나 있었다(레인을 안 돌렸다 vs
  돌렸다) — 정정. 이미 답이 나와 있던 Question 을 HEAD 재실행 질문으로 다시 세웠다.
  Concept 이 인용한 코드가 SSOT 에 없어 뺐다
- candidateScope·sourceRepository 기록. 나머지 43개 주제는 재선별 대기(PENDING 905)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:39:20 +09:00

157 lines
15 KiB
Markdown

---
kind: CASE
slug: analysis-finding-a05-f032
title: 포화된 풀의 대기 수를 단언하는 시험이 풀 계약 레인에 없다
topic: multitenancy-isolation
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:analysis-finding-a05-f032
evidenceCapturedOn: 2026-09-04
body: case-analysis-finding-a05-f032.body.md
assets:
- key: analysis-finding-a05-f032
file: ../../../final/evidence/rendered/analysis-finding-a05-f032.svg
evidence:
- ../../../final/evidence/raw/analysis-finding-a05-f032.txt
source:
- 원본 분석 절은 final/document.md#a05 §125 이다.
---
# 포화된 풀의 대기 수를 단언하는 시험이 풀 계약 레인에 없다
`jpa-nightly.yml:128` 은 이 레인이 포화된 풀의 대기 수 보고를 검사한다고 적는다. 그 주장을 확인하는 줄은 레인 전체에 `PoolPressureContractTest:31` 하나뿐이고, 거기 들어간 `3` 은 바로 앞줄이 생성자에 써 넣은 값이다. 실제 풀을 띄우는 `HikariPoolSaturationContractTest``:100` 에서 `getThreadsAwaitingConnection()` 을 읽어 담고도 그 값만 빼고 단언한다.
## 관계
- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다**
워크플로가 적은 셋 가운데 둘째만 실제 풀에 닿지 않는데, 레인이 초록이면 셋 다 확인된 것으로 읽힌다.
- **풀 계약 레인이 실행되지 않아 포화 동작이 확인되지 않았다**
그 질문은 이 레인이 세 계약을 검증한다는 것을 사실로 두고 실행만 남았다고 적는다. 이 기록은 그중 대기 수 보고에 단언이 없다는 것을 확인했으므로, 레인을 돌려도 그 미지수는 닫히지 않는다.
- **REQUIRES_NEW의 커넥션 비용과 풀 사이징 제약**
그 제약이 이 레인에서 실제 풀로 확인되는 유일한 주장이다. `RequiresNewPoolPressureContractTest:45``:61` 이 크기 1 과 2 로 나눠 확인한다.
## 문제
야간 워크플로가 이 레인이 검사하는 것을 셋으로 적는다. 같은 문장이 build.gradle 의 태스크 주석에도 있다.
셋 각각에 대응하는 단언이 있는지 확인했다.
## 결론
이 레인은 릴리스에 걸려 있다. build.gradle:309 가 jpaPlatformPoolContractTest 를 jpaPlatformReleaseGate 의 의존으로 넣고, 그 태스크가 도는 소스 세트에는 파일 셋에 시험 여덟이 있다.
셋 중 첫째에는 실제 HikariDataSource 가 있다. RequiresNewPoolPressureContractTest:45 가 크기 1 짜리 풀에서 안쪽 트랜잭션이 커넥션을 못 얻는 것을, :61 이 크기 2 에서는 얻는 것을 잡는다.
셋째는 절반만 그렇다. HikariPoolSaturationContractTest:64~:66 이 재는 것은 대기 시간의 상한뿐이라 즉시 실패해도 통과한다. 옆의 :77 은 아예 대기를 만들지 않는데, :81 이 하나를 놓은 다음에 :83 이 집기 때문이다.
두 번째는 다르다. 레인 안에서 pending() 을 단언하는 줄이 PoolPressureContractTest:31 하나인데, 그 값은 :29 의 new PoolMeasurement(4, 2, 3, Duration.ofMillis(80)) 에 넣은 것이다. HikariDataSource 가 없다.
같은 시험의 나머지 셋도 계산이거나 되읽기다. PoolMeasurement:26 의 total() 은 active + idle, :31 의 saturated() 는 pending > 0 이므로, :33 과 :34 는 손으로 넣은 값에서 유도되는 항등식이다.
실제 풀에서 그 값을 읽는 자리는 HikariPoolSaturationContractTest:100 이다. :95~:101 이 HikariPoolMXBean 에서 세 값을 읽어 PoolMeasurement 를 만드는데, :103~:105 의 단언은 active 와 total 과 커넥션 유효성이다.
그 시험이 쥔 커넥션은 :94 의 하나뿐이고 POOL_SIZE 는 :32 에서 2 다. 기다리는 스레드가 생길 수 없는 조합이다.
PoolMeasurement 자체가 testkit 소스 세트에 있고, HikariPoolMXBean 이나 getThreadsAwaitingConnection 을 읽는 프로덕션 자리는 main 파일 4714 개에 0 이다. 잘못된 것은 런타임이 아니라 레인이 자기에 대해 적은 문장이다.
## 검증 환경
OpenJDK : 21.0.12
확인 방식 : 야간 워크플로의 단계 주석과 그 내력 인용, 레인이 도는 소스 세트와 릴리스 게이트 의존 인용, 세 파일의 시험과 단언 전수, 레인 범위에서 pending() 단언 계수, 값 객체 시험과 포화 세 시험의 본문 인용, PoolMeasurement 의 import 자리와 계산 메서드 인용, 프로덕션에서 대기 수를 읽는 자리 계수와 훑은 파일 수 대조
소스 수정 : x
## 재현 조건
1. 워크플로의 단계 주석과 build.gradle 의 같은 문장, 그리고 릴리스 게이트 의존을 인용한다.
2. 레인이 도는 소스 세트의 파일을 나열하고 세 파일의 시험과 단언을 전부 뽑는다.
3. 레인 범위에서 pending() 을 단언하는 줄을 세고 그 시험 본문을 싣는다.
4. 실제 풀을 띄우는 세 시험을 전문으로 싣는다.
5. PoolMeasurement 를 어디서 import 하는지와 그 타입의 계산 메서드를 인용한다.
6. 프로덕션에서 대기 수를 읽는 자리를 세고 훑은 파일 수를 함께 센다.
## 본문
<!-- body:start -->
야간 워크플로가 JPA 풀 계약 레인을 부른다. 그 단계의 주석이 레인이 무엇을 검사하는지 셋으로 적는다.
## 야간 워크플로가 적은 셋과 레인의 여덟 시험
:::evidence key="analysis-finding-a05-f032" alt="저장소 루트에서 돌린 정적 검색 출력 271줄. 먼저 jpa-nightly.yml 122~134번 줄이 실린다. 단계 이름은 풀 포화와 REQUIRES_NEW 커넥션 동작을 검증한다는 것이고, 124~126번 주석이 이 단계가 예전에는 명시적 프로퍼티로 단언을 끄고 그 결과를 인증이라 불렀으며 그래서 유일하게 단언된 임계값이 임계값을 단언하지 않는다는 것이었다고 적는다. 127~129번이 지금 검사하는 셋을 적는데 REQUIRES_NEW 가 동시 스레드당 커넥션 둘을 요구한다는 것, 포화된 풀이 자기 대기 수를 보고한다는 것, 호출자가 커넥션 없이 진행하는 대신 기다린다는 것이며, 어느 러너에서나 참이라 끌 것이 없다고 적는다. 이어서 persistence-jpa/build.gradle 281~310번이 실린다. 281~286번 주석이 같은 셋을 다시 적고, 287~296번의 jpaPlatformPoolContractTest 태스크가 jpaPlatformPerformanceTest 소스 세트를 돌리며, 300~310번의 jpaPlatformReleaseGate 가 309번에서 그 태스크를 의존에 넣는다. 그 아래 소스 세트의 시험 파일 셋이 나열되는데 HikariPoolSaturationContractTest 와 PoolPressureContractTest 와 RequiresNewPoolPressureContractTest 다. 다음으로 세 파일의 시험 이름과 단언이 전부 나열되고 시험이 여덟이다. 이어서 레인 범위에서 measurement.pending 을 단언하는 줄이 1 개라고 나오고, 그 줄이 있는 시험이 실린다. PoolPressureContractTest 26~47번인데 29번이 new PoolMeasurement(4, 2, 3, Duration.ofMillis(80)) 을 만들고 31번이 pending 이 3 인지, 32번이 지연이 80밀리초인지, 33번이 total 이 6 인지, 34번이 saturated 가 참인지 확인한다. 37~47번의 두 번째 시험은 동시 스레드 8 과 깊이 1 로 required 를 8 곱하기 2 더하기 1 로 계산해 47번에서 17 과 같은지 확인한다. 그 아래 HikariPoolSaturationContractTest 32번의 POOL_SIZE 가 2 라는 것과 50~118번의 세 시험이 실린다. 52번 시험은 POOL_SIZE 만큼 쥔 뒤 61번에서 한 번 더 요청해 SQLException 이 나는 것과 64~66번에서 기다린 시간이 ACQUIRE_TIMEOUT 더하기 2초보다 작은 것을 확인한다. 77번 시험은 79번과 80번에서 둘을 얻고 81번에서 하나를 놓은 뒤 83번에서 세 번째를 얻어 유효한지 확인한다. 92번 시험은 94번에서 커넥션 하나만 쥐고 95~101번이 HikariPoolMXBean 에서 활성 수와 유휴 수와 getThreadsAwaitingConnection 을 읽어 PoolMeasurement 를 만드는데, 103~105번의 단언은 active 가 1 인지와 total 이 1 이상인지와 쥔 커넥션이 유효한지 셋이다. 110~118번이 그 풀을 만드는 pool 메서드다. 다음으로 RequiresNewPoolPressureContractTest 43~89번이 실린다. 45번 시험이 크기 1 짜리 풀에서 바깥 커넥션을 쥔 채 안쪽을 얻으려 하면 예외가 나는 것을, 61번 시험이 크기 2 에서는 얻어지고 두 커넥션이 다른 객체인 것을 확인하며, 80번 시험은 동시 스레드 1 과 깊이 1 로 required 를 계산해 88번에서 3 과 같은지 확인한다. 마지막으로 PoolMeasurement 를 import 하는 자리가 두 시험 파일이라는 것과 그 타입 6~34번이 실리는데, 9~11번 자바독이 대기 수와 획득 지연을 함께 기록하는 이유를 적으며 표본을 뜨는 순간에 대기 수가 0 으로 보이면서도 호출자들이 늘 기다리는 풀이 있을 수 있다고 적고, 26~28번의 total 이 active 더하기 idle 을, 31~33번의 saturated 가 pending 이 0 보다 큰지를 계산한다. 그 아래 프로덕션에서 대기 수를 읽는 자리가 0 개이고 그 검색이 훑은 main 파일이 4714 개라고 나온다." caption="야간 워크플로가 적은 셋과 그 내력 · 레인이 도는 태스크와 릴리스 게이트 의존 · 세 파일의 시험 여덟과 단언 전수 · 레인 범위의 pending 단언 하나와 그 입력 · 포화를 다루는 세 시험 전문 · REQUIRES_NEW 세 시험 전문 · PoolMeasurement 자바독과 계산 메서드 · 프로덕션의 대기 수 읽기 0 — 271줄 · exit 0" zoom="true"
:::
`jpa-nightly.yml:127`\~`:129` 가 셋을 적는다. `REQUIRES_NEW` 가 동시 스레드당 커넥션 둘을 요구한다는 것, 포화된 풀이 자기 대기 수를 보고한다는 것, 호출자가 커넥션 없이 진행하는 대신 기다린다는 것이다.
`:124`\~`:126` 은 이 단계의 내력도 적는다. 예전에는 명시적 프로퍼티로 단언을 끄고 그 결과를 인증이라 불렀고, 그래서 유일하게 단언된 임계값이 임계값을 단언하지 않는다는 것이었다.
`build.gradle:281`\~`:286` 이 같은 셋을 다시 적고 `:287` 의 태스크가 `jpaPlatformPerformanceTest` 소스 세트를 돌린다. `:300``jpaPlatformReleaseGate``:309` 에서 그 태스크를 의존에 넣으므로, 릴리스가 이 셋에 기댄다.
그 소스 세트에 있는 것은 `HikariPoolSaturationContractTest``PoolPressureContractTest``RequiresNewPoolPressureContractTest` 셋이고, 그 안의 `@Test` 를 전부 세면 여덟이다.
## 첫 번째는 실제 풀로 확인된다
`RequiresNewPoolPressureContractTest:45``poolOfOneCannotServeAnInnerTransaction` 은 크기 1 짜리 풀에서 바깥 커넥션을 쥔 채 안쪽을 얻으려 하면 `SQLException` 이 나는 것을 확인한다.
`:61``poolOfTwoServesTheSameNesting` 은 크기 2 에서는 얻어지고 두 커넥션이 다른 객체인 것을 확인한다.
둘 다 실제 `HikariDataSource` 를 띄운다.
같은 파일 `:80``sizingRuleMatchesTheObservedRequirement` 는 다르다. `:81`\~`:84``concurrentThreads = 1``maxRequiresNewDepth = 1``required = 1 * (1 + 1) + 1` 을 계산하고 `:88` 이 그것을 `3` 과 견준다. `PoolPressureContractTest:39` 도 같은 모양이고 숫자만 8 과 17 이다.
## 세 번째는 절반이다
`HikariPoolSaturationContractTest:52``saturatedPoolFailsWithinItsTimeout``POOL_SIZE` 만큼 쥔 뒤 한 번 더 요청한다. `:61``SQLException` 을, `:64`\~`:66` 이 기다린 시간이 `ACQUIRE_TIMEOUT` 에 2 초를 더한 값보다 작은 것을 확인한다.
상한만 있다. 얼마나 기다렸는지에 대한 하한 단언이 없어서, 즉시 실패해도 이 시험은 통과한다.
`:77``releasingAConnectionLetsTheNextCallerThrough` 는 기다림과 무관하다. `:79``:80` 이 둘을 얻고 `:81` 이 하나를 놓은 뒤 `:83` 이 세 번째를 얻으므로, 세 번째 호출은 이미 빈 자리를 집는다.
## 두 번째를 단언하는 유일한 줄은 손으로 만든 값이다
레인 안에서 `pending()` 을 단언하는 줄은 `PoolPressureContractTest:31` 하나다.
그 시험 `:29``new PoolMeasurement(4, 2, 3, Duration.ofMillis(80))` 을 만든다. `:31``pending()` 이 3 인지, `:32` 가 지연이 80 밀리초인지, `:33``total()` 이 6 인지, `:34``saturated()` 가 참인지 확인한다.
앞의 둘은 생성자에 넣은 값을 그대로 되읽는다. 뒤의 둘도 새로운 것을 보지 않는다. `PoolMeasurement:26`\~`:28``total()``active + idle` 이고 `:31`\~`:33``saturated()``pending > 0` 이므로, `4 + 2 = 6``3 > 0` 을 확인하는 것이다.
`HikariDataSource` 도 데이터베이스도 이 시험에는 없다.
## 실제 풀에서 읽은 대기 수는 단언되지 않는다
`HikariPoolSaturationContractTest:92``measurementReportsPoolState` 는 실제 풀을 띄우고 `:94` 에서 커넥션 하나를 쥔다.
`:95`\~`:101``HikariPoolMXBean` 에서 활성 수와 유휴 수와 `getThreadsAwaitingConnection()` 을 읽어 `PoolMeasurement` 를 만든다.
`:103`\~`:105` 의 단언은 셋이다. `active()` 가 1 인지, `total()` 이 1 이상인지, 쥔 커넥션이 유효한지다. `pending()` 은 없다.
그 풀은 포화 상태도 아니다. `:32``POOL_SIZE` 가 2 인데 커넥션 하나만 쥐었으므로 기다리는 스레드가 생길 수 없다.
`PoolMeasurement:9`\~`:11` 자바독이 이 함정을 직접 적는다. 표본을 뜨는 순간에 대기 수가 0 으로 보이면서도 호출자들이 늘 기다리는 풀이 있을 수 있다는 것이다.
## 이 결함이 있는 곳
`PoolMeasurement``testkit` 소스 세트의 타입이고, 두 시험 파일만 그것을 `import` 한다.
프로덕션에서 `getThreadsAwaitingConnection` 이나 `HikariPoolMXBean` 을 읽는 자리는 main 파일 4714 개에 0 이다. 런타임이 대기 수를 잘못 다루는 것이 아니라, 레인이 검사한다고 적은 것을 검사하지 않는다.
## 원문에 없는 것
원문은 야간 워크플로가 광고하는 셋 중 하나에 대한 단언이 레인 전체에 없다고 적는다. 여기에 더한 것은 그 주장에 가장 가까운 두 자리가 각각 어떻게 비껴가는지와, 나머지 두 주장에도 같은 모양이 섞여 있다는 점이다.
`PoolPressureContractTest:31` 은 값을 손으로 넣고 되읽고, `HikariPoolSaturationContractTest:100` 은 실제 값을 읽어 담고서 그것만 빼고 단언한다. 그리고 첫 번째 주장 쪽 `RequiresNewPoolPressureContractTest:80``PoolPressureContractTest:39` 는 리터럴 산술의 결과를 리터럴과 견주는 시험이다.
## 확인하지 못한 것
`jpaPlatformPoolContractTest` 를 돌리지 않았다. 여덟 시험의 통과 여부는 코드만 읽고 판단하지 않았다.
커넥션 둘을 모두 쥔 상태에서 그 MXBean 이 무엇을 돌려주는지 직접 재지 않았다.
계수 범위를 레인으로 한정했다. 저장소 전체에는 `src/test``PoolMeasurementTest` 가 같은 단언을 한 번 더 갖고 있고, 그쪽까지 포함해 전수로 따지지는 않았다.
레인을 실제로 돌려 여덟 시험이 통과하는지 보지 않았다.
`getThreadsAwaitingConnection()` 이 포화 상태에서 어떤 값을 내는지 띄워서 재지 않았다.
<!-- body:end -->