- 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>
146 lines
16 KiB
Markdown
146 lines
16 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: analysis-finding-a06-f019
|
|
title: MongoChangeHistoryLostException 을 만드는 코드가 없다
|
|
topic: multitenancy-isolation
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:analysis-finding-a06-f019
|
|
evidenceCapturedOn: 2026-09-04
|
|
body: case-analysis-finding-a06-f019.body.md
|
|
assets:
|
|
- key: analysis-finding-a06-f019
|
|
file: ../../../final/evidence/rendered/analysis-finding-a06-f019.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/analysis-finding-a06-f019.txt
|
|
source:
|
|
- 원본 분석 절은 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. 히스토리 유실 시 호출자가 받는 예외 타입을 고정한 시험을 인용한다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
변경 스트림이 실패한 뒤의 처리를 이 패키지가 맡는다.
|
|
|
|
## 자바독이 나눈 세 실패와 진입점 넷
|
|
|
|
:::evidence key="analysis-finding-a06-f019" alt="저장소 루트에서 돌린 정적 검색 출력 208줄. 먼저 복구 패키지의 파일 넷이 나열되는데 MongoChangeHistoryLostException 과 MongoChangeStreamRecoveryDecision 과 MongoChangeStreamRecoveryPolicy 와 MongoInvalidateRecovery 다. 이어서 MongoChangeStreamRecoveryPolicy 7~66번 전문이 실린다. 8~13번 자바독은 실패를 셋으로 나누는데 재개 가능한 네트워크나 선출 실패는 저장된 토큰이 아직 유효하므로 자동으로 재개하고, 컬렉션이 삭제되거나 이름이 바뀐 무효화는 resumeAfter 가 아니라 startAfter 가 필요하며 드라이버가 그것을 암묵적으로 해 주지 않고, 유실된 히스토리는 사람이 필요하다고 적는다. 17~21번이 두 런북 경로 상수이고, 23~33번의 onHistoryLost 는 절대 자동 재개하지 않으며 현재 시각부터 다시 시작하면 알 수 없는 범위의 변경이 빠진 투영이 스스로를 건강하다고 보고하게 된다고 적는다. 36~38번이 onResumableFailure, 40~48번이 onInvalidate 인데 43~44번 자바독이 저장하는 체크포인트가 startAfter 위치여야 한다고 적는다. 50~61번의 onFailure 가 53~55번에서 서버 코드가 히스토리 유실이면 HISTORY_LOST 로 멈추고, 57~58번에서 재개 가능 라벨이면 재개하며, 60번에서 나머지를 FAILED 로 멈춘다. 63~66번이 그 서버 코드를 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번이 증상 목록에 그 이름을 올린다. 이어서 그 예외 8~29번이 실린다. 9번 자바독은 oplog 에 저장된 재개 위치가 더 이상 없다는 뜻이고, 11~14번은 복구가 기술적 결정이 아니라 업무 결정이라 전용 타입이며, 플랫폼이 지금부터 재개할 수도 있고 드라이버가 그것을 쉽게 해 주지만 그러면 투영이 oplog 에서 떨어진 모든 변경을 조용히 놓치므로 정직한 선택지는 투영을 다시 만들거나 원본과 재조정하는 것뿐이고 둘 다 누군가가 골라야 한다고 적는다. 22~28번 생성자의 메시지는 변경 스트림의 재개 위치가 더 이상 oplog 에 없어서 지금부터 다시 시작해 틈을 조용히 건너뛰는 대신 소비를 멈췄다는 것이다. 그 아래 onFailure 50~61번이 다시 실리고, 소비자가 복구를 부르는 자리로 ReactiveMongoChangeStreamConsumer 119번과 214번이 나온다. 이어서 히스토리 유실 시 호출자가 받는 것을 고정한 시험 ChangeStreamConsumerLifecycleTest 108~124번이 실리는데 112번이 서버 코드 286 오류를 흘리고 115번이 MongoQueryException 으로 끝나는 것을 단언하며 117~119번이 상태가 HISTORY_LOST 이고 런북이 히스토리 유실 런북인지를, 120~122번이 스트림을 한 번만 열었는지를 확인한다. 다음으로 MongoInvalidateRecovery 24~45번이 실려 31~32번 서명과 35번의 checkpoint.position 이 intended 와 다른지 보는 검사가 나오고, 그 메서드를 부르는 자리가 ReactiveMongoChangeStreamConsumer 119번과 정의와 시험 하나로 나열되는데 소비자 쪽은 checkpoint 와 checkpoint.position 을 두 인자로 넘긴다. 마지막으로 MongoChangeStreamState 1~35번이 실리는데 6~9번 자바독이 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` 하나다. `onHistoryLost` 와 `onResumableFailure` 는 main 0 에 시험 1 씩이고 `onInvalidate` 는 둘 다 0 이다.
|
|
|
|
`MongoInvalidateRecovery` 의 두 메서드도 호출자 유무가 다르다. `requireCorrectResumeOption` 은 main 1 이고 `checkpointFor` 는 0 이다. `MongoChangeStreamState:32` 의 `autoResumable()` 도 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:214` 가 `recovery.onFailure(MongoDriverFailureView.from(driverFailure))` 로 결정을 받는다. 상태와 런북은 그 결정에서 온다.
|
|
|
|
예외는 바뀌지 않는다. `ChangeStreamConsumerLifecycleTest:112` 가 서버 코드 286 오류를 흘리고 `:115` 가 `verifyError(MongoQueryException.class)` 로 끝나는 것을 단언한다. `:117`\~`:119` 가 상태는 `HISTORY_LOST` 이고 런북은 히스토리 유실 런북인 것을 확인한다.
|
|
|
|
그래서 호출자가 `catch (MongoChangeHistoryLostException)` 으로 이 상황을 가르려 하면 잡히지 않는다. 상태를 물어보는 경로로만 알 수 있다.
|
|
|
|
`MongoChangeStreamState:6`\~`:9` 는 `HISTORY_LOST` 가 `FAILED` 와 분리된 이유를 적는다. 플랫폼이 스스로 복구하기를 거부하는 유일한 상태이고, 지금부터 재개하면 투영이 건강해 보이면서 조용히 틀리게 되는데 그것이 누군가 봐야 하는 멈춘 소비자보다 나쁘기 때문이다.
|
|
|
|
이 구분은 `MongoChangeStreamState` 에만 있고, 호출자가 받는 예외 타입에는 없다.
|
|
|
|
## requireCorrectResumeOption 에 같은 값이 두 번 들어간다
|
|
|
|
`MongoInvalidateRecovery:31`\~`:32` 의 `requireCorrectResumeOption` 이 체크포인트와 의도한 위치를 받는다. `:35` 가 `checkpoint.position() != intended` 면 던진다.
|
|
|
|
`requireCorrectResumeOption` 을 부르는 main 코드는 `ReactiveMongoChangeStreamConsumer:119` 하나인데, 인자가 `checkpoint` 와 `checkpoint.position()` 이다. 같은 값을 두 번 넣으므로 그 조건이 참이 될 수 없다.
|
|
|
|
시험 쪽 `MongoChangeStreamRecoveryPolicyTest:59` 만 다른 값을 넘겨 던지는 것을 확인한다.
|
|
|
|
## 원문에 없는 것
|
|
|
|
원문은 호출 계수를 정리하면서 전용 예외가 어디에서도 만들어지지 않는 것을 가장 무겁게 봤다. 그 판정은 그대로다.
|
|
|
|
원문이 적지 않은 것은 문서 쪽이다. `docs/mongodb/change-stream-guide.md:75` 가 이 예외는 재개 토큰이 가장 오래된 oplog 항목보다 앞설 때 던져진다고 적고, `:71` 의 표가 히스토리 유실을 그 예외로 잇는다. `docs/mongodb/runbooks/history-lost.md:23` 은 그것을 증상 목록에 올린다. 세 자리가 코드에 없는 예외를 운영자에게 안내한다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
히스토리 유실을 실제로 일으켜 보지 않았다. 예외 타입 판단은 시험 단언에 기댄다.
|
|
|
|
`MongoChangeStreamRecoveryDecision` 이 `halt` 와 `resume` 말고 다른 상태를 만들 수 있는지 그 타입을 따로 열지 않았다.
|
|
|
|
`docs/architecture/mongo-api-surface.txt:168` 이 이 예외를 공개 표면에 올려 둔 것이 의도인지 판단하지 않았다.
|
|
|
|
서버 코드 286 을 직접 흘려 호출자가 받는 타입을 관측하지 않았다. 판단 근거는 시험이 단언하는 값이다.
|
|
|
|
호출자가 없는 다섯 진입점을 포크가 쓰도록 남겨 둔 것인지 설계 문서로 판단하지 않았다.
|
|
|
|
<!-- body:end -->
|