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

16 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-f019 MongoChangeHistoryLostException 을 만드는 코드가 없다 multitenancy-isolation clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:analysis-finding-a06-f019 2026-09-04 case-analysis-finding-a06-f019.body.md
key file
analysis-finding-a06-f019 ../../../final/evidence/rendered/analysis-finding-a06-f019.svg
../../../final/evidence/raw/analysis-finding-a06-f019.txt
원본 분석 절은 final/document.md#a06 §69 이다.

MongoChangeHistoryLostException 을 만드는 코드가 없다

MongoChangeHistoryLostException 의 클래스 자바독은 이 예외가 왜 전용 타입인지를 적고, docs/mongodb/change-stream-guide.md:75 는 그것이 던져진다고 적는다. 그런데 자바 소스에서 그 이름이 나오는 두 줄은 둘 다 자기 선언이고, 히스토리 유실을 재현한 시험은 호출자가 받는 예외를 MongoQueryException 으로 단언한다.

관계

  • 타입이 문서화한 불변식은 타입이 강제한다 MongoChangeHistoryLostException:11~:14 가 복구를 업무 결정이라 부르며 전용 타입을 둔 이유를 적는데, 그 예외를 new 하는 줄이 main 에도 시험에도 없다.
  • 중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다 onFailure:53~:55 가 히스토리 유실을 자기 안에서 처리하므로 onHistoryLost:29 는 같은 결정을 만드는 두 번째 구현이다.
  • TTL 규칙 셋을 가진 타입들을 부르는 프로덕션 코드가 없다 두 기록 모두 자바독에 규칙을 적어 둔 타입이 선언만 있고, 그것을 만들거나 부르는 프로덕션 코드가 0 이다.

문제

이 패키지는 스트림 실패 뒤의 처리를 담고, 네 파일이 정책과 결정과 무효화 복구와 전용 예외를 나눠 맡는다.

그중 무엇이 프로덕션 호출자를 갖는지 확인했다.

결론

MongoChangeStreamRecoveryPolicy 는 실패 종류마다 진입점을 두어 넷이 있는데, 프로덕션에서 불리는 것은 onFailure:51 뿐이다. 나머지 셋 가운데 둘은 시험만 부르고 onInvalidate 는 아무도 부르지 않는다.

그 이유가 onFailure 안에 있다. :53~:55 가 히스토리 유실 서버 코드를 자기 안에서 걸러 onHistoryLost:31~:32 와 똑같은 결정을 만든다.

무효화 복구 쪽도 둘 중 하나만 불린다. checkpointFor 는 프로덕션 호출이 없고, 복구 패키지 밖 타입인 MongoChangeStreamState:32 의 autoResumable() 도 마찬가지다.

그 하나뿐인 호출이 자기 자신과 견준다. ReactiveMongoChangeStreamConsumer:119 가 체크포인트와 그 체크포인트의 위치를 두 인자로 넘기는데, MongoInvalidateRecovery:35 의 검사는 그 둘이 다른지를 본다.

MongoChangeHistoryLostException 은 어디에서도 만들어지지 않는다. 그 이름이 자바 소스에 나오는 줄이 둘뿐이고 둘 다 그 파일 자신의 선언이며, new 를 부르는 줄은 main 에도 시험에도 없다.

:11~:14 자바독이 그 타입이 왜 전용이어야 하는지 적는다. 남은 선택지가 투영 재생성과 원본 재조정 둘뿐이고 어느 쪽도 코드가 혼자 고를 수 없다는 것이다.

실제로는 상태만 HISTORY_LOST 가 되고 드라이버 예외가 그대로 나간다. ChangeStreamConsumerLifecycleTest:115 가 verifyError(MongoQueryException.class) 로 그것을 고정한다. 그래서 호출자는 예외 타입으로 이 상황을 구분할 수 없고 MongoChangeStreamState 를 확인해야 한다.

문서 셋이 그 반대를 적는다. docs/mongodb/change-stream-guide.md:71 이 히스토리 유실을 이 예외로 잇고 :75 가 그것이 던져진다고 적으며, docs/mongodb/runbooks/history-lost.md:23 이 증상 목록에 그 이름을 올린다.

검증 환경

OpenJDK : 21.0.12 확인 방식 : 복구 패키지의 파일 전수, 정책 전문 인용, 진입점마다 main 과 시험 호출 계수와 대조, 전용 예외의 자바독과 그 이름이 자바 소스와 문서에 나오는 줄 전수, onFailure 의 히스토리 유실 분기 인용, 소비자가 복구를 부르는 자리 전수, requireCorrectResumeOption 정의와 호출처 전수, 히스토리 유실 시 호출자가 받는 것을 고정한 시험 인용, 상태 열거형 자바독과 autoResumable 인용 소스 수정 : x

재현 조건

  1. 복구 패키지의 파일을 전부 나열하고 정책을 전문으로 싣는다.
  2. 진입점마다 main 과 시험에서 부르는 줄을 각각 센다.
  3. 전용 예외의 이름이 자바 소스에 몇 줄 나오는지 세고 전부 나열한 뒤, 같은 이름을 문서가 쓰는 자리도 함께 싣는다.
  4. 그 예외의 클래스 자바독을 인용한다.
  5. onFailure 의 히스토리 유실 분기와 소비자가 복구를 부르는 자리를 인용한다.
  6. requireCorrectResumeOption 의 검사와 그것을 부르는 자리를 전부 나열한다.
  7. 히스토리 유실 시 호출자가 받는 예외 타입을 고정한 시험을 인용한다.

본문

변경 스트림이 실패한 뒤의 처리를 이 패키지가 맡는다.

자바독이 나눈 세 실패와 진입점 넷

:::evidence key="analysis-finding-a06-f019" alt="저장소 루트에서 돌린 정적 검색 출력 208줄. 먼저 복구 패키지의 파일 넷이 나열되는데 MongoChangeHistoryLostException 과 MongoChangeStreamRecoveryDecision 과 MongoChangeStreamRecoveryPolicy 와 MongoInvalidateRecovery 다. 이어서 MongoChangeStreamRecoveryPolicy 766번 전문이 실린다. 813번 자바독은 실패를 셋으로 나누는데 재개 가능한 네트워크나 선출 실패는 저장된 토큰이 아직 유효하므로 자동으로 재개하고, 컬렉션이 삭제되거나 이름이 바뀐 무효화는 resumeAfter 가 아니라 startAfter 가 필요하며 드라이버가 그것을 암묵적으로 해 주지 않고, 유실된 히스토리는 사람이 필요하다고 적는다. 1721번이 두 런북 경로 상수이고, 2333번의 onHistoryLost 는 절대 자동 재개하지 않으며 현재 시각부터 다시 시작하면 알 수 없는 범위의 변경이 빠진 투영이 스스로를 건강하다고 보고하게 된다고 적는다. 3638번이 onResumableFailure, 4048번이 onInvalidate 인데 4344번 자바독이 저장하는 체크포인트가 startAfter 위치여야 한다고 적는다. 5061번의 onFailure 가 5355번에서 서버 코드가 히스토리 유실이면 HISTORY_LOST 로 멈추고, 5758번에서 재개 가능 라벨이면 재개하며, 60번에서 나머지를 FAILED 로 멈춘다. 6366번이 그 서버 코드를 286 과 280 으로 적는다. 다음으로 진입점마다 main 과 시험 호출 계수가 나오는데 onFailure 가 main 1 에 시험 3, onHistoryLost 가 main 0 에 시험 1, onResumableFailure 가 main 0 에 시험 1, onInvalidate 가 둘 다 0, requireCorrectResumeOption 이 main 1 에 시험 1, checkpointFor 가 main 0 에 시험 1, autoResumable 이 main 0 에 시험 3 이고, new MongoChangeHistoryLostException 은 main 과 시험 모두 0 이다. onInvalidate 라는 이름이 나오는 줄 전부가 2 개라는 대조가 먼저 나오고, 그 예외 이름이 자바 소스에 나오는 줄이 2 개인데 둘 다 MongoChangeHistoryLostException 자신의 16번 클래스 선언과 22번 생성자다. 이어서 같은 이름을 문서가 쓰는 자리 넷이 나오는데 docs/architecture/mongo-api-surface.txt 168번이 공개 표면 목록에 올리고, docs/mongodb/change-stream-guide.md 71번 표가 oplog 에 토큰이 없는 경우를 이 예외로 잇고 75번이 재개 토큰이 가장 오래된 oplog 항목보다 앞설 때 이 예외가 던져진다고 적으며, docs/mongodb/runbooks/history-lost.md 23번이 증상 목록에 그 이름을 올린다. 이어서 그 예외 829번이 실린다. 9번 자바독은 oplog 에 저장된 재개 위치가 더 이상 없다는 뜻이고, 1114번은 복구가 기술적 결정이 아니라 업무 결정이라 전용 타입이며, 플랫폼이 지금부터 재개할 수도 있고 드라이버가 그것을 쉽게 해 주지만 그러면 투영이 oplog 에서 떨어진 모든 변경을 조용히 놓치므로 정직한 선택지는 투영을 다시 만들거나 원본과 재조정하는 것뿐이고 둘 다 누군가가 골라야 한다고 적는다. 2228번 생성자의 메시지는 변경 스트림의 재개 위치가 더 이상 oplog 에 없어서 지금부터 다시 시작해 틈을 조용히 건너뛰는 대신 소비를 멈췄다는 것이다. 그 아래 onFailure 5061번이 다시 실리고, 소비자가 복구를 부르는 자리로 ReactiveMongoChangeStreamConsumer 119번과 214번이 나온다. 이어서 히스토리 유실 시 호출자가 받는 것을 고정한 시험 ChangeStreamConsumerLifecycleTest 108124번이 실리는데 112번이 서버 코드 286 오류를 흘리고 115번이 MongoQueryException 으로 끝나는 것을 단언하며 117119번이 상태가 HISTORY_LOST 이고 런북이 히스토리 유실 런북인지를, 120122번이 스트림을 한 번만 열었는지를 확인한다. 다음으로 MongoInvalidateRecovery 2445번이 실려 3132번 서명과 35번의 checkpoint.position 이 intended 와 다른지 보는 검사가 나오고, 그 메서드를 부르는 자리가 ReactiveMongoChangeStreamConsumer 119번과 정의와 시험 하나로 나열되는데 소비자 쪽은 checkpoint 와 checkpoint.position 을 두 인자로 넘긴다. 마지막으로 MongoChangeStreamState 135번이 실리는데 69번 자바독이 HISTORY_LOST 가 FAILED 와 분리된 이유를 적는다 — 플랫폼이 스스로 복구하기를 거부하는 유일한 상태이고, oplog 가 저장된 토큰을 지나쳐 굴러간 뒤 지금부터 재개하면 그 사이의 모든 변경을 조용히 버려서 투영이 건강해 보이면서 조용히 틀리게 되는데 그것이 누군가 봐야 하는 멈춘 소비자보다 나쁘다는 것이다." caption="복구 패키지 파일 넷과 정책 전문 · 진입점별 main·시험 호출 계수 · 예외 이름이 자바 소스에 두 줄이고 문서에 넷 · 그 예외가 적은 존재 이유 · onFailure 의 히스토리 유실 분기와 소비자가 부르는 두 자리 · 호출자가 받는 예외 타입을 고정한 시험 · 자기 자신과 견주는 검사 · 상태 열거형이 적은 구분 — 208줄 · exit 0" zoom="true" :::

MongoChangeStreamRecoveryPolicy:8~:13 자바독이 실패를 셋으로 나눈다. 재개 가능한 네트워크나 선출 실패는 저장된 토큰이 아직 유효하므로 자동 재개하고, 무효화는 startAfter 가 필요하며 드라이버가 암묵적으로 해 주지 않고, 유실된 히스토리는 사람이 필요하다.

진입점은 넷이다. onHistoryLost:29, onResumableFailure:36, onInvalidate:46, onFailure:51 이다.

호출자를 가진 것과 갖지 못한 것

main 에서 불리는 것은 onFailure 하나다. onHistoryLostonResumableFailure 는 main 0 에 시험 1 씩이고 onInvalidate 는 둘 다 0 이다.

MongoInvalidateRecovery 의 두 메서드도 호출자 유무가 다르다. requireCorrectResumeOption 은 main 1 이고 checkpointFor 는 0 이다. MongoChangeStreamState:32autoResumable() 도 main 0 에 시험 3 인데, 이쪽은 복구 패키지 밖 타입이다.

onHistoryLost 가 호출자를 갖지 못한 이유는 onFailure 안에 있다. :53~:55 가 서버 코드 286 이나 280 이면 HISTORY_LOST 로 멈추는 결정을 만든다. onHistoryLost:31~:32 가 만드는 것과 같은 결정이다.

전용 예외가 만들어지지 않는다

MongoChangeHistoryLostException 이라는 이름이 저장소 전체에 두 줄 나온다. :16 의 클래스 선언과 :22 의 생성자다.

new 를 부르는 줄이 main 도 시험도 0 이다.

:11~:14 자바독은 복구가 기술적 결정이 아니라 업무 결정이라 전용 타입을 두었다고 적는다. 플랫폼이 지금부터 재개할 수도 있고 드라이버가 그것을 쉽게 해 주지만, 그러면 투영이 oplog 에서 떨어진 모든 변경을 조용히 놓친다. 남은 선택지는 투영을 다시 만들거나 원본과 재조정하는 것뿐이고 둘 다 누군가가 골라야 한다.

:22~:28 의 생성자 메시지에도 재개 위치가 더 이상 oplog 에 없어서, 지금부터 다시 시작해 틈을 건너뛰는 대신 소비를 멈췄다고 적혀 있다.

실제로 나가는 것은 드라이버 예외다

ReactiveMongoChangeStreamConsumer:214recovery.onFailure(MongoDriverFailureView.from(driverFailure)) 로 결정을 받는다. 상태와 런북은 그 결정에서 온다.

예외는 바뀌지 않는다. ChangeStreamConsumerLifecycleTest:112 가 서버 코드 286 오류를 흘리고 :115verifyError(MongoQueryException.class) 로 끝나는 것을 단언한다. :117~:119 가 상태는 HISTORY_LOST 이고 런북은 히스토리 유실 런북인 것을 확인한다.

그래서 호출자가 catch (MongoChangeHistoryLostException) 으로 이 상황을 가르려 하면 잡히지 않는다. 상태를 물어보는 경로로만 알 수 있다.

MongoChangeStreamState:6~:9HISTORY_LOSTFAILED 와 분리된 이유를 적는다. 플랫폼이 스스로 복구하기를 거부하는 유일한 상태이고, 지금부터 재개하면 투영이 건강해 보이면서 조용히 틀리게 되는데 그것이 누군가 봐야 하는 멈춘 소비자보다 나쁘기 때문이다.

이 구분은 MongoChangeStreamState 에만 있고, 호출자가 받는 예외 타입에는 없다.

requireCorrectResumeOption 에 같은 값이 두 번 들어간다

MongoInvalidateRecovery:31~:32requireCorrectResumeOption 이 체크포인트와 의도한 위치를 받는다. :35checkpoint.position() != intended 면 던진다.

requireCorrectResumeOption 을 부르는 main 코드는 ReactiveMongoChangeStreamConsumer:119 하나인데, 인자가 checkpointcheckpoint.position() 이다. 같은 값을 두 번 넣으므로 그 조건이 참이 될 수 없다.

시험 쪽 MongoChangeStreamRecoveryPolicyTest:59 만 다른 값을 넘겨 던지는 것을 확인한다.

원문에 없는 것

원문은 호출 계수를 정리하면서 전용 예외가 어디에서도 만들어지지 않는 것을 가장 무겁게 봤다. 그 판정은 그대로다.

원문이 적지 않은 것은 문서 쪽이다. docs/mongodb/change-stream-guide.md:75 가 이 예외는 재개 토큰이 가장 오래된 oplog 항목보다 앞설 때 던져진다고 적고, :71 의 표가 히스토리 유실을 그 예외로 잇는다. docs/mongodb/runbooks/history-lost.md:23 은 그것을 증상 목록에 올린다. 세 자리가 코드에 없는 예외를 운영자에게 안내한다.

확인하지 못한 것

히스토리 유실을 실제로 일으켜 보지 않았다. 예외 타입 판단은 시험 단언에 기댄다.

MongoChangeStreamRecoveryDecisionhaltresume 말고 다른 상태를 만들 수 있는지 그 타입을 따로 열지 않았다.

docs/architecture/mongo-api-surface.txt:168 이 이 예외를 공개 표면에 올려 둔 것이 의도인지 판단하지 않았다.

서버 코드 286 을 직접 흘려 호출자가 받는 타입을 관측하지 않았다. 판단 근거는 시험이 단언하는 값이다.

호출자가 없는 다섯 진입점을 포크가 쓰도록 남겨 둔 것인지 설계 문서로 판단하지 않았다.