Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f021.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

12 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-a06-f021 드라이런만 감사 싱크를 직접 불러 번역을 거치지 않는다 multitenancy-isolation clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:analysis-finding-a06-f021 2026-09-04 case-analysis-finding-a06-f021.body.md
key file
analysis-finding-a06-f021 ../../../final/evidence/rendered/analysis-finding-a06-f021.svg
../../../final/evidence/raw/analysis-finding-a06-f021.txt
원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md §76 이다.

드라이런만 감사 싱크를 직접 불러 번역을 거치지 않는다

감사 레코드를 만드는 자리는 넷이다. :114·:117·:122MongoAdminGateway:127audit 헬퍼를 지나 싱크 실패를 MongoOperationRejectedException 으로 바꾸고, dryRun:147auditSink.accept 를 직접 부른다.

관계

  • 단일 admission point는 우회 경로를 세어야 성립한다 그 규칙 2 가 하위 계층 타입을 직접 참조하는 곳을 세라고 적는다. 감사 레코드를 만드는 네 자리 가운데 dryRun:147 하나가 auditSink 를 직접 부른다.
  • 도달성 판정은 단어가 아니라 import로 확인한다 dryRun 호출 0 이라는 판정을 같은 pathspec 과 같은 정규식 모양이 execute 에 대해 18 줄을 찾은 것과 대조해 확인했다.
  • 실패 번역 사슬의 순서는 계약이다 audit 헬퍼가 싱크의 RuntimeExceptionMongoOperationRejectedException 으로 바꾸는 단일 번역 지점인데, 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 를 부르는 줄을 각각 세어 대조한다.

본문

관리 게이트웨이는 감사 쓰기를 헬퍼 하나로 모은다.

감사 실패를 거부로 바꾸는 헬퍼

:::evidence key="analysis-finding-a06-f021" alt="저장소 루트에서 돌린 정적 검색 출력 145줄. 먼저 MongoAdminGateway 120137번이 실린다. 120121번 주석이 종결 레코드가 실패 전파 전에 쓰이고 그 세부가 예외 메시지가 아니라 예외 타입이라고 적고, 122123번이 실패 레코드를 남긴 뒤 다시 던진다. 127137번의 private audit 헬퍼가 128129번에서 auditSink.accept 를 try 로 감싸고 130136번에서 RuntimeException 을 잡아 MongoOperationRejectedException 으로 바꾸는데, 메시지가 관리 감사 싱크가 해당 단계 레코드를 거절했으며 감사할 수 없는 관리 작업은 실행되지 않는다는 것이다. 이어서 실행 경로 전체인 83126번이 실린다. 83번 서명이 명령과 승인과 본문 셋을 받고, 87번이 authorization.require 로 권한을 요구하며, 8893번이 만료된 명령을 거절하는데 오래된 명령은 더 이상 그것이 쓰인 때의 모습이 아닌 클러스터를 서술할 수 있다는 메시지다. 94번이 연산의 highRisk 를 보고 95102번이 승인이 null 이면 데이터를 파괴하거나 컬렉션을 다시 쓰는 연산은 이 명령에 묶인 승인 아래에서 돌거나 아예 돌지 않는다며 거절하고, 103번이 approval.require 를, 106110번이 단일 사용을 강제하며 104105번 주석이 그것이 없으면 한 reshard 에 대한 승인 하나가 같은 모양의 이후 모든 reshard 를 허가한다고 적는다. 113번 주석이 fail closed 라며 의도를 기록할 수 없으면 명령이 돌지 않는다고 적고, 114번이 의도 레코드를, 117번이 성공 레코드를, 122번이 실패 레코드를 헬퍼로 보낸다. 그 아래 auditSink.accept 를 부르는 줄이 2 개이고 audit 헬퍼를 부르는 줄이 3 개라고 나오며, auditSink 라는 이름이 나오는 자리가 26번 필드와 38번 생성자 인자와 42번 대입과 129번과 147번 다섯으로, 헬퍼를 부르는 자리가 114·117·122번 셋으로 나열된다. 다음으로 MongoAdminGateway 139149번의 dryRun 이 실린다. 140144번 자바독은 이것이 실행 없이 연산을 검증하고 의도를 기록하는 것이며, 드라이런이 모든 고위험 작업의 전제 조건이라 누군가 기억해서 넘기는 플래그가 아니라 일급 호출이어야 한다고 적는다. 145번 서명 뒤 146번이 권한을 요구하고 147148번이 auditSink.accept 를 직접 부른다. 헬퍼를 거치지 않는다. 마지막으로 MongoAdminAuditStateMachineTest 84113번이 실린다. 85번 DisplayName 이 실패하는 감사 싱크가 명령이 도는 것을 막는다고 적고, 86번의 anUnauditableCommandDoesNotRun 이 8894번에서 감사 컬렉션에 닿을 수 없다며 던지는 싱크로 게이트웨이를 만들고 95번에서 ran 플래그를 두며, 97107번이 COLL_MOD 일상 명령을 승인 null 로 execute 에 넘기고, 108109번이 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~:137auditauditSink.accept(record)try 로 감싼다.

:130RuntimeException 을 잡아 :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:86anUnauditableCommandDoesNotRun:88~:94 에서 던지는 싱크로 게이트웨이를 만들고, :108~:109 에서 MongoOperationRejectedExceptioncannot be audited 메시지를, :110~:112 에서 본문이 돌지 않았다는 것을 단언한다.

같은 파일에서 dryRun 을 다루는 줄은 0 이다.

dryRun 을 부르는 코드가 없다

그 메서드를 부르는 줄이 저장소 전체에 하나도 없다. 형제인 execute 는 18 줄이 부른다.

같은 pathspec 과 같은 정규식 모양이 한쪽에서 18 을 찾았으므로, 0 은 검색이 아무것도 훑지 못해 나온 값이 아니다. 그 정규식은 다른 게이트웨이에서도 8 줄을 잡는데 RedisRawGatewayContractTestPolicyAwareMongoNativeGatewayTest 의 것이라 이 계수에서 뺐다.

원문에 없는 것

원문은 두 감사 경로 중 하나만 fail-closed 이고 그 차이가 문서화돼 있지 않다고 적는다. 그 판정은 그대로다.

원문이 적지 않은 것은 executedryRun 이 감사 앞에서 지나는 검사의 수다. execute:87 의 권한과 :88 의 만료와 :94~:102 의 승인과 :106 의 단일 사용 넷을 지난 뒤에 :114 로 간다. dryRun:146 의 권한 하나만 지난다.

확인하지 못한 것

던지는 싱크를 넣고 dryRun 을 불러 어떤 예외가 나오는지 프로브로 재현하지 않았다.

포크가 드라이런을 어디서 부르도록 설계된 것인지 설계 문서로 판단하지 않았다.

dryRun 이 남기는 레코드를 승인 사슬이 어떻게 참조하는지 그 소비자를 찾지 않았다.

던지는 싱크를 넣고 dryRun 을 불러 어떤 예외가 나오는지 프로브로 재현하지 않았다.

dryRun 이 남기는 레코드를 승인 사슬이 어떻게 참조하는지 그 소비자를 찾지 않았다.