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

149 lines
14 KiB
Markdown

---
kind: CASE
slug: analysis-finding-a06-f014
title: 인덱스 diff 는 열네 요소 중 keySignature 와 unique 만 비교한다
topic: multitenancy-isolation
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:analysis-finding-a06-f014
evidenceCapturedOn: 2026-09-04
body: case-analysis-finding-a06-f014.body.md
assets:
- key: analysis-finding-a06-f014
file: ../../../final/evidence/rendered/analysis-finding-a06-f014.svg
evidence:
- ../../../final/evidence/raw/analysis-finding-a06-f014.txt
source:
- 원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md §57 이다.
---
# 인덱스 diff 는 열네 요소 중 keySignature 와 unique 만 비교한다
`MongoIndexManifest:22`~`:35` 가 열네 요소를 선언하는데 `MongoIndexDescriptorView:15`~`:20` 은 여섯 컴포넌트만 갖는다. `MongoIndexDiffEngine.compare` 가 두 값을 함께 읽는 곳은 셋인데, `:49`~`:50` 은 양방향이고 `:55``hidden` 은 한 방향만 본다.
## 관계
- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다**
이 사례가 그 규칙의 형태다. 견주지 않은 요소가 달라져도 `MongoIndexDiff` 의 네 목록이 전부 빈다. `MongoIndexDiff:9`~`:11` 은 그 결과를 CI 산출물로 설계했다고 적는데, 지금 그것을 만드는 프로덕션 코드는 없다.
- **문서와 상수가 서로 일치하는 것으로는 아무것도 증명되지 않는다**
그 규칙이 요구하는 검사 경계 명시가 여기에 없다. 그 규칙 3 이 요구하는 검사 경계 명시가 여기에 없다. `MongoIndexDescriptorView:9` 가 축소 사실은 밝히지만, 그 결과 비교되지 않는 아홉 요소를 어디에도 적지 않는다.
- **TTL 규칙 셋을 가진 타입들을 부르는 프로덕션 코드가 없다**
그 기록의 `expireAfter` 가 이 diff 에서 견주어지지 않는 열 요소 중 하나다. TTL 보존 기간을 바꿔도 빈 보고서가 나온다.
## 문제
인덱스 드리프트 보고서는 선언한 인덱스와 서버에 있는 인덱스를 견주어 만든다. 매니페스트는 인덱스 하나에 대해 열네 가지를 선언한다.
그 열넷 중 무엇이 실제로 견주어지는지 확인했다.
## 결론
MongoIndexManifest:22~:35 의 요소는 열넷이고 MongoIndexDescriptorView:15~:20 은 여섯이다. 매니페스트 요소 가운데 뷰에 같은 이름이 있는 것은 name 과 unique 와 hidden 과 metadataOwnership 넷뿐이다.
MongoIndexDiffEngine.compare:41~:58 이 매니페스트와 뷰를 대조한다. 두 값을 한 줄에서 함께 읽는 곳은 셋인데, :49~:50 이 keySignature 와 unique 로 change 를 정하고 :55 가 declared.hidden() && !actual.hidden() 로 hide 를 정한다.
그 반대는 검사하지 않는다. 서버가 숨긴 인덱스를 매니페스트가 보인다고 선언한 경우인데, MongoIndexDiff:13~:14 가 적은 폐기·숨김·관측·승인 순서에서 숨김까지 간 인덱스를 다시 쓰기로 바꾸면 그 조합이 남는다. 그때 계획기는 그 인덱스를 쓰지 않는다.
뷰에 대응 필드가 없는 열 요소는 어느 방향으로도 비교되지 않는다. 그 열에서 keys 하나만 keySignature 로 요약돼 들어가고, 남는 아홉에 expireAfter 가 있다.
MongoIndexDiff:9~:11 은 이 결과가 CI 산출물이며 실행마다 순서가 바뀌면 이전 실행과 대조할 수 없어서 모든 목록을 정렬한다고 적는다. 네 목록이 모두 비면 render() 가 빈 문자열을 낸다.
다만 그 산출물을 지금 만드는 프로덕션 코드는 없다. 엔진 이름이 main 에 나오는 줄이 자기 클래스 선언 하나뿐이고, 부르는 것은 시험 넷이다.
MongoIndexDescriptorView:9~:12 는 그 축소 자체는 밝힌다. 무엇이 감지 불가가 되는지만 적지 않는다.
## 검증 환경
OpenJDK : 21.0.12
확인 방식 : 매니페스트와 뷰의 요소 전수 인용, 두 목록의 요소 계수와 자기검증, 요소마다 뷰에 있는지 이름으로 대조, 비교 엔진 본문 인용과 두 값을 함께 읽는 줄 계수와 전수, diff 레코드 전문 인용, 엔진의 main 등장 계수와 시험 호출 대조
소스 수정 : x
## 재현 조건
1. MongoIndexManifest 와 MongoIndexDescriptorView 의 레코드 헤더를 전문으로 싣는다.
2. 두 목록의 요소를 세고, 열넷이 아니면 스크립트가 실패하게 한다.
3. 매니페스트 요소마다 같은 이름이 뷰에 있는지 대조해 전부 나열한다.
4. MongoIndexDiffEngine.compare 를 인용하고 그 안에서 declared 와 actual 을 한 줄에서 함께 읽는 줄을 세고 전부 뽑는다.
5. MongoIndexDiff 의 클래스 자바독을 인용한다.
6. MongoIndexDiffEngine 이 main 에 나오는 줄을 세고, 그 엔진의 compare 를 부르는 시험 줄과 대조한다.
## 본문
<!-- body:start -->
`MongoIndexDiffEngine` 이 매니페스트와 서버에서 읽은 뷰를 대조해 드리프트 보고서를 만든다.
## 매니페스트가 선언하는 열넷
:::evidence key="analysis-finding-a06-f014" alt="저장소 루트에서 돌린 정적 검색 출력 192줄. 먼저 MongoIndexManifest 11~45번이 실린다. 12~19번 자바독은 이것이 diff 나 검토에 필요한 모든 것을 담은 선언된 인덱스 하나이고, expectedUsage 가 이 인덱스가 존재하는 연산을 이름 지어 이 인덱스가 아직 필요한지를 프로덕션 통계로 추측하지 않고 답할 수 있게 하며, metadataOwnership 이 누가 만들었는지를 적어 드리프트 정리가 선언하지 않은 암호화나 검색 인덱스를 지우자고 제안하지 못하게 한다고 적고, 키 순서가 표현상의 세부가 아니라 인덱스 정체성의 일부라 보존된다고 적는다. 21~35번의 record 헤더가 name 과 keys 와 unique 와 sparse 와 hidden 과 deprecated 와 partialFilterExpression 과 collationProfile 과 expireAfter 와 wildcardProjection 과 shardKeySupport 와 expectedUsage 와 owner 와 metadataOwnership 열넷을 선언한다. 이어서 MongoIndexDescriptorView 6~20번이 실린다. 7~12번 자바독은 이것이 서버에 실제로 있는 인덱스이며 D4 관리 클라이언트로 읽어 diff 가 견줄 수 있는 필드로 줄인 것이고, 소유권은 매니페스트를 믿는 대신 여기서 추론하는데 diff 의 목적 자체가 매니페스트가 모르는 인덱스를 찾는 것이며 그중 일부는 암호화나 검색에 속해 절대 삭제 후보가 되면 안 되기 때문이라고 적는다. 14~20번이 collection 과 name 과 keySignature 와 unique 와 hidden 과 metadataOwnership 여섯을 선언한다. 다음으로 MongoIndexDiffEngine 25~70번이 실린다. 41~58번 반복문이 선언된 인덱스마다 같은 이름의 뷰를 찾는데, 43~48번이 뷰가 없고 폐기 표시도 없으면 create 에 넣고, 49~50번이 keySignature 가 다르거나 unique 가 다르면 change 에 넣으며 51~52번 주석이 같은 이름에 다른 정의는 MongoDB 가 조용히 다시 만들지 않으므로 질의가 스캔을 시작할 때 발견되는 대신 재빌드가 계획돼야 한다고 적고, 55~57번이 선언은 숨김인데 서버는 아닐 때만 hide 에 넣는다. 60~67번은 매니페스트에 없는 서버 인덱스를 dropCandidates 에 넣는데 애플리케이션 드리프트로 삭제 가능한 소유권만 본다. 그 아래 한 줄에서 declared 값과 actual 값을 견주는 줄이 3 개라고 나오고 그 셋이 49번과 50번과 55번으로 실리며, 참고로 그 반복문에서 declared 나 actual 이 나오는 줄 전부도 함께 나온다. 이어서 매니페스트 요소가 14 개이고 뷰 요소가 6 개라고 나온 뒤, 매니페스트 요소마다 뷰에 있는지가 전부 나열되는데 name 과 unique 와 hidden 과 metadataOwnership 넷만 뷰에 있고 나머지 열은 뷰에 없다. 요소 수가 14 가 아니면 실패하는 자기검증도 함께 찍힌다. 마지막으로 MongoIndexDiff 1~49번이 실리는데 6~15번 자바독이 모든 목록이 정렬돼 있어 같은 입력에 대해 diff 가 바이트 단위로 같으며 실행마다 순서가 바뀌는 CI 산출물은 이전 실행과 견줄 수 없고 그것이 인덱스 드리프트 보고서의 목적 대부분이라고 적고, dropCandidates 는 이름 그대로 폐기와 숨김과 관측과 승인을 통과해야 무엇이든 삭제되는 제안일 뿐이라고 적으며, 31번의 empty 와 36~38번의 isClean 과 41~49번의 render 가 이어진다. 그 아래 MongoIndexDiffEngine 이 main 에 나오는 줄이 1 개인데 그것이 23번의 클래스 선언이고, 대조로 센 시험 쪽 engine.compare 호출은 MongoIndexDiffEngineTest 30·42·49·61번 네 줄이다." caption="매니페스트가 선언하는 열넷과 그 자바독 · 비교용 뷰가 나르는 여섯과 축소를 밝힌 자바독 · 비교 엔진 본문과 실제로 견주는 두 줄 · 요소마다 뷰에 있는지 대조한 전수와 자기검증 · CI 산출물이라 적은 diff 자바독과 호출 계수 — 192줄 · exit 0" zoom="true"
:::
`MongoIndexManifest:22`\~`:35` 가 인덱스 하나에 대해 열네 요소를 선언한다. `name`, `keys`, `unique`, `sparse`, `hidden`, `deprecated`, `partialFilterExpression`, `collationProfile`, `expireAfter`, `wildcardProjection`, `shardKeySupport`, `expectedUsage`, `owner`, `metadataOwnership` 이다.
`:14`\~`:17` 자바독은 그 열넷 가운데 둘을 따로 설명한다. `expectedUsage` 는 이 인덱스가 존재하는 연산을 이름 지어 "아직 필요한가" 를 프로덕션 통계로 추측하지 않고 답할 수 있게 하고, `metadataOwnership` 은 누가 만들었는지를 적어 드리프트 정리가 선언하지 않은 암호화나 검색 인덱스를 지우자고 제안하지 못하게 한다.
## 비교용 뷰가 선언하는 여섯 필드
`MongoIndexDescriptorView:15`\~`:20` 이 여섯 컴포넌트를 선언한다. `collection`, `name`, `keySignature`, `unique`, `hidden`, `metadataOwnership` 이다.
`:9`\~`:12` 자바독이 그 축소를 밝힌다. D4 관리 클라이언트로 읽어 diff 가 비교할 수 있는 필드로 줄였다는 것이다. 소유권을 매니페스트에서 받지 않고 여기서 추론하는 이유도 적는다 — diff 의 목적이 매니페스트가 모르는 인덱스를 찾는 것이고, 그중 일부는 암호화나 검색에 속해 삭제 후보가 되면 안 되기 때문이다.
그 축소로 어떤 요소가 비교 대상에서 빠지는지는 어디에도 적혀 있지 않다.
## compare 가 declared 와 actual 을 함께 읽는 세 줄
`MongoIndexDiffEngine.compare:41` 이 선언된 인덱스마다 같은 이름의 뷰를 찾는다.
`:43`\~`:48` 이 뷰가 없고 폐기 표시도 없으면 `create` 에 넣는다.
`:49`\~`:50``keySignature` 가 다르거나 `unique` 가 다르면 `change` 에 넣는다. `:51`\~`:52` 주석은 같은 이름에 다른 정의를 MongoDB 가 조용히 다시 만들지 않으므로, 질의가 스캔을 시작할 때 발견되는 대신 재빌드가 계획돼야 한다고 적는다.
`:55`\~`:57``declared.hidden() && !actual.hidden()` 일 때 `hide` 에 넣는다.
한 줄에서 선언 값과 서버 값을 함께 읽는 줄은 셋이다. `:49``:50``:55` 다. 앞의 둘이 `change` 를 결정하고 `:55``hide` 를 결정한다.
## 한 방향만 보는 검사
`:55` 의 조건은 선언이 숨김이고 서버가 아닌 경우다.
반대에 대한 분기가 없다. 서버에서는 숨겨져 있는데 매니페스트가 보인다고 선언한 상태다.
이 상태에서는 매니페스트가 `hidden` 을 거짓으로 선언한 인덱스를 서버가 숨겨 두고 있고, 질의 계획기는 숨겨진 인덱스를 쓰지 않는다. 보고서는 그것을 어느 목록에도 넣지 않는다.
## 뷰에 없는 열 요소
매니페스트 요소마다 뷰에 같은 이름이 있는지 대조했다. `name``unique``hidden``metadataOwnership` 넷만 있다. 뷰 요소가 여섯이 아니면 스크립트가 실패한다.
나머지 열은 없다. `keys`, `sparse`, `deprecated`, `partialFilterExpression`, `collationProfile`, `expireAfter`, `wildcardProjection`, `shardKeySupport`, `expectedUsage`, `owner` 다.
`keys``keySignature` 라는 다른 이름으로 요약돼 들어간다. 나머지 아홉에는 뷰에 대응하는 컴포넌트가 없다.
그 아홉에 `expireAfter` 가 있다. 30일을 1일로 바꾸는 것은 대량 삭제인데, 이 diff 는 그것을 차이로 보고하지 않는다.
## 네 목록이 모두 비었을 때의 산출물
`MongoIndexDiff:9`\~`:11` 은 모든 목록을 정렬해 같은 입력에 대해 바이트 단위로 같은 결과를 낸다고 적는다. 실행마다 순서가 바뀌는 CI 산출물은 이전 실행과 견줄 수 없고 그것이 드리프트 보고서의 목적 대부분이기 때문이다.
`:13`\~`:14``dropCandidates` 가 이름 그대로 제안일 뿐이며 폐기와 숨김과 관측과 승인을 통과해야 무엇이든 삭제된다고 적는다.
`:41`\~`:49``render()` 가 네 목록을 이어 붙인다. 모두 비면 결과가 빈 문자열이다. 비교하지 않는 아홉 요소가 달라져도 같은 결과다.
다만 지금 그 보고서를 만드는 프로덕션 코드는 없다. `MongoIndexDiffEngine` 이 main 에 나오는 줄이 `:23` 의 클래스 선언 하나뿐이고, 그 엔진의 `compare` 를 부르는 것은 `MongoIndexDiffEngineTest:30`·`:42`·`:49`·`:61` 네 줄이다.
## 원문과 갈리는 자리
원문은 비교되는 것이 `keySignature``unique` 둘이고 `hidden` 이 한 방향이라고 적는다. 그 판정은 그대로다. 다만 한 줄에서 두 값을 함께 읽는 줄로 세면 `:55` 를 포함해 셋이다.
여기에 더한 것은 둘이다. 하나는 뷰에 무엇이 없는지를 이름으로 전부 맞춰 본 결과다 — 매니페스트 열넷 가운데 뷰에 이름이 있는 것은 넷이고 `keys``keySignature` 로 형태를 바꿔 들어가므로 감지 불가가 되는 것은 아홉이다. 다른 하나는 그 엔진이 아직 프로덕션에서 불리지 않는다는 것이다.
## 확인하지 못한 것
선언과 서버 상태를 실제로 넣어 빈 보고서가 나오는 것을 프로브로 보이지 않았다.
이 보고서가 어느 CI 단계의 산출물이 되는지 워크플로 파일을 훑지 않았다.
`keySignature` 의 생성 규칙을 열어 보지 않았다. 키 순서가 그 서명에 담기는지 확인하지 않았다.
<!-- body:end -->