--- 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 를 부르는 줄을 각각 세어 대조한다. ## 본문 관리 게이트웨이는 감사 쓰기를 헬퍼 하나로 모은다. ## 감사 실패를 거부로 바꾸는 헬퍼 :::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` 이 남기는 레코드를 승인 사슬이 어떻게 참조하는지 그 소비자를 찾지 않았다.