Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f034.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

14 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-f034 세 카드의 태그를 채우는 시험이 프로덕션 타입을 한 줄도 부르지 않는다 multitenancy-isolation clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:analysis-finding-a05-f034 2026-09-04 case-analysis-finding-a05-f034.body.md
key file
analysis-finding-a05-f034 ../../../final/evidence/rendered/analysis-finding-a05-f034.svg
../../../final/evidence/raw/analysis-finding-a05-f034.txt
원본 분석 절은 final/document.md#a05 §133 이다.

세 카드의 태그를 채우는 시험이 프로덕션 타입을 한 줄도 부르지 않는다

readiness-cards.yamlselected 카드 일곱 중 셋이 postgresqlIntegrationTest 의 시험을 시나리오로 지목하는데, 그 세 파일은 dev.caskeletonimport 하는 줄이 0 이다. 대조로 센 jpa-transaction-runtime 의 시험은 15 줄을 import 한다.

관계

  • 증거 등급과 provenance — R1과 R2를 가르는 것 그 등급의 조건은 결과가 어디서 왔는지를 보지 무엇을 지났는지를 보지 않는다. 시나리오가 지목한 시험이 프로덕션 코드를 부르는지는 R1 에서도 R2 에서도 검사되지 않는다.
  • 빠뜨림이 통과가 되는 게이트는 게이트가 아니다 여기서는 빠뜨림이 아니라 지목이 문제다. 태그마다 시나리오가 붙어 있어 레지스트리 검사는 통과하는데, 그 시나리오가 도는 것이 시험이 자기 @BeforeAll 로 만든 표다.
  • 후보 증거는 통과해도 R1에 머무르고 R2는 별도 게이트가 판정한다 그 결정이 후보 검증에 맡긴 것은 건너뛴 시험과 스키마와 내용 해시와 선행 조건이다. 시나리오가 프로덕션 타입을 한 줄도 부르지 않는 것은 그 넷 중 어디에도 걸리지 않는다.

문제

준비도 카드는 태그마다 그것을 덮는 시나리오나 task-claim 을 적고, 게이트가 그 selector 를 실제로 돌린다.

그 selector 가 무엇을 지나는지 확인했다.

결론

selected 일곱 모두에서 no-skip 이 비어 있는데, 이것은 결함이 아니라 설계다. build.gradle:1672~:1674 가 그 태그를 필수로 요구하면서 :1678~:1680 이 클레임 가능한 태그에서 빼고, jpa-evidence.gradle:680~:684 가 JUnit XML 의 실행·건너뜀·실패 건수로 그 태그를 채운다.

jpa-primary-foundation 의 base-card-manifests 도 생성기가 채운다. 그 카드는 readiness-task 자체가 나머지 여섯을 모으는 롤업이라 시나리오가 0 인 것이 정상이고, support-task 는 다섯 걸려 있다.

남는 것은 셋이다. jpa-aggregate-store 와 jpa-query-model 은 시나리오가 하나씩이고, jpa-observability-lifecycle 은 둘이다.

그 셋이 지목하는 세 파일은 dev.caskeleton 을 import 하는 줄이 0 이다. import 없이 쓰는 같은 패키지 타입까지 잡으려고 대문자 이름을 전부 뽑아 다시 셌는데 그것도 0 이다. 대조로 센 PostgreSqlTransactionIntegrationTest 는 두 방식 모두 15 다.

그 시험들이 읽고 쓰는 표는 자기 @BeforeAll 이 만든 것이다. readiness_aggregate 도 readiness_query 도 프로덕션 소스와 마이그레이션 4714 개 파일 어디에도 없다.

세 번째 카드의 observability 는 표가 아니라 문자열이다. 견주는 쪽과 견주어지는 쪽이 모두 PostgreSqlLifecycleIntegrationTest 안에 있고, 그 리터럴 postgresql-primary 는 프로덕션에 0 건이다.

검증 환경

OpenJDK : 21.0.12 확인 방식 : 카드 전체 계수와 selected 카드의 태그·시나리오·task-claim·support-task 계수, 레지스트리가 덮을 수 없는 두 태그의 근거를 빌드 스크립트와 생성기에서 인용, 세 카드의 시나리오 selector 나열, 그 세 시험의 본문 인용, import 계수와 소스 세트 파일 수 대조, 표 이름과 문자열의 프로덕션 등장 계수와 훑은 파일 수 대조, 시나리오 일곱 카드의 import 전수 소스 수정 : x

재현 조건

  1. 카드 전체를 상태별로 세고, selected 카드마다 필수 태그와 시나리오와 task-claim 과 support-task 를 센다.
  2. no-skip 과 base-card-manifests 가 레지스트리 밖에서 채워지는 자리를 빌드 스크립트와 생성기에서 인용한다.
  3. 남은 세 카드의 시나리오 selector 를 나열하고 그 시험 본문을 싣는다.
  4. 세 파일의 dev.caskeleton import 줄을 세고, 파일이 쓰는 대문자 이름 가운데 프로덕션에 정의된 타입이 몇인지도 센다.
  5. 두 표 이름과 postgresql-primary 가 프로덕션에 나오는지 세고 훑은 파일 수를 함께 센다.
  6. 시나리오 일곱을 가진 카드의 시험이 무엇을 import 하는지 나열한다.

본문

readiness-cards.yaml 은 카드마다 필수 증거 태그를 적고, 그 태그를 덮는 시나리오나 task-claim 을 함께 적는다.

태그 커버리지에서 두 태그는 빼고 읽어야 한다

:::evidence key="analysis-finding-a05-f034" alt="저장소 루트에서 돌린 준비도 레지스트리 분석과 정적 검색 출력 237줄. 먼저 카드가 열일곱이고 implemented-candidate 여섯, not-implemented 넷, selected 일곱이라고 나온다. 그 일곱마다 필수 태그 수와 시나리오 수와 task-claim 수와 support-task 수와 레지스트리가 덮지 않는 태그가 나열되는데, jpa-observability-lifecycle 은 태그 넷에 시나리오 둘, jpa-security-baseline 은 태그 여섯에 시나리오 셋과 task-claim 하나, jpa-flyway-migration 은 태그 넷에 시나리오 넷, jpa-transaction-runtime 은 태그 넷에 시나리오 일곱, jpa-aggregate-store 와 jpa-query-model 은 각각 태그 넷에 시나리오 하나, jpa-primary-foundation 은 태그 넷에 시나리오 0 과 task-claim 둘과 support-task 다섯이다. 덮지 않는 태그는 여섯 카드가 no-skip 하나씩이고 jpa-primary-foundation 만 base-card-manifests 가 더 있다. 이어서 그 두 태그를 레지스트리가 덮을 수 없는 근거가 실린다. build.gradle 16721674번이 no-skip 을 필수 태그에 넣으라고 강제하고 16781680번이 클레임 가능한 태그 집합에서 no-skip 을 빼며, jpa-evidence.gradle 676692번이 JUnit XML 의 실행 건수와 건너뜀과 실패와 오류 건수로 noSkipResult 를 계산해 no-skip 을 채우고, cardId 가 jpa-primary-foundation 이고 선행 조건이 모두 매니페스트를 가졌으면 base-card-manifests 를 채운다. 다음으로 남은 세 카드의 시나리오가 나열된다. jpa-aggregate-store 는 필수 태그 real-postgresql 과 mapping 과 optimistic-conflict 와 no-skip 에 시나리오 하나가 앞의 셋을 함께 덮고, jpa-query-model 도 keyset 과 query-plan 을 시나리오 하나가 함께 덮으며, jpa-observability-lifecycle 은 시나리오 둘이 각각 real-postgresql 과 lifecycle 을, observability 를 덮는다. 그 아래에 세 시험의 본문이 실린다. PostgreSqlAggregateIntegrationTest 는 1930번 BeforeAll 이 Docker 가용성을 확인하고 create table readiness_aggregate 를 실행하며, 3684번이 JDBC 로 UUID 와 Instant 를 넣고 다시 읽어 값을 견주고, 6684번이 커넥션 둘을 열어 version 을 조건에 넣은 갱신을 각각 실행해 먼저 커밋한 쪽이 1 이고 두 번째가 0 임을 확인한다. PostgreSqlQueryIntegrationTest 는 2235번이 readiness_query 표와 인덱스를 만들고 generate_series 로 천 행을 넣으며, 4580번이 keyset 페이지를 읽고 70번에서 set enable_seqscan=off 를 실행한 뒤 72번 explain format json 의 계획에 인덱스 이름이 들어 있는지 78번에서 확인한다. PostgreSqlLifecycleIntegrationTest 는 5166번이 크기 2 인 풀을 만들어 커넥션 둘을 쥐고 상태가 SATURATED 인지와 active 와 maximum 이 2 인지를 확인한 뒤 6365번이 boundedTags 를 component 가 postgresql-primary 이고 state 가 saturated 인 맵과 견주는데, 107119번의 private record 안 115117번이 바로 그 맵을 만드는 자리다. 다음으로 세 시험이 dev.caskeleton 을 import 하는 줄이 각각 0 개이고 같은 소스 세트에 파일이 스물일곱이며, 이름 단위로 훑어도 세 파일이 쓰는 대문자 이름 중 프로덕션에 정의된 타입이 각각 0 개인데 대조로 건 PostgreSqlTransactionIntegrationTest 는 15 개라고 나온다. 두 시험이 쓰는 표 이름 readiness_aggregate 와 readiness_query 가 프로덕션 소스와 마이그레이션에 0 건이고 그 검색이 훑은 main 파일이 4714 개이며, postgresql-primary 도 프로덕션에 0 건이다. 마지막으로 대조 카드 jpa-transaction-runtime 의 시나리오 일곱이 각각 어떤 태그를 덮는지 나열되고, 그 시험이 import 하는 dev.caskeleton 타입 열다섯 중 여덟이 실린다." caption="카드 열일곱과 selected 일곱의 태그 커버리지 · no-skip 과 base-card-manifests 를 레지스트리 밖에서 채우는 자리 · 남은 세 카드의 시나리오 · 그 세 시험의 본문 · import 와 이름 단위 계수 대조 · 표 이름과 문자열의 프로덕션 등장 0 · 시나리오 일곱 카드의 대조 — 237줄 · exit 0" zoom="true" :::

카드는 열일곱이고 selected 가 일곱이다. 일곱 모두 no-skip 이 시나리오로도 task-claim 으로도 덮이지 않는다.

그것이 설계다. build.gradle:1672~:1674 가 모든 카드에 no-skip 을 필수 태그로 넣으라고 강제하고, :1678~:1680 이 클레임 가능한 태그 집합을 만들 때 그 하나를 뺀다. 카드가 no-skip 을 덮겠다고 적으면 검증기가 거절한다.

대신 jpa-evidence.gradle:680~:684 가 매니페스트를 만들 때 JUnit XML 의 실행 건수와 건너뜀·실패·오류 건수를 읽어 noSkipResult 를 계산하고 그 태그를 채운다.

base-card-manifestsjpa-primary-foundation 에만 있는 태그이고, jpa-evidence.gradle:685~:691 이 그 카드의 선행 조건 여섯이 모두 매니페스트를 가졌을 때 채운다. readiness-taskverifyJpaPrimaryFoundationEvidence 이고 선행 조건이 나머지 여섯 base 카드 전부라, 이 카드는 자기 시나리오를 갖지 않는 쪽이 맞다.

남는 세 카드

jpa-aggregate-store 는 시나리오 하나가 real-postgresqlmappingoptimistic-conflict 를 함께 덮는다.

jpa-query-model 도 시나리오 하나가 real-postgresqlkeysetquery-plan 을 함께 덮는다.

jpa-observability-lifecycle 은 둘인데, 하나가 real-postgresqllifecycle 을, 다른 하나가 observability 를 덮는다.

그 시나리오들이 도는 것

PostgreSqlAggregateIntegrationTest@BeforeAll:19~:30assertDockerAvailable() 뒤에 create table readiness_aggregate 를 실행한다. :36~:64UUIDInstant 를 JDBC 로 넣고 다시 읽어 값을 견준다. :66~:84 는 커넥션 둘을 열어 where id = ? and version = ? 갱신을 각각 실행하고, 먼저 커밋한 쪽의 갱신 건수가 1 이고 두 번째가 0 인 것을 확인한 뒤 롤백한다.

optimistic-conflict 를 덮는 것이 그 부분이다. @Version 이 아니라 손으로 쓴 조건부 갱신이고, 대상은 이 시험이 만든 표다.

PostgreSqlQueryIntegrationTest:22~:35readiness_query 와 인덱스를 만들고 generate_series 로 천 행을 넣는다. :45~:68 이 keyset 페이지를 읽고, :70set enable_seqscan=off 를 실행한 뒤 :72explain (format json) 계획에 인덱스 이름이 들어 있는지를 :78 에서 확인한다.

query-plan 태그는 실제 EXPLAIN 계획을 본다. 다만 대안을 끈 뒤에 본 것이고, 대상은 역시 이 시험이 만든 표다.

PostgreSqlLifecycleIntegrationTest:51~:66 은 크기 2 인 풀에 커넥션 둘을 쥐고 상태와 activemaximum 을 확인한다. observability 를 덮는 것은 :63~:65boundedTags() 비교인데, 그 맵을 만드는 :115~:117 이 같은 파일 private record 안에 있고 리터럴도 같은 파일에 있다.

세 파일 모두 프로덕션 타입에 닿지 않는다

dev.caskeletonimport 하는 줄이 세 파일 다 0 이다. 같은 패키지 타입은 import 없이 쓸 수 있으므로 파일이 쓰는 대문자 이름을 전부 뽑아 프로덕션에 같은 이름의 .java 가 있는지도 셌는데, 그것도 셋 다 0 이다. 같은 소스 세트에 파일이 스물일곱 있다.

표 이름 둘과 boundedTags() 가 견주는 postgresql-primary 를 프로덕션 소스와 마이그레이션에서 찾으면 셋 다 0 건이다. 그 검색이 훑은 main 파일은 4714 개다.

시나리오 일곱을 가진 카드는 다르다

jpa-transaction-runtime 은 태그 넷에 시나리오 일곱이고, 하나가 여러 태그를 겸하는 대신 태그마다 여러 시나리오가 붙는다.

그 시나리오가 가리키는 PostgreSqlTransactionIntegrationTestdev.caskeleton 을 15 줄 import 한다. PersistenceExceptionTranslator, StandardSqlStateErrorMapping, PostgreSqlLocalTimeoutConfigurer, PostgreSqlSqlStateErrorMapping, JpaTransactionSettings, SpringTransactionPort 같은 것들이다. 이름 단위로 세도 15 다.

원문에 없는 것

원문은 이 세 카드의 시나리오가 프로덕션 경로를 지나지 않는다고 적는다. 여기에 더한 것은 그 판정을 남기기 위해 무엇을 빼야 하는지다.

no-skip 은 일곱 카드 모두에서 비어 있지만 레지스트리가 덮을 수 없는 태그이고, jpa-primary-foundation 의 시나리오 0 은 롤업 카드의 정상 상태다. 둘을 빼고 나면 남는 것이 셋이고, 그 셋에 대해서는 import 계수와 이름 단위 계수가 같은 답을 낸다.

확인하지 못한 것

매니페스트를 생성해 등급이 무엇으로 찍히는지 보지 않았다.

이름 단위 검사의 판정 기준은 프로덕션 트리에 같은 이름의 .java 가 있는지다. 시험 전용 타입과 이름이 겹치면 과대 계수될 수 있다.

verifyJpaCandidateEvidence 가 이 세 카드에 어떤 블로커를 붙이는지 빌드를 돌려 보지 않았다.

verifyJpaCandidateEvidence 가 이 세 카드에 어떤 블로커를 붙이는지 빌드를 돌려 보지 않았다.