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

139 lines
12 KiB
Markdown

---
kind: CASE
slug: analysis-finding-a06-f021
title: 드라이런만 감사 싱크를 직접 불러 번역을 거치지 않는다
topic: multitenancy-isolation
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:analysis-finding-a06-f021
evidenceCapturedOn: 2026-09-04
body: case-analysis-finding-a06-f021.body.md
assets:
- key: analysis-finding-a06-f021
file: ../../../final/evidence/rendered/analysis-finding-a06-f021.svg
evidence:
- ../../../final/evidence/raw/analysis-finding-a06-f021.txt
source:
- 원본 분석 절은 final/document.md#a06 §76 이다.
---
# 드라이런만 감사 싱크를 직접 불러 번역을 거치지 않는다
감사 레코드를 만드는 자리는 넷이다. `:114`·`:117`·`:122``MongoAdminGateway:127``audit` 헬퍼를 지나 싱크 실패를 `MongoOperationRejectedException` 으로 바꾸고, `dryRun:147``auditSink.accept` 를 직접 부른다.
## 관계
- **단일 admission point는 우회 경로를 세어야 성립한다**
그 규칙 2 가 하위 계층 타입을 직접 참조하는 곳을 세라고 적는다. 감사 레코드를 만드는 네 자리 가운데 `dryRun:147` 하나가 `auditSink` 를 직접 부른다.
- **도달성 판정은 단어가 아니라 import로 확인한다**
`dryRun` 호출 0 이라는 판정을 같은 pathspec 과 같은 정규식 모양이 `execute` 에 대해 18 줄을 찾은 것과 대조해 확인했다.
- **실패 번역 사슬의 순서는 계약이다**
`audit` 헬퍼가 싱크의 `RuntimeException``MongoOperationRejectedException` 으로 바꾸는 단일 번역 지점인데, `dryRun:147` 이 그것을 지나지 않아 싱크가 던진 예외가 그대로 호출자에게 간다.
## 문제
관리 게이트웨이는 모든 감사 쓰기를 헬퍼로 보내고, 그 헬퍼가 싱크 실패를 플랫폼 거부 예외로 바꾼다.
그 헬퍼를 지나지 않는 감사 쓰기가 있는지 확인했다.
## 결론
audit:127~:137 이 싱크 호출을 try 로 감싸고 RuntimeException 을 플랫폼 거부 예외로 바꾼다. 거절된 레코드의 단계 이름이 메시지에 들어간다.
실행 경로의 세 기록이 그 헬퍼를 지난다. :114 의 의도와 :117 의 성공과 :122 의 실패다. :113 이 그 순서를 fail closed 라고 부른다.
싱크를 직접 부르는 줄은 헬퍼 안의 :129 와 드라이런 안의 :147 둘이고, 뒤쪽만 번역을 거치지 않는다.
dryRun:145~:149 는 권한 검사 뒤에 싱크를 바로 부른다. 그래서 싱크가 던진 예외가 그대로 호출자에게 도달한다.
:142~:143 자바독은 드라이런을 고위험 작업의 전제 조건으로 놓는다. 자바독대로면 고위험 명령은 감사가 강제되지 않는 호출을 먼저 지난다.
시험은 execute 쪽만 고정한다. MongoAdminAuditStateMachineTest:86 이 던지는 싱크로 거부 예외와 본문 미실행을 함께 단언하는데, 같은 파일에 dryRun 을 거는 줄이 0 이다.
dryRun 을 부르는 줄이 저장소 전체에 0 이다. 같은 게이트웨이의 execute 는 18 줄이 부르고 그중 일곱이 main 이다.
## 검증 환경
OpenJDK : 21.0.12
확인 방식 : 감사 헬퍼와 그 번역 인용, 실행 경로가 헬퍼를 부르는 세 자리 인용, auditSink 이름이 나오는 자리 전수와 헬퍼 호출 계수, dryRun 본문과 그 자바독 인용, 감사 실패를 고정하는 시험 인용과 그 파일의 dryRun 계수, 두 진입점의 호출 계수와 다른 게이트웨이 오염분 대조
소스 수정 : x
## 재현 조건
1. audit 헬퍼와 그것이 던지는 예외를 인용한다.
2. 실행 경로가 그 헬퍼를 부르는 세 자리를 인용한다.
3. auditSink 라는 이름이 나오는 자리와 audit 헬퍼를 부르는 자리를 각각 전부 찾는다.
4. dryRun 본문과 그 자바독을 인용한다.
5. 감사 실패를 고정하는 시험을 인용하고, 같은 파일에서 dryRun 이 나오는 줄을 센다.
6. dryRun 과 execute 를 부르는 줄을 각각 세어 대조한다.
## 본문
<!-- body:start -->
관리 게이트웨이는 감사 쓰기를 헬퍼 하나로 모은다.
## 감사 실패를 거부로 바꾸는 헬퍼
:::evidence key="analysis-finding-a06-f021" alt="저장소 루트에서 돌린 정적 검색 출력 145줄. 먼저 MongoAdminGateway 120~137번이 실린다. 120~121번 주석이 종결 레코드가 실패 전파 전에 쓰이고 그 세부가 예외 메시지가 아니라 예외 타입이라고 적고, 122~123번이 실패 레코드를 남긴 뒤 다시 던진다. 127~137번의 private audit 헬퍼가 128~129번에서 auditSink.accept 를 try 로 감싸고 130~136번에서 RuntimeException 을 잡아 MongoOperationRejectedException 으로 바꾸는데, 메시지가 관리 감사 싱크가 해당 단계 레코드를 거절했으며 감사할 수 없는 관리 작업은 실행되지 않는다는 것이다. 이어서 실행 경로 전체인 83~126번이 실린다. 83번 서명이 명령과 승인과 본문 셋을 받고, 87번이 authorization.require 로 권한을 요구하며, 88~93번이 만료된 명령을 거절하는데 오래된 명령은 더 이상 그것이 쓰인 때의 모습이 아닌 클러스터를 서술할 수 있다는 메시지다. 94번이 연산의 highRisk 를 보고 95~102번이 승인이 null 이면 데이터를 파괴하거나 컬렉션을 다시 쓰는 연산은 이 명령에 묶인 승인 아래에서 돌거나 아예 돌지 않는다며 거절하고, 103번이 approval.require 를, 106~110번이 단일 사용을 강제하며 104~105번 주석이 그것이 없으면 한 reshard 에 대한 승인 하나가 같은 모양의 이후 모든 reshard 를 허가한다고 적는다. 113번 주석이 fail closed 라며 의도를 기록할 수 없으면 명령이 돌지 않는다고 적고, 114번이 의도 레코드를, 117번이 성공 레코드를, 122번이 실패 레코드를 헬퍼로 보낸다. 그 아래 auditSink.accept 를 부르는 줄이 2 개이고 audit 헬퍼를 부르는 줄이 3 개라고 나오며, auditSink 라는 이름이 나오는 자리가 26번 필드와 38번 생성자 인자와 42번 대입과 129번과 147번 다섯으로, 헬퍼를 부르는 자리가 114·117·122번 셋으로 나열된다. 다음으로 MongoAdminGateway 139~149번의 dryRun 이 실린다. 140~144번 자바독은 이것이 실행 없이 연산을 검증하고 의도를 기록하는 것이며, 드라이런이 모든 고위험 작업의 전제 조건이라 누군가 기억해서 넘기는 플래그가 아니라 일급 호출이어야 한다고 적는다. 145번 서명 뒤 146번이 권한을 요구하고 147~148번이 auditSink.accept 를 직접 부른다. 헬퍼를 거치지 않는다. 마지막으로 MongoAdminAuditStateMachineTest 84~113번이 실린다. 85번 DisplayName 이 실패하는 감사 싱크가 명령이 도는 것을 막는다고 적고, 86번의 anUnauditableCommandDoesNotRun 이 88~94번에서 감사 컬렉션에 닿을 수 없다며 던지는 싱크로 게이트웨이를 만들고 95번에서 ran 플래그를 두며, 97~107번이 COLL_MOD 일상 명령을 승인 null 로 execute 에 넘기고, 108~109번이 MongoOperationRejectedException 과 cannot be audited 메시지를, 110~112번이 나중에 아무도 재구성할 수 없는 관리 작업은 일어나면 안 된다는 설명과 함께 본문이 돌지 않았다는 것을 단언한다. 그 아래 드라이런의 감사 실패를 보는 시험 줄이 0 개이고, 이 게이트웨이의 dryRun 을 부르는 줄이 저장소 전체에 0 개이며, 대조로 센 같은 게이트웨이의 execute 는 18 개이고 그 호출 자리가 main 일곱과 test 열하나로 나뉘어 전부 나열된 뒤, 같은 정규식이 다른 게이트웨이에서 잡는 줄이 8 개라고 참고로 붙는다." caption="감사 실패를 플랫폼 예외로 바꾸는 헬퍼 · execute 가 감사 앞에서 지나는 네 검사와 세 기록이 헬퍼를 지나는 자리 · auditSink 를 부르는 자리 전수 · 헬퍼를 거치지 않는 dryRun 과 그 자바독 · 감사 실패를 고정하는 시험의 단언 셋과 dryRun 계수 0 · 두 진입점의 호출 대조 — 145줄 · exit 0" zoom="true"
:::
`MongoAdminGateway:127`\~`:137``audit``auditSink.accept(record)``try` 로 감싼다.
`:130``RuntimeException` 을 잡아 `:131`\~`:135` 에서 `MongoOperationRejectedException` 으로 바꾼다. 메시지는 감사 싱크가 그 단계 레코드를 거절했고, 감사할 수 없는 관리 작업은 실행되지 않는다는 것이다.
## 실행 경로의 세 기록
`:114` 가 의도 레코드를 헬퍼로 보낸다. `:113` 주석에는 fail closed 라고, 의도를 기록할 수 없으면 명령이 돌지 않는다고 적혀 있다.
`:117` 이 성공 레코드를, `:122` 가 실패 레코드를 보낸다. `:120`\~`:121` 주석은 종결 레코드가 실패 전파 전에 쓰이고 그 세부가 예외 메시지가 아니라 예외 타입이라고 적는다.
셋 다 헬퍼를 지난다. 싱크가 던지면 셋 다 플랫폼 예외가 된다.
## audit 헬퍼를 지나지 않는 dryRun\:147
`auditSink.accept` 를 부르는 줄은 둘이다. `:129` 가 헬퍼 안이고 `:147` 이 드라이런 안이다. `audit` 헬퍼를 부르는 줄은 셋이고 전부 실행 경로에 있다.
`dryRun:145`\~`:149``:146` 에서 권한을 요구한 뒤 `:147`\~`:148` 에서 `auditSink.accept(...)` 를 바로 부른다.
싱크가 던지면 그 예외가 번역 없이 호출자에게 간다. `MongoOperationRejectedException` 이 아니라 싱크가 던진 것 그대로다.
## 자바독이 적는 드라이런의 지위
`:142`\~`:143` 자바독에는 드라이런이 모든 고위험 작업의 전제 조건이라서, 누군가 기억해서 넘기는 플래그가 아니라 일급 호출이어야 한다고 적혀 있다.
자바독대로라면 고위험 작업은 드라이런이 먼저 돌아야 하는데, 그 드라이런이 남기는 레코드는 싱크가 거절해도 명령을 멈추지 않는다. `execute` 가 남기는 세 레코드와 다른 점이 이것이다.
## 시험이 거는 것은 execute 뿐이다
`MongoAdminAuditStateMachineTest:86``anUnauditableCommandDoesNotRun``:88`\~`:94` 에서 던지는 싱크로 게이트웨이를 만들고, `:108`\~`:109` 에서 `MongoOperationRejectedException``cannot be audited` 메시지를, `:110`\~`:112` 에서 본문이 돌지 않았다는 것을 단언한다.
같은 파일에서 `dryRun` 을 다루는 줄은 0 이다.
## dryRun 을 부르는 코드가 없다
그 메서드를 부르는 줄이 저장소 전체에 하나도 없다. 형제인 `execute` 는 18 줄이 부른다.
같은 pathspec 과 같은 정규식 모양이 한쪽에서 18 을 찾았으므로, 0 은 검색이 아무것도 훑지 못해 나온 값이 아니다. 그 정규식은 다른 게이트웨이에서도 8 줄을 잡는데 `RedisRawGatewayContractTest``PolicyAwareMongoNativeGatewayTest` 의 것이라 이 계수에서 뺐다.
## 원문에 없는 것
원문은 두 감사 경로 중 하나만 fail-closed 이고 그 차이가 문서화돼 있지 않다고 적는다. 그 판정은 그대로다.
원문이 적지 않은 것은 `execute``dryRun` 이 감사 앞에서 지나는 검사의 수다. `execute``:87` 의 권한과 `:88` 의 만료와 `:94`\~`:102` 의 승인과 `:106` 의 단일 사용 넷을 지난 뒤에 `:114` 로 간다. `dryRun``:146` 의 권한 하나만 지난다.
## 확인하지 못한 것
던지는 싱크를 넣고 `dryRun` 을 불러 어떤 예외가 나오는지 프로브로 재현하지 않았다.
포크가 드라이런을 어디서 부르도록 설계된 것인지 설계 문서로 판단하지 않았다.
`dryRun` 이 남기는 레코드를 승인 사슬이 어떻게 참조하는지 그 소비자를 찾지 않았다.
던지는 싱크를 넣고 `dryRun` 을 불러 어떤 예외가 나오는지 프로브로 재현하지 않았다.
`dryRun` 이 남기는 레코드를 승인 사슬이 어떻게 참조하는지 그 소비자를 찾지 않았다.
<!-- body:end -->