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

20 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-f004 전용 category 를 붙이는 유일한 분기가 도달할 수 없다 multitenancy-isolation clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:analysis-finding-a06-f004 2026-09-04 case-analysis-finding-a06-f004.body.md
key file
analysis-finding-a06-f004 ../../../final/evidence/rendered/analysis-finding-a06-f004.svg
../../../final/evidence/raw/analysis-finding-a06-f004.txt
원본 분석 절은 final/document.md#a06 §16 이다.

전용 category 를 붙이는 유일한 분기가 도달할 수 없다

MongoFailureCategory:68SCHEMA_VERSION_UNSUPPORTED 가 있고 MongoDataSchemaUnsupportedException:42~:53 이 세 버전을 공개 접근자로 노출한다. 그 값을 쓰는 자리는 저장소 전체에 열거형 선언과 DefaultMongoFailureTranslator:110case 둘뿐인데, 분류기가 그 값을 만들지 않으므로 그 case 는 돌지 않는다.

관계

  • 모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다 그 규칙은 한번 접힌 정보가 복구되지 않는다고 적는다. 여기서는 스키마 버전 거절이 OPERATION_REJECTED 로 접혀 다른 로컬 거절과 구별되지 않는다.
  • 위험한 조합은 정책이 아니라 생성자가 거부하게 만든다 그 규칙대로면 버전을 모르는 예외는 만들어질 수 없어야 한다. MongoDataSchemaUnsupportedException 의 생성자는 -1 셋을 그대로 받고, DefaultMongoFailureTranslator:111 이 그 값으로 예외를 만든다.
  • 중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다 같은 예외를 만드는 자리가 둘인데 조립 상태가 반대다. DefaultMongoFailureTranslatorMongoPlatformAutoConfiguration:93 이 빈으로 만들지만 그 case 가 실행되지 않고, MongoSchemaVersionPolicy 는 값을 아는 쪽인데 new 를 부르는 main 줄이 0 이다.

문제

이 실패에는 열거형 값 하나와 예외 타입 하나가 따로 마련돼 있다. 예외는 세 정수를 접근자로 내주므로 분기할 범주와 진단할 값을 함께 줄 수 있다.

그 어휘가 실제로 쓰이는지 확인했다.

결론

MongoFailureCategory:3~:9 는 이 값이 메트릭과 대시보드에 나타난다고 적는다. 그래서 알 수 없는 서버 코드는 새 범주를 만드는 대신 UNCLASSIFIED 로 간다.

new MongoDataSchemaUnsupportedException 을 부르는 main 자리는 둘이다.

MongoSchemaVersionPolicy:85 가 실제 버전 값 셋을 넘긴다. version.value() 와 range.minimumSupported().value() 와 range.current().value() 다.

그런데 그 예외가 들고 가는 context 는 :84 의 MongoFailureContext.rejected(...) 가 만든 것이다. MongoFailureContext:135~:148 이 그 안에 OPERATION_REJECTED 와 NOT_SENT 를 박는다. 전용 범주가 아니라 로컬 거절 범주다.

DefaultMongoFailureTranslator:111 이 전용 범주를 붙이는 유일한 자리인데 세 값 모두 -1 이다.

그런데 그 분기에 들어올 값이 없다. SCHEMA_VERSION_UNSUPPORTED 라는 이름이 저장소 전체에 두 줄만 있고 그 둘이 방금 인용한 선언과 case 이므로, 그 값을 컨텍스트에 넣는 코드가 존재하지 않는다. 같은 검색을 OPERATION_REJECTED 로 걸면 넷이 나온다.

분류기가 범주를 정하는 자리는 DefaultMongoFailureClassifier 안에 여럿이다. :88~:96 이 드라이버 라벨을 먼저 보고, :99 와 :106~:109 와 :112 가 단계와 결과에 따라 고르며, 마지막이 :156 의 CATEGORY_BY_CODE 조회다. 어느 자리에서도 이 범주가 나오지 않는다.

그래서 SCHEMA_VERSION_UNSUPPORTED 로 집계되는 실패가 생기지 않는다. 다른 범주로 새는 것도 아직 없다. new MongoSchemaVersionPolicy 를 부르는 main 줄이 0 이고 그 이름이 main 에 나오는 자리가 자기 선언 셋뿐이라, 이 리비전에서는 그 예외를 던지는 코드가 돌지 않는다. 시험만 여섯 번 만든다.

MongoSchemaVersionPolicyTest 가 그 어긋남을 잡지 못한다. 시험 여섯이 전부 던져진 타입이나 반환값만 보고, 범주와 세 접근자를 확인하는 줄이 하나도 없다.

그래서 이 결함은 채택자가 정책을 배선하는 순간에 드러난다. 전용 칸이 비어 있는 채로 거절만 다른 칸에 쌓이기 시작한다.

이 모듈에 살아 있는 다른 스키마 실패 경로는 세 번째 범주를 쓴다. MongoFailureContext.schemaMismatch:157 이 SCHEMA_VALIDATION 과 NO_WRITE_PERFORMED 를 박고, PolicyAwareMongoTypeMapper:112·:185·:192 가 그것을 부른다. 저장된 문서가 이 릴리스가 기대하는 스키마와 맞지 않을 때 실제로 도는 것이 그 경로다.

-1 을 채워 넣는 자리가 하나 더 있다. DefaultMongoFailureTranslator:106 의 MongoDocumentTooLargeException 인데, 이쪽은 서버 코드 10334 와 17420 이 그 범주로 매핑돼 있어 실제로 만들어진다. 접근자 계약에 값이 없을 수 있다는 말이 없는 것만 남는다.

검증 환경

OpenJDK : 21.0.12 확인 방식 : 범주 열거형의 클래스 자바독과 두 상수 인용, 전용 예외 전문 인용, 프로덕션 생성 지점 전수, 정책이 만드는 context 와 그 팩터리가 박는 값 인용, translator 의 switch 전문 인용, 전용 범주가 나오는 줄의 저장소 전체 계수와 대조 계수, 분류기가 범주를 정하는 자리 전부 인용, 같은 모듈의 다른 스키마 실패 팩터리와 그 호출자 인용, 두 생성 지점의 조립 계수와 대조, 정책 시험의 단언 전수와 접근자 단언 계수와 대조 소스 수정 : x

재현 조건

  1. 범주 열거형의 클래스 자바독과 이 값의 주석을 인용한다.
  2. 전용 예외의 자바독과 세 접근자를 인용한다.
  3. new MongoDataSchemaUnsupportedException 을 부르는 main 자리를 전부 찾는다.
  4. 정책이 만드는 context 와 그 팩터리 메서드 본문을 인용한다.
  5. translator 의 switch 를 전문으로 싣는다.
  6. 전용 범주가 저장소 전체에 몇 줄 나오는지 세고, 다른 범주 이름으로 같은 검색을 건다.
  7. 분류기가 범주를 정하는 자리를 전부 인용하고, 같은 모듈의 다른 스키마 실패 팩터리와 그 호출자를 함께 싣는다.
  8. 정책 시험의 단언을 전부 뽑고 접근자를 단언하는 줄을 센다.

본문

MongoFailureCategory 는 실패의 안정된 부모 범주다. 스키마 버전 실패를 위한 값이 그 안에 있다.

이 실패에 마련된 값과 타입

:::evidence key="analysis-finding-a06-f004" alt="저장소 루트에서 돌린 정적 검색 출력 288줄. 먼저 MongoFailureCategory 310번 클래스 자바독이 실리는데 서버 코드와 드라이버 오류 라벨은 릴리스마다 바뀌지만 이 범주는 안 바뀌며 메트릭과 대시보드에 나타나는 값이 이것이고, 그래서 알 수 없는 서버 코드가 런타임에 새 범주를 만드는 대신 UNCLASSIFIED 로 간다고 적는다. 6372번에 OPERATION_REJECTED 와 SCHEMA_VERSION_UNSUPPORTED 와 UNCLASSIFIED 세 상수가 각자 주석과 함께 나오는데, 68번의 주석은 저장된 문서의 스키마 버전이 지원 범위 밖이라는 것이다. 이어서 MongoDataSchemaUnsupportedException 511번 자바독이 실리는데 도메인 역직렬화 전에 올라오는 것이 이 예외의 가치이고 새 릴리스가 쓴 문서나 은퇴한 버전이 남긴 문서를 현재 모양으로 조용히 밀어 넣으면 안 되며 버전은 정수라 메시지에 이름을 적어도 안전하다고 적는다. 2239번 생성자가 세 정수를 요구하고 stored schema version 뒤에 문서 버전과 지원 범위를 이어 붙인 메시지를 만들며, 4154번이 documentVersion 과 minimumSupported 와 currentVersion 세 공개 접근자다. 다음으로 new MongoDataSchemaUnsupportedException 을 부르는 main 자리가 둘 나오는데 MongoSchemaVersionPolicy 85번과 DefaultMongoFailureTranslator 111번이다. 그 아래 MongoSchemaVersionPolicy 8287번이 실리는데 8384번이 MongoFailureContext.rejected 로 context 를 만들고 8586번이 version.value 와 range.minimumSupported().value 와 range.current().value 를 넘긴다. 이어서 MongoFailureContext 135149번의 rejected 팩터리가 실리는데 140번이 MongoFailureCategory.OPERATION_REJECTED 를, 141번이 MongoExecutionOutcome.NOT_SENT 를 박는다. 다음으로 DefaultMongoFailureTranslator 88118번이 실린다. 91번이 classification.category() 로 switch 를 열고 106번이 MongoDocumentTooLargeException 을 -1L 과 -1L 로, 110111번이 MongoDataSchemaUnsupportedException 을 -1 과 -1 과 -1 로 만들며, 112114번 주석이 벌크 부분 실패와 로컬 거절은 그 세부를 소유한 계층이 올리는 것이라 그 범주를 달고 번역까지 온 실패는 이 릴리스가 알아보지 못하는 것이라고 적고 115116번이 그 셋을 MongoUnclassifiedFailureException 으로 보낸다. 이어서 SCHEMA_VERSION_UNSUPPORTED 가 저장소 전체에 나오는 줄이 2 개라고 나오고 그 둘이 MongoFailureCategory 68번의 선언과 DefaultMongoFailureTranslator 110번의 case 로 나열되며, 대조로 센 OPERATION_REJECTED 는 4 개다. 그 아래 DefaultMongoFailureClassifier 2452번의 CATEGORY_BY_CODE 지도가 실리는데 서버 코드 11000 부터 31 까지 스물여섯 항목이 DUPLICATE_KEY 와 SCHEMA_VALIDATION 과 WRITE_CONFLICT 와 WRITE_CONCERN 과 READ_CONCERN 과 SERVER_SELECTION 과 CONNECTION 과 TIMEOUT 과 CURSOR 와 DOCUMENT_TOO_LARGE 와 SHARD_ROUTING 과 RESUME 과 ENCRYPTION 으로 가고 스키마 버전 항목이 없다. 이어서 같은 파일 86120번이 실리는데 8687번 주석이 라벨을 먼저 보는 이유가 서버 자신의 복구 가능성 진술이 어떤 코드 표보다 우선하기 때문이라고 적고, 8889번과 9192번이 커밋 미상과 일시적 트랜잭션 라벨을 각각 처리하며, 9496번이 쓰기 없음 라벨을, 99번이 categoryOf 를, 101109번이 아무것도 보내지 않은 단계에서 UNCLASSIFIED 를 SERVER_SELECTION 으로 바꾸는 것을, 111112번이 커밋 단계의 미상 결과를 처리한다. 150157번의 categoryOf 는 서버 코드가 없으면 재시도 가능 쓰기 라벨 여부로 CONNECTION 이나 UNCLASSIFIED 를 돌려주고 있으면 그 지도를 조회하며 없으면 UNCLASSIFIED 다. 그 아래 같은 정규식을 MongoFailureContextTest 에 걸면 1 개가 나온다는 대조가 있다. 다음으로 MongoFailureContext 151172번의 schemaMismatch 팩터리가 실린다. 152155번 자바독은 저장된 문서가 이 릴리스가 기대하는 스키마와 맞지 않는 경우이고 쓴 것이 없지만 읽기가 서버에 닿았으며 돌아온 것이 쓸 수 없는 값이라 결과가 NOT_SENT 가 아니라 NO_WRITE_PERFORMED 라고 적으며, 162번이 SCHEMA_VALIDATION 을 163번이 NO_WRITE_PERFORMED 를 박는다. 그 팩터리를 부르는 자리는 PolicyAwareMongoTypeMapper 112번과 185번과 192번이다. 이어서 new MongoSchemaVersionPolicy 를 부르는 main 줄이 0 개라고 나오고 그 타입이 main 에 나오는 자리가 MongoSchemaVersionPolicy 17번의 클래스 선언과 22번과 29번의 두 생성자 셋뿐이며, 대조로 센 시험 쪽 생성은 6 개다. DefaultMongoFailureTranslator 를 빈으로 만드는 main 줄은 2 개인데 MongoPlatformAutoConfiguration 93번과 DefaultMongoFailureTranslator 47번의 정적 팩터리다. 다음으로 MongoDocumentTooLargeException 26~36번이 실리는데 28번 자바독이 추정 직렬화 크기이며 내용이 아니라 크기라 로그에 남겨도 안전하다고만 적고, 33번은 초과된 상한이라고만 적는다. 마지막으로 MongoSchemaVersionPolicyTest 의 시험 여섯과 그 단언이 나오는데 21번과 31번과 45번이 isInstanceOf(MongoDataSchemaUnsupportedException.class) 이고 나머지는 resolveMissingVersion 과 writeVersion 과 requiresReadTimeConversion 과 음수 버전 거절을 본다. 그 시험이 category 나 세 버전 접근자를 단언하는 줄은 0 개다." caption="범주 열거형의 대시보드 자바독과 두 상수 · 전용 예외의 자바독과 세 접근자 · 프로덕션 생성 지점 둘 · 정책이 만드는 context 와 rejected 팩터리가 박는 값 · translator 의 switch 전문 · 그 범주가 저장소 전체에 두 줄뿐인 것과 분류기의 범주 지도 · 두 생성 지점의 조립 상태 · 같은 모양의 DocumentTooLarge · 정책 시험의 단언과 접근자 단언 0 — 288줄 · exit 0" zoom="true" :::

:3~:9 클래스 자바독이 이 열거형의 용도를 적는다. 릴리스마다 바뀌는 서버 코드와 드라이버 라벨 위에 얹는 고정된 값이고, 알 수 없는 서버 코드가 런타임에 새 범주를 만들어 메트릭 카디널리티를 터뜨리는 대신 UNCLASSIFIED 로 가는 이유가 그것이다.

:68SCHEMA_VERSION_UNSUPPORTED 는 저장된 문서의 스키마 버전이 지원 범위 밖이라는 뜻이다. 바로 위 :65OPERATION_REJECTED 는 플랫폼이 서버에 닿기 전에 로컬에서 거절했다는 뜻이다.

MongoDataSchemaUnsupportedException:5~:11 은 이 예외가 도메인 역직렬화 전에 올라오는 것이 핵심이라고 적는다. 버전은 정수이고 데이터를 담지 않아서 메시지에 이름을 적어도 안전하다는 것도 함께 있다.

:22~:39 의 생성자가 그 셋을 요구하고 메시지를 조립한다. :42·:47·:52documentVersion()minimumSupported()currentVersion() 이다. 분기할 범주와 진단할 값이 둘 다 준비돼 있다.

생성 지점 둘

new MongoDataSchemaUnsupportedException 을 부르는 main 자리는 MongoSchemaVersionPolicy:85DefaultMongoFailureTranslator:111 둘이다.

MongoSchemaVersionPolicy 가 넘기는 context 의 범주

MongoSchemaVersionPolicy:85~:86version.value()range.minimumSupported().value()range.current().value() 를 넘긴다. 진짜 값이다.

그런데 :83~:84 가 그 예외에 넣을 context 를 MongoFailureContext.rejected(new MongoOperationName("schema.version-check")) 로 만든다.

MongoFailureContext:135~:148 이 그 팩터리다. :140 이 범주를 OPERATION_REJECTED 로, :141 이 결과를 NOT_SENT 로 박는다. 인자로 받는 것은 연산 이름 하나뿐이라 다른 범주를 넣을 자리가 없다.

DefaultMongoFailureTranslator:111 이 넣는 -1, -1, -1

DefaultMongoFailureTranslator:91classification.category()switch 를 연다. :110~:111 이 그 범주를 받으면 MongoDataSchemaUnsupportedException-1, -1, -1 로 만든다.

여기서는 버전을 알 수 없다. 드라이버가 보고한 실패에서 문서의 스키마 버전을 읽을 방법이 없기 때문이다.

그 case 에 들어오는 값이 없다

SCHEMA_VERSION_UNSUPPORTED 가 저장소 전체에 나오는 줄은 둘이다. MongoFailureCategory:68 의 선언과 DefaultMongoFailureTranslator:110case 다. OPERATION_REJECTED 로 같은 검색을 걸면 넷이 나온다. 2 는 pathspec 이 아무것도 훑지 못해 나온 값이 아니다.

분류기가 범주를 정하는 자리는 하나가 아니다. :88~:89:91~:92 가 드라이버 라벨을 먼저 보고 커밋 미상이나 일시적 트랜잭션으로 보내고, :94~:96 이 쓰기 없음 라벨을 처리하며, :99categoryOf 를 부르고, :106~:109 가 아무것도 보내지 않은 단계에서 UNCLASSIFIEDSERVER_SELECTION 으로 바꾸고, :112 가 커밋 단계의 미상 결과를 처리한다.

마지막이 :150~:157categoryOf 다. 서버 코드가 없으면 재시도 가능 쓰기 라벨 여부로 CONNECTION 이나 UNCLASSIFIED 를 주고, 있으면 CATEGORY_BY_CODE 를 조회한다. 그 지도 :27~:52 의 스물여섯 항목이 열세 범주로 나뉘는데 스키마 버전이 없다.

어느 자리에서도 SCHEMA_VERSION_UNSUPPORTED 가 나오지 않는다. 그 이름이 저장소 전체에 두 줄뿐이라는 계수가 그것을 한 번에 말한다.

그래서 classification.category()SCHEMA_VERSION_UNSUPPORTED 인 경우가 생기지 않고, -1, -1, -1 예외는 만들어지지 않는다.

두 생성 지점의 조립 상태가 반대다

DefaultMongoFailureTranslator 는 빈이다. MongoPlatformAutoConfiguration:93 이 분류기를 넣어 만들고, DefaultMongoFailureTranslator:47 에 기본 분류기를 쓰는 정적 팩터리도 있다. 조립돼 있는데 문제의 분기만 죽었다.

MongoSchemaVersionPolicy 는 반대다. new MongoSchemaVersionPolicy 를 부르는 main 줄이 0 이고, 그 이름이 main 에 나오는 자리는 :17 의 클래스 선언과 :22·:29 의 두 생성자뿐이다. 같은 검색을 시험에 걸면 여섯이 나온다.

그래서 이 리비전에서는 세 버전을 아는 예외가 아예 던져지지 않는다. 대시보드의 SCHEMA_VERSION_UNSUPPORTED 칸이 비는 것은 맞지만, OPERATION_REJECTED 칸으로 새는 것도 아직 없다.

이 정책은 채택자가 쓰라고 공개된 타입이다. 배선하는 순간 스키마 버전 거절이 로컬 가드레일 거절과 같은 칸에 들어가고, 전용 칸은 계속 비어 있다.

MongoSchemaVersionPolicyTest:21·:31·:45isInstanceOf 로 예외 타입만 확인한다. 나머지 시험은 resolveMissingVersionwriteVersionrequiresReadTimeConversion 과 음수 버전 거절을 본다. category() 나 세 접근자가 나오는 줄이 하나도 없다.

같은 정규식을 MongoFailureContextTest 에 걸면 1 이 나온다. 0 은 그 정규식이 아무것도 못 잡아서 나온 값이 아니다.

살아 있는 스키마 실패는 세 번째 범주로 간다

MongoFailureContext:151~:171schemaMismatch 팩터리가 있다. :162SCHEMA_VALIDATION 을, :163NO_WRITE_PERFORMED 를 박는다. :154~:155 자바독이 그 결과를 고른 이유를 적는다 — 쓴 것이 없지만 읽기는 서버에 닿았고 돌아온 것이 쓸 수 없는 값이라는 것이다.

부르는 자리는 PolicyAwareMongoTypeMapper:112·:185·:192 셋이다. 타입 매퍼가 문서를 도메인 타입으로 옮기다 실패할 때 도는 경로다.

그래서 이 모듈의 스키마 관련 실패는 세 범주로 갈린다. 타입 매퍼가 쓰는 SCHEMA_VALIDATION, 조립되지 않은 정책이 쓰는 OPERATION_REJECTED, 그리고 아무도 만들지 않는 SCHEMA_VERSION_UNSUPPORTED 다.

-1 을 넣는 다른 예외 하나

DefaultMongoFailureTranslator:106MongoDocumentTooLargeException-1L, -1L 로 만든다.

MongoDocumentTooLargeException:28 은 그 값을 추정 직렬화 크기라고, :33 은 초과된 상한이라고 적는다. 값이 없을 수 있다는 말은 없다.

이쪽은 도달 가능하다. CATEGORY_BY_CODE:46~:47 이 서버 코드 10334 와 17420 을 DOCUMENT_TOO_LARGE 로 보낸다. 드라이버가 보고한 실패에서 두 수를 알 수 없는 것은 어쩔 수 없지만, 접근자 계약에 그 사실이 없다.

원문과 갈리는 자리

원문은 생성 지점이 둘이고 각각 반쪽만 맞다고 적으면서, 전용 범주 칸이 세 버전이 -1 인 실패만 받는다고 적는다.

확인 결과는 다르다. 분류기가 그 범주를 만들지 않으므로 -1 예외조차 생기지 않고, DefaultMongoFailureTranslator:110~:111 에는 값이 들어오지 않는다. 원문이 제안한 수정 — 정책이 전용 범주를 가진 context 를 만들면 translator 쪽 -1 경로를 정리할 수 있다 — 은 그대로 맞고, 정리 대상이 이미 실행되지 않는 case 라는 점만 다르다.

확인하지 못한 것

다른 분류기 구현이 조립될 수 있는지는 조립 경로로 따지지 않았다. 도달 불가라는 판정은 이름 계수 하나에 기대고 있다.

이 범주 이름을 소비하는 대시보드나 메트릭 설정은 저장소 밖에 있어 보지 않았다.

MongoSchemaVersionPolicy 가 던진 예외를 잡는 호출자가 세 접근자를 읽어 진단에 쓰는지 추적하지 않았다.