- 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>
15 KiB
kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | rootTreeNode | evidenceCapturedOn | body | assets | evidence | source | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | analysis-finding-a05-f032 | 포화된 풀의 대기 수를 단언하는 시험이 풀 계약 레인에 없다 | multitenancy-isolation | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:analysis-finding-a05-f032 | 2026-09-04 | case-analysis-finding-a05-f032.body.md |
|
|
|
포화된 풀의 대기 수를 단언하는 시험이 풀 계약 레인에 없다
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
재현 조건
- 워크플로의 단계 주석과 build.gradle 의 같은 문장, 그리고 릴리스 게이트 의존을 인용한다.
- 레인이 도는 소스 세트의 파일을 나열하고 세 파일의 시험과 단언을 전부 뽑는다.
- 레인 범위에서 pending() 을 단언하는 줄을 세고 그 시험 본문을 싣는다.
- 실제 풀을 띄우는 세 시험을 전문으로 싣는다.
- PoolMeasurement 를 어디서 import 하는지와 그 타입의 계산 메서드를 인용한다.
- 프로덕션에서 대기 수를 읽는 자리를 세고 훑은 파일 수를 함께 센다.
본문
야간 워크플로가 JPA 풀 계약 레인을 부른다. 그 단계의 주석이 레인이 무엇을 검사하는지 셋으로 적는다.
야간 워크플로가 적은 셋과 레인의 여덟 시험
:::evidence key="analysis-finding-a05-f032" alt="저장소 루트에서 돌린 정적 검색 출력 271줄. 먼저 jpa-nightly.yml 122134번 줄이 실린다. 단계 이름은 풀 포화와 REQUIRES_NEW 커넥션 동작을 검증한다는 것이고, 124126번 주석이 이 단계가 예전에는 명시적 프로퍼티로 단언을 끄고 그 결과를 인증이라 불렀으며 그래서 유일하게 단언된 임계값이 임계값을 단언하지 않는다는 것이었다고 적는다. 127129번이 지금 검사하는 셋을 적는데 REQUIRES_NEW 가 동시 스레드당 커넥션 둘을 요구한다는 것, 포화된 풀이 자기 대기 수를 보고한다는 것, 호출자가 커넥션 없이 진행하는 대신 기다린다는 것이며, 어느 러너에서나 참이라 끌 것이 없다고 적는다. 이어서 persistence-jpa/build.gradle 281310번이 실린다. 281286번 주석이 같은 셋을 다시 적고, 287296번의 jpaPlatformPoolContractTest 태스크가 jpaPlatformPerformanceTest 소스 세트를 돌리며, 300310번의 jpaPlatformReleaseGate 가 309번에서 그 태스크를 의존에 넣는다. 그 아래 소스 세트의 시험 파일 셋이 나열되는데 HikariPoolSaturationContractTest 와 PoolPressureContractTest 와 RequiresNewPoolPressureContractTest 다. 다음으로 세 파일의 시험 이름과 단언이 전부 나열되고 시험이 여덟이다. 이어서 레인 범위에서 measurement.pending 을 단언하는 줄이 1 개라고 나오고, 그 줄이 있는 시험이 실린다. PoolPressureContractTest 2647번인데 29번이 new PoolMeasurement(4, 2, 3, Duration.ofMillis(80)) 을 만들고 31번이 pending 이 3 인지, 32번이 지연이 80밀리초인지, 33번이 total 이 6 인지, 34번이 saturated 가 참인지 확인한다. 3747번의 두 번째 시험은 동시 스레드 8 과 깊이 1 로 required 를 8 곱하기 2 더하기 1 로 계산해 47번에서 17 과 같은지 확인한다. 그 아래 HikariPoolSaturationContractTest 32번의 POOL_SIZE 가 2 라는 것과 50118번의 세 시험이 실린다. 52번 시험은 POOL_SIZE 만큼 쥔 뒤 61번에서 한 번 더 요청해 SQLException 이 나는 것과 6466번에서 기다린 시간이 ACQUIRE_TIMEOUT 더하기 2초보다 작은 것을 확인한다. 77번 시험은 79번과 80번에서 둘을 얻고 81번에서 하나를 놓은 뒤 83번에서 세 번째를 얻어 유효한지 확인한다. 92번 시험은 94번에서 커넥션 하나만 쥐고 95101번이 HikariPoolMXBean 에서 활성 수와 유휴 수와 getThreadsAwaitingConnection 을 읽어 PoolMeasurement 를 만드는데, 103105번의 단언은 active 가 1 인지와 total 이 1 이상인지와 쥔 커넥션이 유효한지 셋이다. 110118번이 그 풀을 만드는 pool 메서드다. 다음으로 RequiresNewPoolPressureContractTest 4389번이 실린다. 45번 시험이 크기 1 짜리 풀에서 바깥 커넥션을 쥔 채 안쪽을 얻으려 하면 예외가 나는 것을, 61번 시험이 크기 2 에서는 얻어지고 두 커넥션이 다른 객체인 것을 확인하며, 80번 시험은 동시 스레드 1 과 깊이 1 로 required 를 계산해 88번에서 3 과 같은지 확인한다. 마지막으로 PoolMeasurement 를 import 하는 자리가 두 시험 파일이라는 것과 그 타입 634번이 실리는데, 911번 자바독이 대기 수와 획득 지연을 함께 기록하는 이유를 적으며 표본을 뜨는 순간에 대기 수가 0 으로 보이면서도 호출자들이 늘 기다리는 풀이 있을 수 있다고 적고, 2628번의 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() 이 포화 상태에서 어떤 값을 내는지 띄워서 재지 않았다.