752 lines
131 KiB
Markdown
752 lines
131 KiB
Markdown
---
|
|
title: branch / feature-redis-runtime-role-isolation-contract
|
|
source_type: branch-note
|
|
status: raw
|
|
branch: feature-redis-runtime-role-isolation-contract
|
|
parent_branch:
|
|
related_projects: [ca-skeleton]
|
|
governing_docs: [raw/project-notes/ca-skeleton-operational-contract]
|
|
tags: [branch, ca-skeleton, redis, runtime, role-isolation, codec]
|
|
created: 2026-07-28
|
|
target_merge:
|
|
status_label: in-progress
|
|
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-061
|
|
kind: project-work-item
|
|
project: ca-skeleton-operational-contract
|
|
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-061
|
|
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-REDIS-ROLE-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-OPTIONAL-ADAPTER-001@1]
|
|
refines: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-REDIS-ROLE-001@1]
|
|
overrides: []
|
|
depends_on: [WI-CA-SKELETON-OPERATIONAL-CONTRACT-060]
|
|
imports: []
|
|
delegates: []
|
|
accepts_delegations: []
|
|
contract_packet: 1
|
|
---
|
|
|
|
# branch: feature-redis-runtime-role-isolation-contract
|
|
|
|
> Layer: `raw/branch-notes/` — 단일 브랜치의 **TODO·결정·진행 기록**. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출. 원본은 raw에 영구 보관.
|
|
> `status_label`: `in-progress` | `review` | `merged` | `abandoned`
|
|
> **스캐폴딩 상태** — 결정(D-row)·구현 가이드는 비어 있다. `/branch-spec feature-redis-runtime-role-isolation-contract` 로 채운다.
|
|
|
|
<!-- section-id: branch-parent -->
|
|
## 부모 (필수)
|
|
|
|
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
|
|
|
|
> 분해 근거: `docs/superpowers/specs/2026-07-28-ca-skeleton-production-capability-feature-decomposition-design.md` §4.2 — 기술 런타임 (Tier T). 본 branch 는 project §8.0 `WI-CA-SKELETON-OPERATIONAL-CONTRACT-061` 의 실행 단위다.
|
|
|
|
형제 branch (같은 부모의 다른 자식 — 인접 영역):
|
|
|
|
- [[raw/branch-notes/feature-redis-atomic-program-catalog-contract]]
|
|
- [[raw/branch-notes/feature-cache-strategy-registry-contract]]
|
|
- [[raw/branch-notes/feature-cachestore-multi-backend-router]]
|
|
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
> project Work Item 에서 내려온 실행 계약의 snapshot. 여기에는 **pinned pointer + 1줄 요약 + branch 적용점**만 쓰고 상세를 복제하지 않는다.
|
|
|
|
- **생성 시 프로젝트 개정**: `1`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: 3-role 연결·키·코덱·health 계약 test 와 incompatible co-location 거부 test 가 통과한다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| Decision Ref | Project Summary | Branch Application | Source |
|
|
|---|---|---|---|
|
|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-REDIS-ROLE-001@1` | Redis는 cache·coordination·session 3개 role로 분리하고 key prefix나 DB number를 격리 수단으로 쓰지 않는다 | `/branch-spec` 에서 적용 경계 작성 | [[raw/project-notes/ca-skeleton-operational-contract]] |
|
|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-OPTIONAL-ADAPTER-001@1` | optional adapter stack은 축마다 구현체 하나를 고정하고 core stack과 분리된 matrix로 관리한다 | `/branch-spec` 에서 적용 경계 작성 | [[raw/project-notes/ca-skeleton-operational-contract]] |
|
|
|
|
<!-- section-id: branch-local-decisions -->
|
|
### 브랜치 지역 결정
|
|
|
|
> 상세 근거와 선택 조건은 아래 `결정-근거 매핑` 절의 동일 D-row 가 소유한다. 여기에는 요약과 관계만 둔다.
|
|
|
|
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
|
|---|---|---|---|---|
|
|
| D1 | role = 별도 물리 deployment. prefix·DB number 는 격리 수단이 아니다 | `refines DEC-CA-SKELETON-OPERATIONAL-CONTRACT-REDIS-ROLE-001@1` | `redis-key-eviction-maxmemory-policy.md#REDIS-EVICT-C1`, `redis-cluster-spec-hash-slots-tags.md#REDIS-CLUSTER-C4` | `proposed` |
|
|
| D2 | capability 는 endpoint 가 아니라 role 을 참조하고, role 마다 runtime 을 분리한다 | `local` | `internal-design-doc` §10.3 · `internal-code-fact` | `proposed` |
|
|
| D3 | incompatible co-location 5조합은 production startup 거부, local 만 명시 opt-out | `local` | `redis-key-eviction-maxmemory-policy.md#REDIS-EVICT-C2` | `proposed` |
|
|
| D4 | `maxmemory`·eviction·persistence·backup 은 배포 설정이 소유하고 애플리케이션은 `CONFIG SET` 을 실행하지 않는다 | `local` | `redis-key-eviction-maxmemory-policy.md#REDIS-EVICT-C1`, `redis-acl-least-privilege.md#REDIS-ACL-C7` | `proposed` |
|
|
| D5 | 모든 물리 key 는 단일 `RedisKeyBuilder` 가 만들고 bounds 위반은 fail-open 대상이 아니다 | `local` | `internal-code-fact` · `redis-cluster-spec-hash-slots-tags.md#REDIS-CLUSTER-C1` | `proposed` |
|
|
| D6 | digest 는 식별자 성격별로 SHA-256 / HMAC-SHA-256 을 나눠 쓰고 length-prefixed canonical encoding 을 강제한다 | `local` | `internal-code-fact` · `internal-design-doc` §11.2 | `proposed` |
|
|
| D7 | hash tag 는 원자 연산에 필요한 최소 key group 만 묶고 tenant 전체를 태그로 쓰지 않는다 | `local` | `redis-cluster-spec-hash-slots-tags.md#REDIS-CLUSTER-C2`, `#REDIS-CLUSTER-C3` | `proposed` |
|
|
| D8 | key schema / payload schema / policy revision 3개 version 축을 분리한다 | `local` | `internal-design-doc` §11.4 · `internal-code-fact` | `proposed` |
|
|
| D9 | 요청 경로에서 pattern delete 를 하지 않고 대량 무효화는 4개 선택지로만 한다 | `local` | `redis-acl-least-privilege.md#REDIS-ACL-C7` · `internal-design-doc` §11.6 | `proposed` |
|
|
| D10 | Redis value 의 기본 타입은 `byte[]` 이며 codec 은 명시 등록한다 (reflection·JDK serialization 금지) | `local` | `internal-code-fact` · `internal-design-doc` §12.1 | `proposed` |
|
|
| D11 | envelope 는 자기기술(self-describing)이고 hard expiry 를 TTL 과 별도로 싣는다 | `local` | `internal-code-fact` · `internal-design-doc` §12.2 | `proposed` |
|
|
| D12 | schema 불일치·손상은 miss 와 다른 결과 종류로 분리하고 metric 을 나눈다 | `local` | `internal-code-fact` · project note §11 (owner [[raw/branch-notes/feature-capability-provider-selection-contract]]) | `proposed` |
|
|
| D13 | region 마다 크기 상한 5종을 필수로 두고 oversize 처리는 role 별로 비대칭이다 | `local` | `internal-code-fact` · `internal-design-doc` §12.4 | `proposed` |
|
|
| D14 | session·idempotency·rate 는 cache codec 을 재사용하지 않는다 | `local` | `internal-design-doc` §12.5 | `proposed` |
|
|
| D15 | topology 는 standalone·sentinel·cluster 배타 sum type 이고 binding model 과 runtime model 을 분리한다 | `local` | `redis-cluster-spec-hash-slots-tags.md#REDIS-CLUSTER-C4` · `internal-design-doc` §32.2 | `proposed` |
|
|
| D16 | timeout 은 7개 축으로 분리하고 production 은 모두 유한 상한을 요구한다 | `local` | `client-options-disconnected-behavior-queue-lettuce-official.md#LETTUCE-CO-C3` · `internal-design-doc` §31.6 | `proposed` |
|
|
| D17 | 일반 command 는 shared connection, blocking·Pub/Sub·maintenance 는 전용 connection | `local` | `client-options-disconnected-behavior-queue-lettuce-official.md#LETTUCE-CO-C3` · `internal-design-doc` §31.5 | `proposed` |
|
|
| D18 | disconnected 시 command 를 버퍼링하지 않고 즉시 거부하며 queue 는 유한하다 | `local` | `client-options-disconnected-behavior-queue-lettuce-official.md#LETTUCE-CO-C1`~`#LETTUCE-CO-C5` | `proposed` |
|
|
| D19 | correctness role 은 primary read 전용, replica read 는 stale 허용 cache region 만 opt-in | `local` | `redis-replication-asynchronous-read-routing.md#REDIS-REPL-C1`~`#REDIS-REPL-C5` | `proposed` |
|
|
| D20 | production 연결은 TLS 필수이고 plaintext·검증 완화는 startup 에서 거부한다 | `local` | `redis-tls-encryption-in-transit.md#REDIS-TLS-C1`~`#REDIS-TLS-C5` (hostname verification 은 `UNSUPPORTED_DECISION`) | `proposed` |
|
|
| D21 | role 마다 별도 ACL user 를 두고 `reset` 기준 least privilege 로 부여한다 | `local` | `redis-acl-least-privilege.md#REDIS-ACL-C1`~`#REDIS-ACL-C7` | `proposed` |
|
|
| D22 | `PING` 성공은 health 가 아니다 — liveness 는 Redis 비의존, readiness 는 required role 만 | `local` | `redis-replication-asynchronous-read-routing.md#REDIS-REPL-C2` · `internal-design-doc` §35.1~§35.3 | `proposed` |
|
|
| D23 | 연결 credential(username·password·trust material)은 Redis leaf 소유 SPI 로 조달하고 rotation 은 role 단위로 무중단 수행한다 | `local` | `redis-acl-least-privilege.md#REDIS-ACL-C1` · `redis-tls-encryption-in-transit.md#REDIS-TLS-C3` · `internal-design-doc` §34.8·§34.9 | `proposed` |
|
|
| D24 | client·topology 런타임 metric 을 bounded tag 로 노출하고 endpoint·key·tenant·token 을 tag 로 쓰지 않는다 | `local` | `client-options-disconnected-behavior-queue-lettuce-official.md#LETTUCE-CO-C3`·`#LETTUCE-CO-C4` · `redis-cluster-spec-hash-slots-tags.md#REDIS-CLUSTER-C5`·`#REDIS-CLUSTER-C6` · `internal-design-doc` §35.4·§35.10 | `proposed` |
|
|
| D25 | role 마다 persistence·replication **기대치(claim)** 를 선언하고 "AOF + replica = 절대 유실 없음" 이라고 말하지 않는다 | `local` | `redis-replication-asynchronous-read-routing.md#REDIS-REPL-C3`·`#REDIS-REPL-C5` · `internal-design-doc` §28.7 | `proposed` |
|
|
| D26 | Redis 런타임의 startup·graceful shutdown 을 **순서 불변식**으로 고정한다 | `local` | `internal-design-doc` §36.1·§36.2 · `internal-code-fact` | `proposed` |
|
|
|
|
<!-- section-id: declared-overrides -->
|
|
### 선언한 예외
|
|
|
|
| Override ID | Overrides | Reason | Approval | Status |
|
|
|---|---|---|---|---|
|
|
|
|
<!-- GENERATED: project-contract-imports:start -->
|
|
## 가져온 프로젝트 계약
|
|
|
|
| Ref | Owner | 요약 | Branch 적용 |
|
|
|---|---|---|---|
|
|
<!-- GENERATED: project-contract-imports:end -->
|
|
|
|
<!-- section-id: branch-goal -->
|
|
## 목표
|
|
|
|
- `WI-CA-SKELETON-OPERATIONAL-CONTRACT-061` 의 완료 조건을 구현한다: 3-role 연결·키·코덱·health 계약 test 와 incompatible co-location 거부 test 가 통과한다
|
|
|
|
- 이슈:
|
|
- PR:
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- cache·coordination·session 3-role 분리와 role 별 실패 의미
|
|
- role 별 연결·TLS·ACL·timeout·pool·topology refresh 설정
|
|
- key model — namespace·digest·hash tag·version·크기 상한
|
|
- payload envelope·codec·호환성·압축 임계
|
|
- role 별 health 구성요소
|
|
- `maxmemory`·eviction·persistence 의 소유 경계 (배포 설정 소유)
|
|
|
|
### 제외 범위
|
|
|
|
> 의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거.
|
|
|
|
- capability 별 **의미** 계약 — #067(cache 전략·TTL·stampede) · #068(rate-limit 알고리즘) · #069(session) · #070(idempotency) · #071(fenced lock) 소유. 본 branch 는 그들이 공유하는 **물리 형식**(role·연결·key·codec)만 소유한다
|
|
- Lua/Function 카탈로그 · program descriptor · `NOSCRIPT` 재적재 — #062 소유
|
|
- 활성화 SSOT · capability descriptor · readiness 등급 R0~R3 · capability card · 실패 정책 매트릭스 파생 · health **group** 구성 — #060 소유 (본 branch 는 소비자)
|
|
- Pub/Sub · keyspace notification · Streams 의 사용 계약 — `internal-design-doc` §26 경계. 본 branch 는 전용 connection 필요성만 언급한다
|
|
- multi-region / active-active Redis — R2 baseline 밖
|
|
- 배포 인프라 자체(`maxmemory` 값·AOF 설정·Sentinel 구성·backup 정책) — IaC/managed service 소유 (D4). 본 branch 는 **소유 경계와 검증 방식**만 정한다
|
|
- **네트워크 격리**(public 노출 금지·private endpoint/VPC·security group·Cluster bus 보호·egress allowlist) — D4 와 같은 성격의 배포 소유 항목이다. 애플리케이션 password 만으로 네트워크 노출을 정당화하지 않는다는 원칙만 여기 적고, 구성은 IaC 가 소유한다
|
|
- **관리 작업 audit log**(ACL·credential rotation·function 배포·대량 무효화·session 전역 무효화) — R3 성격이며 [[raw/branch-notes/feature-operational-error-observability-foundation]] 의 structured log 계약과 묶여야 한다. 본 branch 는 D23 의 "secret 값을 log 에 남기지 않는다" 경계만 소유한다
|
|
- project decision registry 변경 — owner 는 project-note
|
|
- registry 스키마·owner 이관 절차 — [[raw/branch-notes/feature-contract-registry-governance]] 소유 (§Audit A4 는 권고만)
|
|
|
|
## 근거 (필수, 최소 1개+)
|
|
|
|
| Source | 정당화하는 결정 |
|
|
|---|---|
|
|
| [[raw/official-docs/redis-tls-encryption-in-transit]] | production 에서 role 별 Redis 연결에 TLS(mutual TLS 포함)를 요구하고 `port 0`+`tls-port` 로 plaintext downgrade 를 startup 에서 거절하며, replication/cluster bus 채널은 `tls-replication`/`tls-cluster` 로 별도 활성화해야 한다는 결정의 근거 (단, hostname verification / trust-all 거절은 이 문서만으로 `UNSUPPORTED_DECISION` — 클라이언트 라이브러리 공식 문서 추가 필요) |
|
|
| [[raw/official-docs/client-options-disconnected-behavior-queue-lettuce-official]] | Redis 연결 런타임에서 `autoReconnect` / `disconnectedBehavior=REJECT_COMMANDS` / 유한한 `requestQueueSize` / command replay 억제를 명시 설정해 Redis 장애가 애플리케이션 heap 고갈로 번지지 않게 한다는 결정의 근거 (LETTUCE-CO-C1~C5) |
|
|
| [[raw/official-docs/redis-acl-least-privilege]] | role 마다 별도 named ACL user(cache-runtime / coordination-runtime / session-runtime)를 두고 `reset`(=`-@all` 포함) 상태에서 필요한 command category(`+@<category>`)·key pattern(`~<pattern>`)만 선택적으로 부여하는 least-privilege 결정의 공식 근거 |
|
|
| [[raw/official-docs/redis-key-eviction-maxmemory-policy]] | `maxmemory-policy` 는 인스턴스 단위 설정이라는 것과 `noeviction`/`volatile-*` eviction 시맨틱 — cache(evictable) vs coordination/session(noeviction) role 분리 및 policy 를 배포 설정이 소유한다는 결정의 근거 (REDIS-EVICT-C1~C5) |
|
|
| [[raw/official-docs/redis-cluster-spec-hash-slots-tags]] | key model 에서 hash tag `{...}` 로 원자 연산에 필요한 최소 key group 만 co-locate 하고, Cluster 는 database 0 만 사용하므로 DB number 를 격리 수단으로 쓸 수 없다는 결정의 근거 (16384 hash slot / CRC16 mod / hash tag 파싱 규칙 / multi-key same-slot 제약 / MOVED·ASK 리다이렉션 의미, REDIS-CLUSTER-C1~C6) |
|
|
| [[raw/official-docs/redis-replication-asynchronous-read-routing]] | coordination/session/idempotency/lease/rate role 은 primary read 만 쓰고 stale 을 허용하는 cache region 만 replica read 를 opt-in 한다는 read routing 결정의 근거 — Redis replication 은 기본 asynchronous 이고 replica 는 stale data 를 반환할 수 있으며, `WAIT` 조차 acknowledged write 의 failover 유실을 완전히 막지 못한다는 공식 서술 (REDIS-REPL-C1~C5) |
|
|
|
|
**추가 수집 대상 (미등록 — 아래 3건은 현재 `UNSUPPORTED_DECISION` 의 직접 원인이다):**
|
|
|
|
1. **Lettuce / Spring Data Redis 의 SSL 옵션** — hostname verification. Redis 서버 TLS 문서에는 이 개념이 **0회** 등장한다(§Audit A8) → D20 의 해당 부분 승급용
|
|
2. **Lettuce Cluster topology refresh** (periodic / adaptive 설정) — 이번 회차 fetch 에서 verbatim 미발견(§Audit A7) → D15 승급용
|
|
3. **Lettuce `replayFilter` semantics** — paraphrase 만 확인되어 폐기(§Audit A7) → D18 의 replay 억제 메커니즘 근거용
|
|
|
|
**프로젝트 내부 설계 참조 (등급 `internal-design-doc` — 공식 문서 아님, best practice 로 격상 금지):**
|
|
|
|
- ca-tmpl `docs/superpowers/specs/2026-07-26-redis-production-capability-design.md` (6,771줄) — **본 branch 의 1차 설계 근거**. 인용한 절: §5(HARD invariants) · §6(대안 A~F) · §10(role·co-location·정책 소유) · §11(key model) · §12(payload·codec) · §27(topology) · §28.7(role 별 persistence baseline) · §29.3~§29.4(eviction·OOM) · §31(client·timeout·queue·replay) · §32(configuration·validation) · §34(TLS·ACL·secret) · §35.1~§35.3·§35.10(health·client metrics)
|
|
- ca-tmpl `docs/superpowers/specs/2026-07-26-production-capability-platform-design.md`
|
|
- llm-wiki `docs/superpowers/specs/2026-07-28-ca-skeleton-production-capability-feature-decomposition-design.md` §4.2 #2 (본 branch 의 소유 관심사 정의) · §13(드리프트 정정)
|
|
|
|
> ⚠️ 위 ca-tmpl 문서의 실제 경로는 `/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/superpowers/specs/` 다 — `/home/donghyeon/workspace/ca-tmpl/` 사본에는 **없다**(§Audit A1).
|
|
|
|
## TODO
|
|
|
|
각 항목 옆에 증거 등급 표기: `actually-implemented` | `locally-verified` | `prod-verified` | `documented-only` | `planned` | `needs-confirmation`
|
|
|
|
- [x] `/branch-spec` 로 D1~D26 · §구현 가이드 10절 작성 (2026-07-28, `/depth`+`/coverage` 루프 2회 반영) — 등급: `documented-only`
|
|
- [ ] **선행**: [[raw/branch-notes/feature-capability-provider-selection-contract]] 완료 (활성화 SSOT·descriptor 확정 전 착수 금지) — 등급: `planned`
|
|
- [ ] **사용자 확정 필요**: Spring Data Redis 도입 여부 (§Audit A2 · §구현 가이드 4 `UNSUPPORTED_IMPL_DECISION`) — 등급: `needs-confirmation`
|
|
- [ ] role/deployment 설정 축 도입 + `env-keys.yaml` 신규 row 등록 (D1·D2) — 등급: `planned`
|
|
- [ ] incompatible co-location 거부 5조합 startup 검증 + negative test (D3) — 등급: `planned`
|
|
- [ ] key builder 미구현 검증 3종(hash tag 1개 · `{`/`}` 유입 차단 · kind allowlist) + builder 우회 금지 ArchUnit rule (D5) — 등급: `planned`
|
|
- [ ] HMAC key rotation mode 4종 + `RedisKeyDigestMaterialProvider` SPI (D6) — 등급: `planned`
|
|
- [ ] envelope 누락 필드 보강 — codec 식별자 · payload version 분리 · compressed flag · writtenAt · soft/hard expiry (D11, §Audit A3) — 등급: `planned`
|
|
- [ ] schema/corrupt 축 metric 분리 + 손상 entry quarantine (D12) — 등급: `planned`
|
|
- [ ] topology sum type(standalone/sentinel/cluster) + binding↔runtime model 분리 (D15) — 등급: `planned`
|
|
- [ ] timeout 7축 분리 (shutdown 을 command timeout 에서 떼기 포함, D16) — 등급: `planned`
|
|
- [ ] TLS 설정 축 + production plaintext 거부 (D20) — 등급: `planned`
|
|
- [ ] role 별 ACL user + positive grant 확정 + `@admin` 미부여 negative test (D21) — 등급: `planned`
|
|
- [ ] role health contributor + optional cache `DEGRADED` 표시 (D22) — 등급: `planned`
|
|
- [ ] `RedisCredentialMaterialProvider` SPI + role 별 rotation coordinator (D23) — 등급: `planned`
|
|
- [ ] client·topology metric 축 + `metrics.yaml` 행 등록 (D24, owner branch 경유) — 등급: `planned`
|
|
- [ ] 서버 정책 거부(`OOM`/`NOPERM`)를 결과 모델에 매핑 — `LettuceRedisRuntime.execute()` 의 `RedisCommandExecutionException` 분기 (D12) — 등급: `planned`
|
|
- [ ] role 별 durability baseline 선언 + startup·shutdown 순서 강제 test (D25·D26) — 등급: `planned`
|
|
- [ ] **owner 충돌 해소**: health 판정 SSOT(§Audit A9) · secret rotation policy(§Audit A10) · metric 이름 규약(§Audit A11) · cache fail-open 정책 3자(§Claims To Verify) — `/sync` 로 대조 — 등급: `needs-confirmation`
|
|
- [ ] **잔여 근거 수집 4건** (전부 `UNSUPPORTED_*` 의 직접 원인): Lettuce `SslOptions`/`RedisURI` TLS · Lettuce Cluster topology refresh · Lettuce `replayFilter` semantics · Lua 내부 명령의 ACL 적용 — 등급: `needs-confirmation`
|
|
- [ ] `/depth` + `/coverage` 3회차 재실행 (본 세션은 루프 천장 2회로 종료) — 등급: `planned`
|
|
- [ ] 완료 조건: 3-role 연결·키·코덱·health 계약 test 와 incompatible co-location 거부 test 가 통과한다 — 등급: `planned`
|
|
|
|
## 진행 중 메모
|
|
|
|
**as-built 현황 요약 (2026-07-28, `src/` grep 기준)** — 어디까지 실재하는지의 한 장 요약. 상세는 §구현 가이드.
|
|
|
|
| 영역 | 상태 |
|
|
|---|---|
|
|
| key model | ✅ canonical shape · SHA-256/HMAC digest · length-prefix 인코딩 · 길이·slug·hv 검증 (`actually-implemented`) |
|
|
| envelope | ⚠️ magic·version·type·sourceRevision·payload·SHA-256 digest 만 (목표 필드 절반, §Audit A3) |
|
|
| 결과 모델 | ✅ `Hit`/`Miss`/`NegativeHit`/`IncompatibleSchema`/`Unavailable` sealed 분리 (`actually-implemented`) |
|
|
| connection | ⚠️ standalone 단일 connection · raw Lettuce · `REJECT_COMMANDS`+유한 queue+admission 은 구현됨 |
|
|
| **role 개념** | ❌ **전혀 없음** — capability `"cache"`/hv `1`/kv `1`/kind `"entry"` 하드코딩 |
|
|
| topology | ❌ standalone 전용. Sentinel·Cluster 없음 |
|
|
| TLS · ACL | ❌ 0건. plaintext + password + default user |
|
|
| health | ❌ Redis health contributor 0개 |
|
|
| key rotation | ❌ `hv` 는 상수 `1`, secret 단일 값 |
|
|
| 압축 | ❌ 미구현 |
|
|
|
|
즉 **`internal-design-doc` §0 의 자기 평가(standalone/string cache 는 R1, 나머지는 R0)와 코드가 일치**한다. 본 branch 의 D-row 26개 중 코드에 이미 반영된 것은 D5·D6(부분) · D10 · D12(결과 모델) · D13(일부) · D18 이고, 나머지는 전부 `planned` 다.
|
|
|
|
**실 Redis 검증 lane**: `./gradlew :adapter:outbound:cache-redis:redisServiceTest`(JUnit 태그 `redis-service`, 기본 `test` 에서는 제외). 이 lane 이 `locally-verified` 등급을 뒷받침할 유일한 장치다.
|
|
|
|
## 결정 사항
|
|
|
|
- 2026-07-28: D1~D22 를 `/branch-spec` 으로 작성. 각 결정의 근거·선택 조건·Open Risk 는 아래 `결정-근거 매핑` 표가 소유한다(여기서 재진술하지 않음).
|
|
- 2026-07-28: 근거 수집 방침 — Redis **서버** 동작(eviction·cluster·replication·TLS·ACL)은 Redis 공식 문서 5건으로, **클라이언트** 동작(queue·replay·reconnect)은 Lettuce 공식 문서 1건으로 확보했다. 반면 role 분리·key model 의 *구조 설계*는 ca-tmpl `internal-design-doc` 이 유일한 근거이며 **공식 best practice 가 아니다** — 인용 시 "ca-tmpl 프로젝트 설계 결정" 으로 표기한다.
|
|
- 2026-07-28: 검토했으나 채택하지 않은 대안(`internal-design-doc` §6) — (A) 현행 `RedisClient` seam 에 method 추가, (B) application 에 범용 `RedisPort` 노출, (C) capability 마다 즉시 leaf 분리, (E) Redisson API 중심, (F) Redis Functions 전용. 채택은 **(D) 단일 physical Redis leaf + capability 별 package·semantic port** 다. 이유: (B)는 HARD invariant 2·3(application 에 Redis command 노출 금지) 위반, (C)는 19-모듈 고정 registry 를 즉시 깨고, (E)는 project note §34 의 "Redisson 을 기본 lock provider 로 삼지 않는다" 와 충돌한다.
|
|
|
|
<!-- section-id: decision-evidence -->
|
|
## Decision Evidence Map / 결정-근거 매핑
|
|
|
|
> `Evidence Strength` 어휘: `official-vendor-doc`(공식 벤더 문서) / `internal-design-doc`(ca-tmpl 설계 — **공식 아님, best practice 로 격상 금지**) / `internal-code-fact`(`src/` grep 으로 확인한 as-built) / `internal-policy`(project note 상속) / `UNSUPPORTED_DECISION`.
|
|
> `internal-design-doc` 의 §번호는 ca-tmpl `docs/superpowers/specs/2026-07-26-redis-production-capability-design.md` 기준이다.
|
|
|
|
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
|
|---|---|---|---|---|---|
|
|
| D1 | Redis role(`cache`/`coordination`/`session`)은 **별도 physical deployment**(managed database·cluster·instance)를 뜻한다. key prefix 나 Redis database number 로 role 을 나눴다고 말하지 않는다 | 3 role 중 둘 이상을 실제로 쓰는 모든 환경 → 별도 deployment. local·CI 단일 컨테이너 → D3 의 명시 opt-out 경로로만 공유하고 readiness 를 강등 | `REDIS-EVICT-C1`(`raw/official-docs/redis-key-eviction-maxmemory-policy.md` — `maxmemory-policy` 는 `maxmemory` 한도 도달 시 적용할 policy 를 고르는 **서버 configuration directive**), `REDIS-CLUSTER-C4`(`raw/official-docs/redis-cluster-spec-hash-slots-tags.md` — Cluster 는 database `0` 만 지원하고 `SELECT` 를 허용하지 않음) | **`official-vendor-doc`(전제) + 도출(결론)** — 두 공식 진술은 "policy 는 서버 단위 directive" 와 "Cluster 에 DB number 가 없다"까지만 말한다 | ⚠️ **"인스턴스당 `maxmemory-policy` 는 1개"는 원문이 시사할 뿐 명시하지 않는다** — `REDIS-EVICT-C1` 의 `Does not prove` 열이 "한 인스턴스 안에서 key 별/role 별로 다른 eviction policy 를 동시 적용할 수 있는지 여부 — 문서가 명시하지 않음"이라고 못박았다. D1·D3 은 `REDIS-EVICT-C1`+`C2` 에서 **도출한** 결론이며 단정 인용이 아니다. eviction·CPU·event loop·persistence fork·replication buffer·connection limit 이 인스턴스 단위라는 **전체 목록**도 `internal-design-doc` §10.2 다. deployment 수가 1→3 이 되면 인프라 비용·운영 표면이 늘어난다 — 비용 판단은 배포 소유자 몫 |
|
|
| D2 | capability 는 endpoint 를 직접 알지 않고 **role binding** 을 참조한다. role 마다 connection factory·client resources·credential·metrics scope·lifecycle 을 분리하고 전역 `@Primary RedisConnectionFactory` 를 두지 않는다 | endpoint·credential·topology 가 같고 실패 정책이 compatible → runtime 공유 허용. 하나라도 다르면 분리 | `internal-design-doc` §10.3(`cache region -> cache role -> cache-main deployment` binding 표), §31.4(deployment ID 마다 client·event loop·factory·metrics 소유). `internal-code-fact`: as-built 는 role 개념이 없다 — `RedisCacheAdapterConfig.redisStringCacheRegion()` 이 capability 를 문자열 `"cache"` 로, hv/kv 를 `1`/`1` 로, kind 를 `"entry"` 로 **하드코딩** | **`internal-design-doc` + `internal-code-fact`** — 외부 공식 근거 없음(프레임워크 배선 정책) | 활성화 축·descriptor 모양은 본 branch 소유가 아니다 — [[raw/branch-notes/feature-capability-provider-selection-contract]] D2·D5 가 owner. 두 branch 가 같은 시기에 property 이름을 각자 정하면 배선이 갈라진다 |
|
|
| D3 | 같은 physical deployment 에 다음이 bind 되면 **production startup 을 거부**한다: evictable cache + session / evictable cache + idempotency / evictable cache + fenced coordination / replica-read cache + primary-only correctness capability / 상호 모순되는 persistence·eviction attestation | production·staging → 무조건 거부. local profile → `allow-unsafe-colocation=true` 명시일 때만 공유 허용하고 readiness 를 R1 로 강등 | `REDIS-EVICT-C2`(`#REDIS-EVICT-C2` — `noeviction` 에서는 key 가 evict 되지 않고 새 데이터를 캐시하는 명령이 **에러를 반환**하며 read-only 명령은 정상 동작). 즉 한 인스턴스는 policy 를 하나만 가지므로 evictable 과 noeviction 요구가 공존할 수 없다. 조합 목록 자체는 `internal-design-doc` §10.4 | **`official-vendor-doc`(공존 불가의 원리) + `internal-design-doc`(5조합 목록)** | "readiness R1 강등"의 등급 어휘 owner 는 [[raw/branch-notes/feature-capability-provider-selection-contract]] D8 이고 그 D8 자체가 `UNSUPPORTED_DECISION` 이다. 등급 정의가 바뀌면 본 row 의 강등 표현도 따라가야 한다 |
|
|
| D4 | `maxmemory`·eviction policy·AOF/RDB·replica·Sentinel/Cluster 구성·TLS 종단·backup 은 **배포(IaC/managed service)가 소유**한다. 애플리케이션은 `CONFIG SET` 을 실행하지 않고, 가능하면 read-only introspection 으로 effective policy 를 확인하되 확인 불가를 "원하는 보장이 있다"로 대체하지 않는다 | managed service 가 `CONFIG GET` 을 허용 → 기동 시 read-only 검증. 차단 → operator attestation + 외부 conformance job. 둘 다 없음 → 해당 role 의 guarantee 를 주장하지 않음 | `REDIS-EVICT-C1`(policy 는 서버 configuration directive), `REDIS-ACL-C7`(`raw/official-docs/redis-acl-least-privilege.md` — `@admin` 카테고리는 `CONFIG`·`DEBUG`·`SAVE`·`SHUTDOWN` 등을 포함하며 공식 문서가 "일반 애플리케이션은 쓸 일이 없다"고 서술) | **`official-vendor-doc`** — `CONFIG` 를 애플리케이션 권한에서 빼는 것은 공식 카테고리 서술과 정합 | attestation 형식(서명·발급 주체·만료)은 정해진 바 없다 — §구현 가이드 1 의 `UNSUPPORTED_IMPL_DECISION`. attestation 이 형식적으로 흐르면 D4 는 "확인했다고 적는 칸"이 된다 |
|
|
| D5 | 모든 물리 key 는 **단일 `RedisKeyBuilder`** 로만 만든다. builder 는 전체 UTF-8 byte 길이·slug 문법·digest algorithm/version·hash tag 정확히 1개·user input 의 `{`/`}` 유입 차단·capability 별 kind allowlist 를 검증하고, **검증 실패는 backend outage 가 아니므로 fail-open 하지 않는다** | 모든 capability 의 모든 key → builder 경유. 예외 없음 (probe key 포함) | `internal-code-fact`: `RedisKeyBuilder.build()` 가 `ca:<app>:<env>:<capability>:<region>:hv<n>:kv<n>:{<slot>}:<digest>:<kind>` 를 생성하고 `maximumKeyBytes` 초과 시 `IllegalArgumentException`(`adapter/outbound/cache-redis/.../redis/key/RedisKeyBuilder.java`). `RedisKeyNamespace` 가 slug `[a-z][a-z0-9-]{0,62}`, hv/kv `1..9999`, maximumKeyBytes `1..4096` 강제. `REDIS-CLUSTER-C1`(key→slot 은 `HASH_SLOT = CRC16(key) mod 16384` 고정 공식) | **`internal-code-fact`(as-built) + `official-vendor-doc`(slot 공식)** | as-built 의 검증은 **길이·slug·hv 일치** 3개뿐이다. "hash tag 정확히 1개", "user input 의 `{`/`}` 차단", "capability 별 kind allowlist" 는 코드에 **없다** — `planned`. builder 밖에서 key 를 만드는 경로를 막는 정적 강제(ArchUnit)도 없다 |
|
|
| D6 | digest 는 식별자 성격으로 나눈다: 이미 random opaque ID → SHA-256, 사용자·tenant·email·IP 처럼 사전 공격이 가능한 값 → versioned HMAC-SHA-256. 입력은 **length-prefixed canonical encoding** 후 해싱하고 단순 delimiter join 은 금지한다. key HMAC secret 은 payload 암호화 키와 분리하며 `hv<n>` 세그먼트가 HMAC key version 을 고정한다 | opaque·비민감 → `opaque()`. 민감·composite scope → `sensitive()`. rate-limit IP → trusted resolver 가 normalized binary address 를 만든 뒤 HMAC | `internal-code-fact`: `RedisKeyDigest.opaque()` = SHA-256, `.sensitive()` = HmacSHA256(≥32 byte secret), 둘 다 `canonicalComponents()` 의 `putInt(len) + bytes` length-prefix 인코딩을 통과(`redis/key/RedisKeyDigest.java`). `RedisRuntimeSettings.hmacSecret()` 이 Base64 디코드 후 32 byte 미만을 거부. `internal-design-doc` §11.2(rotation mode 4종) | **`internal-code-fact`(알고리즘·인코딩) + `internal-design-doc`(rotation)** | rotation(`dual-read-delete`/`cold-cutover`/conservative overlap/`fixed`)과 `RedisKeyDigestMaterialProvider` SPI 는 **코드에 없다** — `planned`. 현재는 `app.cache.redis.key-hmac-secret` 단일 값이고 version 축이 없어 secret 교체 = 전체 key 무효화다 |
|
|
| D7 | `{slotTag}` 는 **같은 원자 연산에 필요한 최소 key group** 만 co-locate 한다(idempotency record + operation marker / sliding counter 의 current+previous bucket / lease owner + fencing counter / exact rate decision dedup record). tenant 전체를 hash tag 로 쓰지 않는다 | 원자 연산이 2개 이상 key 를 건드림 → 그 key 들만 같은 태그. 단일 key 연산 → 태그는 resource digest 의 bounded prefix 로 자연 분산 | `REDIS-CLUSTER-C2`("Then instead of hashing the key, only what is between the first occurrence of `{` and the following first occurrence of `}` is hashed."), `REDIS-CLUSTER-C3`(복잡한 multi-key 연산은 모든 key 가 같은 slot 일 때만 지원). `internal-code-fact`: `RedisKeyDigest.fromBytes()` 가 slotTag 를 digest hex 의 앞 8자로 고정 | **`official-vendor-doc`** — 태그 파싱 규칙과 same-slot 제약이 공식 진술 | as-built 는 slot tag 를 **항상 resource digest 앞 8자**로 만든다. 즉 "여러 key 를 의도적으로 같은 slot 에 모으는" D7 의 co-location 용법은 아직 표현할 수 없다 — 현재 코드는 분산만 된다(`planned`) |
|
|
| D8 | **key schema version(`kv`)** / **payload schema version** / **policy revision** 을 서로 다른 축으로 분리한다. key version 은 물리 레이아웃을, payload version 은 같은 key 의 decode 호환성을, policy revision 은 state 해석(TTL·rate 파라미터)을 바꾼다. 정책이 바뀌었는데 기존 counter/token state 를 새 의미로 재사용하지 않는다 | rate-limit key → policy revision 을 key 에 **포함**한다. cache entry → policy revision 은 key 밖(정책 registry) | `internal-design-doc` §11.4. `internal-code-fact`: key 에 `hv`(HMAC key version)와 `kv`(key schema version) 2축이 이미 있고(`RedisKeyBuilder`), envelope 에 별도 `VERSION` 바이트가 있다(`RedisCacheEnvelopeCodec.VERSION = 1`) — 즉 key 축과 payload 축은 분리되어 있다 | **`internal-code-fact`(2축 분리) + `internal-design-doc`(3축 규범)** | **policy revision 축은 코드에 없다.** rate-limit·cache 정책은 아직 key 에도 envelope 에도 실리지 않으므로, 정책 변경 시 옛 state 재사용을 막는 장치가 현재 0이다. 이 축의 실사용자는 #067·#068 이라 본 branch 는 *자리*만 정한다 |
|
|
| D9 | regular request 경로에서 **pattern delete 를 하지 않는다**. 대량 무효화는 (1) key schema/version bump, (2) region generation ID 교체, (3) known-key bounded batch, (4) operator maintenance 의 rate-limited `SCAN`+`UNLINK` 4가지로만 한다. generation key 가 evict 되어 `0` 으로 되돌아가 옛 namespace 를 부활시켜서는 안 되므로, missing 이면 새 random 128-bit generation 을 `SET NX` 로 초기화하고 loser 는 winner 값을 읽는다 | 전체 region 무효화 → (1) 또는 (2). 알려진 key 집합 → (3). 그 외 운영 정리 → (4), 요청 경로 밖 | `REDIS-ACL-C7`(`@admin`/dangerous 계열 command 를 애플리케이션이 쓰지 않는다는 공식 서술 — `KEYS`·`FLUSHALL` 계열 차단의 근거). 4개 선택지와 generation 프로토콜은 `internal-design-doc` §11.6 | **`official-vendor-doc`(위험 command 배제) + `internal-design-doc`(대체 4선택지)** | generation 프로토콜은 **코드에 없다**(`planned`). 또한 `SCAN`+`UNLINK` 를 도는 operator 도구가 없으면 (4)는 문서상의 선택지일 뿐이다. cache region 의 실제 무효화 사용자는 [[raw/branch-notes/feature-cache-strategy-registry-contract]] 라 그쪽 착수 전까지 검증되지 않는다 |
|
|
| D10 | Redis runtime 의 기본 value 타입은 `byte[]` 다. application object 를 reflection 으로 자동 직렬화하지 않고 JDK native serialization 과 무제한 polymorphic/default typing 을 금지한다. codec 은 `CacheCodec` 같은 **adapter 내부 SPI** 로 명시 등록하며 application use case 는 그 타입을 보지 않는다 | 모든 Redis 저장 값 → `byte[]` + 명시 codec. 타입 안전이 필요한 지점 → semantic port 의 typed 시그니처로 해결하고 codec 은 adapter 안에 둔다 | `internal-code-fact`: `LettuceRedisRuntime` 이 `ByteArrayCodec.INSTANCE` 로 접속하고 `RedisBinaryCommands` 가 `byte[]` 만 노출. `CacheRegionPort<String,String>` 만 Spring composition 에 노출되고 `RedisStringCacheRegion`·`RedisCacheEnvelopeCodec` 은 package-private. `internal-design-doc` §12.1 | **`internal-code-fact`** — as-built 가 이미 이 형태 | as-built 의 codec 은 **String 전용 1개**이고 `CacheCodec<T>` 같은 일반 SPI 는 없다. 타입이 늘어나는 순간(#067·#069·#070) SPI 모양을 정해야 하며 그 API 는 아직 결정되지 않았다 |
|
|
| D11 | cache envelope 는 자기기술이며 최소한 magic·envelope version·codec 식별자·payload version·flags(negative/compressed)·source revision·기록 시각·hard expiry·payload length·payload digest·payload 를 담는다. **Redis key TTL 은 hard expiry 이후의 물리적 정리**를 담당하고, **envelope 의 hard expiry 는 client 가 stale/expired 를 판정하고 clock·TTL drift 를 관측하는 방어선**이다 | 재생성 가능한 cache entry → 이 envelope. session·idempotency → D14 의 별도 스키마 | `internal-code-fact`: `RedisCacheEnvelopeCodec` 이 magic `0x43414348` + version + type(POSITIVE/NEGATIVE) + sourceRevision + payload + **SHA-256 digest 접미**를 인코딩하고 digest 불일치를 `CORRUPT_ENVELOPE` 로 판정. `internal-design-doc` §12.2(필드 목록) | **`internal-code-fact`(부분 구현) + `internal-design-doc`(목표 필드)** | **as-built 는 목표 필드의 절반이다** — codec 식별자·payload version(별도 축)·compressed flag·writtenAt·soft/hard expiry 가 **없다**. 따라서 현재는 TTL 만이 만료의 유일한 근거이고 clock drift 를 관측할 수단이 없다. §Audit `ENVELOPE_FIELD_GAP` 참조 |
|
|
| D12 | schema 불일치와 손상은 miss 로 접지 않는다. writer 는 한 version 만 쓰고 reader 는 현재 N 과 migration window 의 N-1 을 읽는다. 알 수 없는 미래 version → `SCHEMA_MISMATCH`, decoder 예외·길이 오류·digest 불일치 → `CORRUPT`, 둘 다 miss 와 **다른 metric**. 손상 항목은 bounded owner-safe quarantine/evict 후 정책에 따라 source 를 조회한다. **프로그래밍 결함을 Redis unavailable 로 분류하지 않는다** | **category → policy 분기 (as-built)**: `FUTURE_VERSION` → `FAIL_FAST`(앞으로 나아갈 수 없음 — 새 writer 가 이미 배포됨), 그 외(`RETIRED_VERSION`·`CORRUPT_ENVELOPE`·`UNKNOWN_ENVELOPE`) → `QUARANTINE_AND_RELOAD`(`RedisStringCacheRegion.java:58-63`). **서버 정책 거부는 별도 축이다** — `noeviction` OOM 은 용량 사건이므로 `Unavailable(UNAVAILABLE, NOT_APPLIED)`(프로그래밍 결함 **아님**), `NOPERM`(ACL 미부여)은 배선 결함이므로 fail closed 이며 `IncompatibleSchema` 로 접지 않는다. rolling deploy 에서 old reader 가 new payload 를 못 읽음 → writer 전환 **전에** dual-readable codec 을 먼저 배포 | `internal-code-fact`: `CacheLookup` sealed 계층이 `Hit`/`Miss`/`NegativeHit`/`IncompatibleSchema`/`Unavailable` 을 이미 분리하고, `RedisCacheEnvelopeCodec.decode()` 가 `FUTURE_VERSION`/`RETIRED_VERSION`/`CORRUPT_ENVELOPE`/`UNKNOWN_ENVELOPE` 를 구분(`application-core/.../cache/CacheLookup.java`, `redis/RedisCacheEnvelopeCodec.java`). 정책 값은 project note §11 "Capability 별 실패 정책 매트릭스" 2행(cache backend 실패 = fail open / cache codec·프로그래밍 결함 = fail closed) — owner [[raw/branch-notes/feature-capability-provider-selection-contract]] D12 | **`internal-code-fact`(결과 모델) + `internal-policy`(정책 값 — 상속, 재진술 아님)** | 결과 종류는 코드에 있으나 **metric 분리는 없다**(`metrics.yaml` 의 cache row 2개는 `cache.gets.total`·`cache.invalidations.total` 뿐, schema/corrupt 축 없음). quarantine 동작도 미구현 — 현재는 `IncompatibleSchema` 를 돌려줄 뿐 손상 키를 지우지 않는다 |
|
|
| D13 | region 마다 (1) 최대 encoded bytes, (2) 최대 decoded bytes, (3) 최대 collection element 수, (4) 최대 압축비, (5) encode/decode deadline 5종 상한을 **필수**로 둔다. 압축은 임계 이상에서만 opt-in 하고 decompression bomb 를 막기 위해 decoded size 와 비율을 먼저 제한하며, secret 과 공격자 제어 값을 같은 압축 컨텍스트에 섞지 않는다 | cache 의 oversize → cache write 만 `REJECTED_TOO_LARGE` 로 만들고 **source 결과 자체는 실패시키지 않는다**. session·idempotency response 의 oversize → 해당 capability 계약에 따라 fail closed | `internal-code-fact`: `RedisRuntimeSettings.maximumValueBytes`(1..16MiB), `maximumQueuedCommands`(1..4096), `maximumInFlightBytes`(≤256MiB) + `maximumRetainedCommandBytes` 교차 검증이 있고, `LettuceRedisRuntime.get()` 이 bounded Lua `GETRANGE` 로 wire bulk reply 를 상한+1 byte 로 잘라 `CA_VALUE_TOO_LARGE` 를 던진다. `internal-design-doc` §12.4 | **`internal-code-fact`(1·5의 일부) + `internal-design-doc`(5종 규범)** | **압축 자체가 미구현이므로 (4) 최대 압축비는 검증할 대상이 없다.** (2)(3)(5)도 코드에 없다 — decoded 상한은 encoded 상한으로 대신하고 있고 collection API 와 encode deadline 은 존재하지 않는다. oversize 의 role 별 비대칭은 session·idempotency branch 착수 전까지 미검증 |
|
|
| D14 | cache codec 을 session·idempotency·rate/lease 에 그대로 재사용하지 않는다. session 은 allowlist 된 security/session attribute 스키마와 rolling 호환성을, idempotency 는 request fingerprint metadata 와 bounded response codec 을, rate/lease 는 고정 primitive 스키마를 쓰며 **임의 객체 직렬화를 두지 않는다** | 재생성 가능한 값 → cache codec. 인증 상태·중복 방지 상태·카운터 → 각 capability 전용 스키마 | `internal-design-doc` §12.5(규범 서술). **메커니즘 근거는 `internal-code-fact`**: as-built envelope 은 (a) `sourceRevision` 을 **필수**로 요구하고(`encode()` 가 blank·>128자·>512 byte 를 거부), (b) payload 를 **단일 String** 으로 전제하며(`Positive(String value, …)`), (c) `AuthoritativeAbsence` 기반 negative entry 개념을 내장한다(`RedisCacheEnvelopeCodec.java:27-39,106-119`). session attribute allowlist·idempotency request fingerprint·rate counter 는 이 세 전제 중 어느 것도 만족하지 않는다 | **`internal-code-fact`(재사용 불가의 메커니즘) + `internal-design-doc`(규범)** | 각 스키마의 실제 필드는 #069·#070·#071 이 정한다. 본 branch 는 "cache codec 재사용 금지"라는 **경계만** 소유하므로, 그 branch 들이 편의상 cache codec 을 재사용하면 이 경계를 강제할 정적 장치가 현재 없다 |
|
|
| D15 | deployment 하나는 `standalone`·`sentinel`·`cluster` 중 **정확히 하나**를 고른다. host list 가 비었거나 두 topology field 가 동시에 설정되면 startup failure 다. Spring binder 가 sealed subtype 을 자동 판별한다고 가정하지 않고 **binding model(concrete `@ConfigurationProperties`)과 검증된 runtime model(immutable sealed)을 분리**해 factory 가 discriminator 와 exactly-one nested property 를 검증한다 | **topology 판별**: 단일 노드 용량으로 충분하고 HA 가 불필요(local·CI·외부 HA 를 제공하는 managed endpoint) → `standalone`. HA 는 필요하지만 key space·throughput 이 한 노드에 들어감 → `sentinel`. key space 또는 throughput 이 한 노드를 넘음 → `cluster`(이때 D7 의 hash tag co-location 이 **필수**가 되고 D19 의 replica opt-in 이 실제 선택지가 된다). 선택 이후의 귀결 — Cluster 는 database `0` 고정(`REDIS-CLUSTER-C4`), Sentinel 은 discovery 채널과 data-node 채널의 credential·trust material 을 **각각** 표현하고 production 에서 둘 다 검증. **`UNSUPPORTED_DECISION`(임계값)**: "한 노드에 들어간다"의 구체 임계(메모리·QPS)는 근거 없음 — workload 측정 후 확정 | `REDIS-CLUSTER-C4`(Cluster 는 database `0` 만 지원, `SELECT` 불가). `internal-design-doc` §27.1·§32.2. `internal-code-fact`: as-built 는 standalone 전용이다 — `LettuceRedisRuntime.redisUri()` 가 `RedisURI.Builder.redis(host, port)` 만 만든다 | **`official-vendor-doc`(Cluster DB 제약) + `internal-design-doc`(sum type 배선)** | Sentinel·Cluster 는 **코드에 전혀 없다**(`planned`). Cluster 진입 시 D7 의 hash tag co-location 이 실제로 필요해지므로 D7 의 미구현 부분과 함께 열린다. Lettuce 의 topology refresh(periodic/adaptive) 설정은 이번 회차 공식 근거 수집에 **실패**했다 — §Audit `LETTUCE_TOPOLOGY_REFRESH_UNSOURCED` |
|
|
| D16 | timeout 을 DNS/connect · TLS handshake · pool acquire · command · capability overall deadline · blocking command · shutdown **7개 축으로 분리**하고 하나의 global timeout 으로 합치지 않는다. production 설정은 모두 유한 상한을 요구하며 `capability overall deadline ≤ caller deadline`, `lease wait + work budget < caller deadline`, `blocking timeout < connection lifecycle timeout` 관계를 startup 이 검증한다 | 정확한 기본값 → workload SLO 로 조정. 그러나 "무한/미설정" 은 어떤 축에서도 production 에서 허용하지 않는다 | `LETTUCE-CO-C3`(`raw/official-docs/client-options-disconnected-behavior-queue-lettuce-official.md` — `requestQueueSize` 기본값이 `Integer.MAX_VALUE` 이고 per-connection 이며 초과 시 `RedisException`). 축 분리 목록은 `internal-design-doc` §31.6. `internal-code-fact`: as-built 는 **command timeout 1축**뿐(`RedisRuntimeSettings.commandTimeout`, 기본 2s, ≤30s) | **`official-vendor-doc`(무한 기본값의 위험) + `internal-design-doc`(7축 분해)** | as-built 는 `shutdownTimeout` 을 `commandTimeout` 으로 **재사용**한다(`LettuceRedisRuntime` 필드). 즉 7축 중 실질 2축이 하나의 값에 묶여 있어, command timeout 을 낮추면 shutdown 이 함께 짧아진다. 나머지 5축은 `planned` |
|
|
| D17 | 일반 non-blocking command 는 thread-safe shared native connection 을 쓴다. blocking `XREAD`·Pub/Sub·connection affinity 가 필요한 transaction·장기 maintenance·stateful command mode 는 **전용 connection/pool** 을 쓰며, blocking operation 이 일반 cache/rate connection 을 점유하지 못하게 한다. 실질 제어 축은 connection 개수가 아니라 **in-flight/queue bound** 다 | 일반 command → shared. 위 5종 → 전용. pool 크기 조정보다 in-flight 상한 조정을 먼저 한다 | `LETTUCE-CO-C3`(`requestQueueSize` 는 **per-connection** 단위), `LETTUCE-CO-C4`(Cluster 는 여러 커넥션을 쓰므로 전체 큐 상한이 `requestQueueSize * ((number of cluster nodes * 2) + 1)` 로 배수화). `internal-design-doc` §31.5 | **`official-vendor-doc`** — queue 가 connection 단위라는 점이 "connection 수 = heap 배수"의 직접 근거 | as-built 는 **단일 connection** 이고 blocking/Pub/Sub 사용처가 아직 없다(`planned`). Streams·Pub/Sub 은 본 branch 범위 밖(§26 경계)이므로 전용 connection 계약은 그 사용처가 생길 때 검증된다 |
|
|
| D18 | reconnect 중 command 를 무제한 버퍼링하지 않는다. `disconnectedBehavior = REJECT_COMMANDS`, 유한한 `requestQueueSize`, driver-level pending replay 억제를 연결 생성 **전에** 고정하고, admission 포화는 `OVERLOADED` 로 즉시 거부한다. retry-safe 연산도 driver replay 가 아니라 capability wrapper 가 total deadline 안에서 새 invocation 으로 재시도한다 | `GET`/`TTL`·desired-absent delete → deadline 안 bounded retry. `INCR`/token consume·idempotency claim·lease acquire → operation dedup·owner token 없이 자동 replay 금지 | `LETTUCE-CO-C1`("As soon as a connection gets closed/reset without the intention to close it, the client will try to reconnect, activate the connection and re-issue any queued commands."), `LETTUCE-CO-C2`(REJECT_COMMANDS = 항상 즉시 거부), `LETTUCE-CO-C3`(기본 큐 무제한), `LETTUCE-CO-C5`(기본 at-least-once — "commands may be duplicated but not lost"). `internal-code-fact`: `LettuceRedisRuntime.clientOptions()` 가 `autoReconnect(true)`·`replayFilter(ignored -> true)`·`REJECT_COMMANDS`·유한 `requestQueueSize`·`TimeoutOptions` 를 명시 설정하고, `RedisCommandAdmission` 이 count+byte 이중 semaphore 로 즉시 거부 | **`official-vendor-doc` + `internal-code-fact`** — 본 branch 에서 **가장 근거가 강한 row** (공식 기본값 + as-built 반대 설정이 모두 확인됨) | `replayFilter` 의 semantics 는 **공식 문서 인용을 확보하지 못했다** — fetch 결과에 verbatim 이 없어 폐기됨(§Audit `LETTUCE_REPLAY_FILTER_UNSOURCED`). 즉 "replay 억제"의 *의도*는 코드에 있으나 *그 API 가 그렇게 동작한다*는 벤더 진술이 없다. Lettuce 버전 업 시 semantics 변경을 잡는 compatibility test 가 필요하다 |
|
|
| D19 | coordination·session·idempotency·lease·rate 는 **primary read 전용**이다. cache 도 기본은 primary 이며 stale 을 명시적으로 허용한 region 만 replica read 를 opt-in 하고, 그 region descriptor 에 "무효화 직후 옛 replica 값을 읽을 수 있음"을 적는다 | replica read 는 latency·확장 옵션이지 read-your-write 보장이 아니다 → 정확성이 걸린 role 은 어떤 경우에도 opt-in 하지 않는다 | `REDIS-REPL-C1`("Redis uses asynchronous replication, with asynchronous replica-to-master acknowledges of the amount of data processed."), `REDIS-REPL-C2`(초기 동기화 중에도 옛 데이터셋으로 쿼리 처리 가능), `REDIS-REPL-C3`("...it is not possible to ensure the replica actually received a given write, so there is always a window for data loss."), `REDIS-REPL-C4`(replica 는 2.6 이후 기본 read-only), `REDIS-REPL-C5`(`WAIT` 조차 CP·strong consistency 를 주지 않으며 acknowledged write 가 failover 로 유실될 수 있음) | **`official-vendor-doc`** — 5개 claim 이 모두 벤더 진술이고 D19 의 금지 방향과 정확히 일치 | as-built 에 replica·read preference 개념이 **없다**(`planned`). 또한 REPL-C3·C5 는 primary-only 로도 유실 창이 남는다는 뜻이다 — D19 는 stale read 를 막을 뿐 **failover 유실을 막지 못한다**. 그 한계를 capability card 에 적는 책임은 [[raw/branch-notes/feature-capability-provider-selection-contract]] D9 |
|
|
| D20 | production 의 role 별 Redis 연결은 TLS 를 켜고 신뢰 CA 를 명시하며 plaintext downgrade 를 금지한다. 서버 측은 `port 0` + `tls-port` 로 비-TLS 포트를 닫고, replication 은 `tls-replication`, Cluster bus 는 `tls-cluster` 로 **채널마다 따로** 켠다. `trust-all` 과 검증 완화는 local 전용이며 production startup 에서 거절한다 | local·CI → 완화 허용(명시 플래그). production·staging → 거절. Sentinel 사용 시 discovery 채널과 data-node 채널의 trust material 을 각각 검증 | `REDIS-TLS-C1`(TLS 는 Redis 6부터의 **컴파일 타임 옵션**이며 기본 활성이 아님), `REDIS-TLS-C2`("You may specify `port 0` to disable the non-TLS port completely."), `REDIS-TLS-C3`(기본 mutual TLS + 신뢰 루트 CA 검증, `tls-auth-clients no` 로 완화 가능), `REDIS-TLS-C4`(`tls-replication yes` 필요), `REDIS-TLS-C5`(`tls-cluster yes` 필요), `REDIS-TLS-C6`(TLS 는 처리량을 낮춤) | **`official-vendor-doc`** — 단, **hostname verification 부분만 `UNSUPPORTED_DECISION`** | **`UNSUPPORTED_DECISION`(부분)**: 이 공식 문서는 "hostname verification" 을 한 번도 언급하지 않는다(source 노트의 self-grep 0건). hostname 검증은 *클라이언트* 책임이며 Redis 서버 문서가 다룰 주제가 아니다 — Lettuce/Spring Data Redis 공식 문서를 추가 수집해야 승급한다. trade-off: 그때까지 "hostname verification 필수"는 **일반적 TLS 관행에 기댄 내부 정책**으로만 유지한다. 또한 as-built 에 SSL 설정이 **0건**이다(`grep withSsl\|verifyPeer` → 0) |
|
|
| D21 | role 마다 별도 named ACL user(`cache-runtime`/`coordination-runtime`/`session-runtime`/`program-deployer`/`operator-readonly`)를 두고, `reset` 으로 무권한 상태에서 시작해 필요한 command category 와 key pattern 만 부여한다. default user 는 production 에서 비활성화한다. ACL key pattern 과 애플리케이션 key builder 는 **같은 versioned prefix registry** 에서 생성되어야 하며 hash tag·user input 으로 prefix 를 탈출할 수 없어야 한다 | Function provisioning 계정과 runtime 계정을 분리한다(`program-deployer` vs `*-runtime`) — 상세는 [[raw/branch-notes/feature-redis-atomic-program-catalog-contract]] 소유. ACL 카테고리는 Redis 버전업으로 확장될 수 있으므로 allowlist + **negative** integration test 를 쓴다 | `REDIS-ACL-C1`(ACL 은 연결 단위로 실행 가능 command 와 접근 가능 key 를 제한), `REDIS-ACL-C2`(`~<pattern>` glob key pattern, 복수 지정 가능), `REDIS-ACL-C3`("* There are no key patterns that the user can access." — 새 user 는 기본 무권한), `REDIS-ACL-C4`(`+@<category>`/`@all` 의미), `REDIS-ACL-C5`(`reset` 이 `-@all` 포함 초기화 수행), `REDIS-ACL-C7`(`@admin` 이 `CONFIG`·`DEBUG`·`SAVE`·`MONITOR`·`ACL`·`SHUTDOWN` 포함) | **`official-vendor-doc`** — 메커니즘 전부가 벤더 진술 | **과대해석 주의**: `REDIS-ACL-C6` 은 공식 문서가 보안(goal 1)과 운영 안전성(goal 2)을 **병렬 목적**으로 제시할 뿐, "ACL 은 보안 경계가 아니라 실수 방지용"이라는 이분법을 지지하지 **않는다**. 그렇게 인용하면 overclaim 이다. 또한 as-built 는 ACL username 을 쓰지 않는다 — `LettuceRedisRuntime.redisUri()` 가 `withPassword` 만 호출하므로 현재는 default user 접속이다(`planned`) |
|
|
| D22 | `PING` 성공은 write 가능·올바른 primary·Cluster slot coverage·persistence 정상·noeviction 여유·필요한 Function version·serializer/key 호환·ACL 권한 중 **어느 것도 보장하지 않는다**. 따라서 health 는 capability 와 role 관점으로 구성한다. **liveness 는 Redis 에 의존하지 않는다**(Redis 장애로 pod 를 반복 재시작하면 connection storm 과 failover 를 악화시킨다). readiness 는 required role 의 connection/auth/TLS·topology/primary·최소 read/write 능력·program digest·최근 성공/오류 예산·queue 포화를 본다 | optional cache backend down → **ready 유지 + `DEGRADED` 표시**. required session·idempotency down → not ready. 사용하지 않는 role → health check 자체를 만들지 않는다 | `REDIS-REPL-C2`(replica 가 초기 동기화 중에도 옛 데이터셋으로 응답 가능 — 즉 응답한다고 최신이 아니다). 나머지 구성요소 목록은 `internal-design-doc` §35.1~§35.3 | **`internal-design-doc` + `official-vendor-doc`(응답≠최신 한 조각)** | ⚠️ **owner 충돌**: [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] 의 `Required vs Optional Dependency Matrix` 가 Redis 행을 **이미 확정**해 두었다 — `startup validation = ping`, `unavailable → degraded ready (cache-aside fallback)`. D22 는 그 행의 `ping` 을 **부정**한다. `degraded ready` 부분은 일치하므로 D22 는 그 행의 *startup validation 열만 refine* 하는 관계다 — §Audit `HEALTH_MATRIX_OWNER_CONFLICT` 에서 사용자 확정 대기. health **group 구성**(어떤 indicator 가 readiness group 에 들어가는가)의 owner 는 [[raw/branch-notes/feature-capability-provider-selection-contract]] D7(`readinessImpact`)이다. 본 branch 는 *role health contributor 가 무엇을 확인하는가*만 소유한다. 현재 코드에 Redis health contributor 는 **0개** |
|
|
| D23 | 연결 credential(ACL username · password · trust material)은 **Redis leaf 소유 SPI** 로 조달하고, resolve 결과를 `Resolved`/`TemporarilyUnavailable`/`Expired`/`InvalidReference`/`PermissionDenied` 로 구분한다. secret 값·reference 전체·provider 예외 메시지를 log/metric 에 남기지 않는다. rotation 은 **role 단위**로 수행하며 listener 는 새 version 을 알릴 뿐 event thread 에서 client 를 바꾸지 않고, role runtime 의 직렬화된 rotation coordinator 가 새 factory 검증 → traffic switch → old connection drain 을 수행한다 | **재시작 창이 허용되면 → registry 현행 값(`restart-only` / `dual-read-restart-only`)을 따르고 8단계 대신 startup re-resolve + 연결 검증만 둔다.** 무중단이 필요하면 → 8단계 프로토콜(new credential/ACL 추가 → dual-valid overlap → 새 factory/session drain → 연결·명령 test → traffic switch → old connection drain → old credential revoke → stale client alert). 한 global connection 을 즉시 끊어 모든 role 이 동시에 outage 되지 않게 **role 별로** 수행한다. event 만 믿지 않고 만료 전 bounded periodic re-resolve 를 둔다 | `REDIS-ACL-C1`(ACL 이 연결 단위 권한 — username 이 연결 credential 의 일부), `REDIS-TLS-C3`(기본 mutual TLS 가 신뢰 루트 CA 검증 인증서를 요구 — trust material 이 credential 과 함께 조달돼야 하는 이유). SPI 형태·rotation 8단계는 `internal-design-doc` §34.8·§34.9 | **`official-vendor-doc`(조달 대상이 무엇인지) + `internal-design-doc`(SPI 모양·8단계)** | ⚠️ **registry 선점**: `docs/registries/secrets-classification.yaml:92-99` 이 `APP_CACHE_REDIS_KEY_HMAC_SECRET` 에 `rotation_policy: dual-read-restart-only` 를 **이미 선언**했고 `APP_CACHE_REDIS_PASSWORD` 는 `restart-only` 다 — 즉 registry 는 이미 "무중단 아님"을 못박았다. D23 의 무중단 rotation 은 그 행의 **변경을 요구**한다. registry owner 는 [[raw/branch-notes/feature-secrets-config-source-contract]] — §Audit `SECRET_ROTATION_POLICY_DRIFT`. 또한 as-built 에 SPI 가 **0건**이다(`grep CredentialMaterialProvider` → 0) |
|
|
| D24 | client·topology 런타임 metric 을 노출하되 tag 는 `deployment`·`role`·`capability`·`operation`·`outcome`·`topology` 로 **bounded** 하고, endpoint·key·tenant·user·session·owner token 은 **tag 로 쓰지 않는다**. metric 행 등록은 `metrics.yaml` 의 change set 절차를 경유한다 | 애플리케이션이 노출 → client·capability 축(connect/reconnect · command timeout · queued/rejected · pool acquire/saturation · MOVED/ASK · topology refresh/failure/age · sentinel failover · connection age · TLS/auth failure · NOSCRIPT · function digest mismatch · BUSY). 운영이 수집 → server INFO 축(`used_memory`·`evicted_keys`·replication lag 등) — **애플리케이션이 server INFO 전체를 고cardinality metric 으로 무분별 export 하지 않는다** | `LETTUCE-CO-C3`(queue 초과 시 `RedisException` — rejected command 를 셀 수 있는 지점), `LETTUCE-CO-C4`(Cluster 전체 queue 상한이 노드 수에 배수 — saturation 을 노드 축으로 봐야 하는 근거), `REDIS-CLUSTER-C5`·`C6`(MOVED = 영구 재배치 / ASK = 일회성 — 두 리다이렉션을 **구분해** 세야 하는 근거). 목록·tag 규칙은 `internal-design-doc` §35.4·§35.10 | **`official-vendor-doc`(개별 계측 지점의 의미) + `internal-design-doc`(목록·tag 집합)** | `metrics.yaml` 행의 owner 는 [[raw/branch-notes/feature-metrics-alerting-contract]] 이고 cardinality bound 표도 그 branch 소유다 — 본 branch 는 **어떤 축을 측정할지** 만 정하고 행 등록은 그 branch 절차를 경유한다. 현재 registry 의 Redis 관련 행은 `cache.gets.total`·`cache.invalidations.total` **2개뿐**이라 위 축을 담을 자리가 0이다. metric 이름 자체는 `internal-design-doc` 의 제안형(`redis.capability.*`)이며 Micrometer dot.case + unit suffix 규약과의 정합은 미검증 |
|
|
| D25 | role 마다 persistence·replication **기대치**를 선언한다 — `cache` = persistence optional / replica read optional / "재생성 가능, 유실 허용", `coordination` = 명시적 AOF·RPO / primary+replica / "저지연 상태이나 **유실은 여전히 가능**", `session` = 제품 RPO 에 맞는 AOF·HA / primary+replica / "세션 연속성은 best effort, 복구는 재인증". **"AOF + replica = 절대 유실 없음"은 금지 문구다** | 이 기대치는 IaC 가 무엇을 provisioning 해야 하는지의 **입력 스펙**이다 — 값 자체(어떤 RPO 인가)는 배포가 정하고(D4), **어떤 claim 을 만족해야 하는가**는 본 branch 가 정한다. 기대치를 만족하는지 확인 불가하면 D4 의 3번째 분기(guarantee 미주장)로 떨어진다 | `REDIS-REPL-C3`("...it is not possible to ensure the replica actually received a given write, so there is always a window for data loss."), `REDIS-REPL-C5`(`WAIT` 조차 CP·strong consistency 를 주지 않으며 acknowledged write 가 failover 로 유실될 수 있음) — **금지 문구의 직접 근거**. 3-row 기대치 표는 `internal-design-doc` §28.7 | **`official-vendor-doc`(금지 문구) + `internal-design-doc`(role 별 기대치 표)** | 기대치를 *선언*할 뿐 강제할 장치가 없다 — D4 의 attestation 형식이 `UNSUPPORTED_IMPL_DECISION` 이므로 "coordination role 이 실제로 AOF 인가"를 검증할 방법이 확정되지 않았다. 또한 `REDIS-REPL-C3`·`C5` 때문에 **어떤 설정으로도 유실 창은 남는다** — 이 한계를 capability card 에 적는 책임은 [[raw/branch-notes/feature-capability-provider-selection-contract]] D9 |
|
|
| D26 | Redis 런타임의 startup 과 graceful shutdown 을 **순서 불변식**으로 고정한다. startup 은 config 검증 → secret 조달 → client resources → topology/connect/auth → program/schema → role attestation → provider binding → health/readiness → background 작업 순이며 **background task 를 connection 검증 전에 시작하지 않는다**. shutdown 은 readiness off → background 중단 → 신규 long operation 중단 → in-flight bounded wait → owner-safe lease release(best effort) → session save → Pub/Sub·stream listener stop → 전용 connection close → shared client resources close 순이며 **release 응답이 없다고 key 를 blind delete 하지 않는다** | 개별 검증(D3 co-location · D15 topology · D16 timeout 관계 · D20 TLS · D21 ACL · D22 health · D23 secret)이 *무엇을* 검사하는지는 각 D-row 가 정하고, 본 D26 은 *어떤 순서로* 실행되는지만 정한다. 두 축을 섞지 않는다 | `internal-design-doc` §36.1(startup 9단계)·§36.2(shutdown 9단계). `internal-code-fact`: as-built 는 `LettuceRedisRuntime.close()` 가 `connection.close()` → `client.shutdown(0, shutdownTimeout)` 2단계뿐이고 Spring `@Bean(destroyMethod = "close")` 에 위임한다 — background 작업·lease·session 이 없으므로 현재는 이 2단계로 충분하지만 D23·#069~#071 이 들어오면 부족하다 | **`internal-design-doc`** — 외부 공식 근거 없음(프레임워크 lifecycle 정책) | 앱 **전체**의 lifecycle phase 순서(web server 를 먼저 멈춘다 등)는 [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] 소유다 — D26 은 그 안에서 **Redis 런타임 내부** 순서만 정한다. 두 순서가 모순되면(예: readiness off 시점) 그 branch 가 우선한다. 순서를 강제할 test 는 없다 — 단계를 건너뛰어도 현재는 검출되지 않는다 |
|
|
|
|
<!-- section-id: implementation -->
|
|
## 구현 가이드
|
|
|
|
> 3-rule meta principle 적용 — R1 각 sub-section 은 Decision ID + Claim ID reference, R2 근거 없는 detail 은 `UNSUPPORTED_IMPL_DECISION` + trade-off 한 줄, R3 본 branch 결정 범위 밖은 이관.
|
|
>
|
|
> 코드 위치는 ca-tmpl repo 기준 상대 경로다. **본 branch 는 계약 SSOT 이고 코드 작성은 ca-tmpl plan 이 소유한다** — 아래 클래스·경로 중 `src/` grep 으로 확인한 것만 `actually-implemented` 로 표기했고 나머지는 전부 `planned` 다.
|
|
>
|
|
> ⚠️ **ground truth 경로**: 실제 최신 ca-tmpl 은 `/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/` 다. `/home/donghyeon/workspace/ca-tmpl/` 은 2026-07-26 시점 사본이며 아래 Redis 클래스 대부분이 **없다** — §Audit `GROUND_TRUTH_PATH_DRIFT` 참조.
|
|
|
|
### 1. Role · deployment · binding 모델
|
|
|
|
> **Trace**: D1(role = physical deployment) · D2(role binding) · D3(co-location 거부) · D4(정책 소유 경계) · `REDIS-EVICT-C1`·`REDIS-EVICT-C2` · `REDIS-CLUSTER-C4` · `REDIS-ACL-C7`
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: **attestation 형식**(managed service 가 `CONFIG GET` 을 막았을 때 role policy 를 증명하는 문서의 서명 주체·형식·만료). 후보 — (a) 배포 저장소의 서명된 YAML, (b) provider 콘솔 스크린샷 + 승인자 기록, (c) 외부 conformance job 의 정기 리포트. trade-off — (a)는 검증 자동화가 쉽지만 실제 인스턴스 상태와 어긋나도 알 수 없고, (c)는 실측이지만 운영 인프라를 추가로 요구한다. **어느 쪽도 근거 없음** — 확정 전까지 attestation 부재는 "해당 role 의 guarantee 미주장"으로 처리한다(D4 의 3번째 분기).
|
|
|
|
| 항목 | 값 | 등급 |
|
|
|---|---|---|
|
|
| role 어휘 | `cache` · `coordination` · `session` (3 필수). rate-limit·stream 은 noisy-neighbor 발생 시 **추가** 분리 후보 | `planned` |
|
|
| role → deployment | `roles.<role>.deployment = <deploymentId>` — 각 role 은 deployment 정확히 1개를 참조 | `planned` |
|
|
| deployment 식별자 | 예시 `cache-main` / `coordination-main` / `session-main` (`internal-design-doc` §32.1 의 starter example — **production 이름 규약이 아님**) | `planned` |
|
|
| role 기본 eviction 기대값 | `cache` = `allkeys-lfu`(또는 검증된 `allkeys-lru`), `coordination`·`session` = `noeviction` (`REDIS-EVICT-C2` 가 `noeviction` write 실패 시맨틱의 근거) | `planned` |
|
|
| runtime 분리 단위 | deployment ID 마다 client · client resources/event loop · connection factory · credential/TLS material · metrics scope · lifecycle | `planned` |
|
|
| 전역 `@Primary` factory | **금지** — role-qualified factory 만 허용. Boot 의 Redis auto-configuration 은 배제하거나 조건을 좁혀 unqualified `RedisConnectionFactory` 가 생기지 않게 한다 | `planned` |
|
|
| as-built 격차 | role 개념이 **전혀 없다**. `RedisCacheAdapterConfig.redisStringCacheRegion()` 이 capability 를 `"cache"`, hv/kv 를 `1`/`1`, kind 를 `"entry"`, maximumKeyBytes 를 `512` 로 하드코딩 | `internal-code-fact`(부재·하드코딩 확인 — **구현됨이 아님**) |
|
|
|
|
**incompatible co-location 거부 규칙** (D3 — production startup fail):
|
|
|
|
| # | 거부 조합 | 왜 |
|
|
|---|---|---|
|
|
| 1 | evictable cache + session | 한 인스턴스는 `maxmemory-policy` 를 하나만 갖는다(`REDIS-EVICT-C1`). cache 가 eviction 을 유발하면 session 도 같은 정책 아래 놓인다 |
|
|
| 2 | evictable cache + idempotency | 동일. 중복 방지 record 가 임의 축출되면 계약이 무너진다 |
|
|
| 3 | evictable cache + fenced coordination | 동일. lease·fencing counter 축출은 정확성 손실 |
|
|
| 4 | replica-read cache + primary-only correctness capability | `REDIS-REPL-C1`·`REDIS-REPL-C3` — replica 는 비동기이고 유실 창이 있다 |
|
|
| 5 | 상호 모순된 persistence/eviction attestation | 같은 deployment 가 두 role 의 서로 다른 보장을 동시에 주장할 수 없다 |
|
|
|
|
local profile 은 `allow-unsafe-colocation=true` 를 **명시**할 때만 한 컨테이너를 공유하고 readiness 를 강등한다. 강등 등급의 어휘 owner 는 [[raw/branch-notes/feature-capability-provider-selection-contract]] D8 이다(재진술 아님, 포인터만).
|
|
|
|
**배포 소유 경계** (D4): 애플리케이션은 `CONFIG SET` 을 실행하지 않는다. `maxmemory` · eviction policy · AOF/RDB · replica · Sentinel/Cluster 구성 · TLS 종단 · backup 은 IaC/managed service 가 소유한다. `REDIS-ACL-C7` 이 `@admin`(= `CONFIG`·`DEBUG`·`SAVE`·`SHUTDOWN` 포함)을 "일반 애플리케이션은 쓸 일이 없다"로 서술하므로, D21 의 ACL 에서 `@admin` 을 부여하지 않는 것으로 이 경계를 **강제**한다.
|
|
|
|
### 2. Key model 계약
|
|
|
|
> **Trace**: D5(단일 builder·bounds) · D6(digest) · D7(hash tag) · D8(version 3축) · D9(대량 무효화) · `REDIS-CLUSTER-C1`·`C2`·`C3` · `REDIS-ACL-C7`
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: **`kind` allowlist 의 실제 값 집합**(`entry`/`bucket`/`record`/`owner` 외에 무엇을 허용할지). `internal-design-doc` §11.1 의 예시 4개는 *예시*이고 규범 목록이 아니다. trade-off — 좁게 잡으면 capability branch 마다 본 branch 를 고쳐야 하고, 넓게 잡으면 allowlist 가 검증 장치로서 의미를 잃는다. **capability branch(#067~#071)가 각자 kind 를 등록하는 개방형 registry** 가 절충안이나 근거는 없다.
|
|
|
|
canonical shape (`actually-implemented` — `adapter/outbound/cache-redis/.../redis/key/RedisKeyBuilder.java`):
|
|
|
|
```text
|
|
ca:<app>:<env>:<capability>:<region>:hv<hashKeyVersion>:kv<keyVersion>:{<slotTag>}:<resourceDigest>:<kind>
|
|
```
|
|
|
|
| 검증 항목 | as-built | 목표 |
|
|
|---|---|---|
|
|
| 전체 UTF-8 byte 길이 | ✅ `maximumKeyBytes` 초과 시 예외 (`1..4096`, cache 는 `512`) | 유지 |
|
|
| slug 문법 | ✅ `[a-z][a-z0-9-]{0,62}` (app·env·capability·region·kind) | 유지 |
|
|
| hv/kv 범위 | ✅ `1..9999`, namespace 와 digest 의 `hv` 일치 강제 | 유지 |
|
|
| digest 형식 | ✅ slotTag `[0-9a-f]{8}`, resourceDigest `[0-9a-f]{64}` | 유지 |
|
|
| hash tag 정확히 1개 | ❌ 없음 | `planned` |
|
|
| user input 의 `{`/`}` 유입 차단 | ❌ 없음 (digest 를 거치므로 현재는 구조적으로 불가하나 **명시 검증은 없음**) | `planned` |
|
|
| capability 별 kind allowlist | ❌ 없음 | `planned` (값 집합은 위 `UNSUPPORTED_IMPL_DECISION`) |
|
|
| builder 우회 금지 정적 강제 | ❌ 없음 | `planned` — ArchUnit rule 후보 |
|
|
|
|
**bounds 위반은 fail-open 대상이 아니다** (D5). 잘못된 key 입력은 backend outage 가 아니라 프로그래밍 결함이며, project note §11 의 "cache codec·프로그래밍 결함 = fail closed" 행에 해당한다(owner [[raw/branch-notes/feature-capability-provider-selection-contract]]).
|
|
|
|
**digest 선택** (D6 — **함수는 `actually-implemented`, 선택 배선은 `planned`**, `redis/key/RedisKeyDigest.java`). ⚠️ production call site 는 현재 **항상 `sensitive()`** 다(`RedisStringCacheRegion.physicalKey()`); `opaque()` 는 테스트에서만 호출된다 — 즉 "식별자 성격에 따라 고른다"는 규칙 자체는 아직 배선되지 않았다:
|
|
|
|
| 입력 성격 | 함수 | 알고리즘 |
|
|
|---|---|---|
|
|
| 이미 random opaque ID, 노출 위험 낮음 | `RedisKeyDigest.opaque(hv, components)` | SHA-256 |
|
|
| 사용자·tenant·email·IP 등 사전 공격 가능 | `RedisKeyDigest.sensitive(hv, secret, components)` | HMAC-SHA-256 (secret ≥ 32 byte) |
|
|
| composite scope | 위 둘 중 성격에 맞는 쪽 | 공통 `canonicalComponents()` = `putInt(len) + bytes` **length-prefixed** 인코딩 후 해싱 |
|
|
|
|
단순 문자열 delimiter join 은 ambiguity 때문에 금지한다 — as-built 가 이미 length-prefix 로 이를 배제한다. `slotTag` 는 digest hex 의 앞 8자다.
|
|
|
|
**미구현 (`planned`)**: HMAC key rotation. `internal-design-doc` §11.2 는 `dual-read-delete`(재생성 가능 data 만) / `cold-cutover`(idempotency·lease·strict rate) / conservative overlap(rate) / `fixed`(unkeyed digest) 4 mode 와 `RedisKeyDigestMaterialProvider` SPI 를 규정하지만 코드에 **없다**. 현재는 `app.cache.redis.key-hmac-secret` 단일 값 + `hv` 하드코딩 `1` 이므로 **secret 교체 = 전체 key 무효화**다.
|
|
|
|
**version 3축** (D8):
|
|
|
|
| 축 | 위치 | 바꾸면 |
|
|
|---|---|---|
|
|
| key schema version | key 의 `kv<n>` (`actually-implemented`, 값은 `1` 고정) | 물리 레이아웃·namespace |
|
|
| payload schema version | ⚠️ **envelope version byte 를 겸용 중** — codec 식별자와 payload version 이 분리되지 않았다. 별도 축은 `planned` (§구현 가이드 3 표 · §Audit A3) | 같은 key 의 decode 호환성 |
|
|
| policy revision | **없음** (`planned`) | state 해석(TTL·rate 파라미터). rate-limit key 에는 **포함**한다 |
|
|
|
|
**대량 무효화** (D9) — 요청 경로에서 pattern delete 금지. 선택지는 (1) key schema/version bump, (2) region generation ID 교체, (3) known-key bounded batch, (4) operator maintenance 의 rate-limited `SCAN`+`UNLINK`. (2)의 generation key 가 evict 되면 `0` 으로 되돌아가 옛 namespace 를 부활시키므로, missing 이면 새 random 128-bit generation 을 `SET NX` 로 초기화하고 loser 는 winner 값을 읽는다. 전부 `planned`.
|
|
|
|
### 3. Payload · envelope · codec
|
|
|
|
> **Trace**: D10(byte[] 원칙) · D11(envelope) · D12(호환성·손상) · D13(크기·압축) · D14(codec 분리) · `internal-code-fact`
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: **`CacheCodec<T>` SPI 의 최종 시그니처**. `internal-design-doc` §12.1 이 `schemaId()`/`writeVersion()`/`encode()`/`decode(storedVersion, payload)` 4-method 형태를 제시하지만 이는 설계 문서의 제안이며 외부 근거가 아니다. trade-off — `decode` 가 `DecodeResult<T>` 를 돌려주면 D12 의 `SCHEMA_MISMATCH`/`CORRUPT` 구분을 codec 층에서 표현할 수 있으나 sealed 타입이 하나 더 늘고, 예외로 표현하면 타입은 단순하지만 결함과 outage 를 호출부가 다시 구분해야 한다. **as-built(`RedisCacheEnvelopeCodec.Decoded` sealed)는 전자에 가깝다** — 정합만 보면 전자가 우세하나 결정 근거는 없다.
|
|
|
|
envelope 필드 — 목표 vs as-built:
|
|
|
|
| 필드 | 목표(`internal-design-doc` §12.2) | as-built (`redis/RedisCacheEnvelopeCodec.java`) |
|
|
|---|---|---|
|
|
| magic | 필수 | ✅ `0x43414348` |
|
|
| envelope version | 필수 | ✅ 1 byte, `VERSION = 1` |
|
|
| codec 식별자 | 필수 | ❌ 없음 |
|
|
| payload version | 필수(envelope version 과 별도 축) | ❌ envelope version 과 미분리 |
|
|
| flags(negative/compressed) | 필수 | ⚠️ negative 는 **type byte** 로 표현, compressed flag 없음 |
|
|
| source revision | optional | ✅ 1..128자 / ≤512 byte |
|
|
| writtenAt | 필수 | ❌ 없음 |
|
|
| soft expiry | optional | ❌ 없음 |
|
|
| **hard expiry** | 필수 | ❌ 없음 — **현재 만료 판정의 유일한 근거가 Redis TTL 이고 clock/TTL drift 를 관측할 수단이 없다** |
|
|
| payload length | 필수 | ✅ `int` |
|
|
| payload digest | 필수 | ✅ SHA-256 접미, 불일치 시 `CORRUPT_ENVELOPE` |
|
|
|
|
**결과 모델** (D12 — `actually-implemented`, `application-core/.../cache/CacheLookup.java`): `Hit` / `Miss` / `NegativeHit` / `IncompatibleSchema(SchemaCategory)` / `Unavailable(reason, certainty)` sealed 계층이 이미 miss 와 outage 와 결함을 분리한다. `SchemaCategory` 는 `FUTURE_VERSION` / `RETIRED_VERSION` / `CORRUPT_ENVELOPE` / `UNKNOWN_ENVELOPE`.
|
|
|
|
**서버 정책 거부 축** (D12·D3·D4·D21) — 결과 타입이 3개(`CacheLookup` / `CacheRecordOutcome` / `CacheInvalidationOutcome`)이므로 각각에 매핑한다. as-built 는 이 축을 **결과 모델 밖으로 내보낸다**(`LettuceRedisRuntime.execute()` 가 `RedisCommandExecutionException` 을 그대로 rethrow):
|
|
|
|
| 서버 응답 | read (`lookup`) | write (`record`) | invalidate |
|
|
|---|---|---|---|
|
|
| `OOM command not allowed` (`noeviction` 한도 초과) — **용량 사건, 프로그래밍 결함 아님** | `Unavailable(UNAVAILABLE, NOT_APPLIED)` | `DEGRADED_UNAVAILABLE` | `DEGRADED_UNAVAILABLE` |
|
|
| `NOPERM` (ACL 미부여) — **배선 결함, fail closed** | (아래 `UNSUPPORTED_IMPL_DECISION`) | (동일) | (동일) |
|
|
|
|
`REDIS-EVICT-C2` 가 OOM 이 **write 에서** 발생하고 read 는 정상 동작한다고 명시하므로, read 열은 실질적으로 Lua 경유 read(`EVAL`)가 write 로 분류될 때만 발생한다.
|
|
|
|
> **UNSUPPORTED_IMPL_DECISION**: **`NOPERM` 을 어떤 형태로 표현할지.** 세 enum(`CacheLookup` sealed / `CacheRecordOutcome` / `CacheInvalidationOutcome`) 어디에도 "배선 결함" 멤버가 없다 — 현재 값은 `RECORDED`·`NOT_RECORDED_CONDITION`·`NOT_RECORDED_PROVIDER_POLICY`·`DEGRADED_UNAVAILABLE`·`INDETERMINATE` 뿐이다. 후보 — (a) 신규 enum 멤버 추가, (b) `DEGRADED_UNAVAILABLE` 재사용, (c) 결과가 아니라 예외로 던져 상위에서 startup/배선 오류로 처리. trade-off — (b)는 타입 변경이 없지만 **용량 사건과 배선 결함을 합쳐** D12 의 대전제("결함을 outage 로 위장하지 않는다")를 스스로 위반하고, (a)는 정확하지만 `application-core` 의 provider-neutral enum 을 Redis 특성 때문에 넓히며, (c)는 fail-closed 의미와 맞지만 cache 경로에서 예외를 던지는 유일한 사례가 된다. **(a) 또는 (c) 가 D12 와 정합하나 결정 근거는 없다.**
|
|
|
|
**미구현 (`planned`)**: (a) schema/corrupt 축의 **별도 metric** — `metrics.yaml` 의 cache row 는 `cache.gets.total`·`cache.invalidations.total` 2개뿐이라 D12 가 요구하는 "miss 와 다른 metric"을 표현할 자리가 없다. (b) 손상 항목의 bounded owner-safe **quarantine/evict** — 현재는 `IncompatibleSchema` 를 돌려줄 뿐 키를 지우지 않는다. (c) N-1 dual-read 와 opportunistic rewrite.
|
|
|
|
**크기·압축** (D13):
|
|
|
|
| bound | as-built | 목표 |
|
|
|---|---|---|
|
|
| 최대 encoded bytes | ✅ `maximumValueBytes` (`1..16_777_216`, 기본 1 MiB) | region 단위로 분리 |
|
|
| 최대 decoded bytes | ❌ encoded 상한으로 대용 | 별도 상한 |
|
|
| 최대 collection element 수 | ❌ (collection API 부재) | 도입 시 필수 |
|
|
| 최대 압축비 | ❌ (압축 미구현) | 압축 도입 시 **먼저** 설정 |
|
|
| encode/decode deadline | ❌ | `planned` |
|
|
| wire bulk reply 상한 | ✅ bounded Lua `GETRANGE` 로 상한+1 byte 에서 `CA_VALUE_TOO_LARGE` (`LettuceRedisRuntime.get()`) | 유지 |
|
|
|
|
oversize 처리는 **role 별 비대칭**이다 — cache write 는 `REJECTED_TOO_LARGE` 로 만들되 source 결과 자체는 실패시키지 않고, session·idempotency response 는 해당 capability 계약에 따라 fail closed 한다. 압축은 임계 이상에서만 opt-in 하고, decompression bomb 를 막기 위해 decoded size 와 비율을 **먼저** 제한하며, secret 과 공격자 제어 값을 같은 압축 컨텍스트에 섞지 않는다.
|
|
|
|
**codec 분리** (D14): session(allowlist 된 attribute 스키마 + rolling 호환성) · idempotency(request fingerprint + bounded response codec) · rate/lease(고정 primitive 스키마)는 cache codec 을 재사용하지 않는다. 각 스키마의 실제 필드는 #069·#070·#071·#068 이 정한다 — 본 §는 **경계만** 소유한다.
|
|
|
|
### 4. Connection runtime · topology
|
|
|
|
> **Trace**: D15(topology sum type) · D16(timeout 7축) · D17(connection 형태) · D18(queue·replay) · D19(read routing) · `LETTUCE-CO-C1`~`C5` · `REDIS-CLUSTER-C4` · `REDIS-REPL-C1`~`C5`
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: **Spring Data Redis 도입 여부 — 본 §의 최상위 분기점.** `internal-design-doc` §31.1·§31.3 은 `spring-data-redis` + `lettuce-core` 를 기본 provider 로 규정하지만, **as-built 는 `lettuce-core` 단독이다** (`cache-redis/build.gradle` 에 `spring-data-redis` 없음, 코드도 `io.lettuce.core.RedisClient` 직접 사용). trade-off — Spring Data Redis 를 넣으면 Sentinel/Cluster 설정·Spring Session 통합·`RedisScript` 추상이 따라오지만 auto-configuration 배제 작업(§31.3)이 늘고 unqualified `RedisConnectionFactory` 위험이 생긴다. raw Lettuce 유지는 표면이 작지만 Sentinel/Cluster·Session 을 직접 배선해야 한다.
|
|
> **선택 조건(잠정)**: Sentinel/Cluster(D15) 또는 `redis-session` 모드(#069)를 R2 범위 **안**에 넣어야 하면 → Spring Data Redis. standalone-only 를 유지하고 session 을 JWT 로 고정하면 → raw Lettuce. 이 조건은 사용자 확정 전까지 *잠정*이며 근거는 없다.
|
|
> **파급**: 이 하나가 D15(topology 배선) · D17(connection/pool) · §10(property 구조) · §5(클라이언트 TLS 배선) · [[raw/branch-notes/feature-redis-atomic-program-catalog-contract]] D1·D2(`RedisScript`/`ScriptExecutor` 전제)를 전부 가른다 — §Audit `SPRING_DATA_REDIS_PREMISE_CONFLICT`. **확정 전까지 §4·D15·D17 착수 금지.**
|
|
|
|
**topology** (D15): deployment 하나는 `standalone` · `sentinel` · `cluster` 중 정확히 하나. host list 가 비었거나 두 필드가 동시에 설정되면 startup failure. binding model(concrete `@ConfigurationProperties`)과 검증된 runtime model(immutable sealed)을 분리하고, factory 가 discriminator 와 exactly-one nested property 를 검증한다. Cluster 는 database `0` 고정(`REDIS-CLUSTER-C4`). as-built 는 **standalone 전용**(`LettuceRedisRuntime.redisUri()` = `RedisURI.Builder.redis(host, port)`) — Sentinel·Cluster 는 `planned`.
|
|
|
|
**timeout 7축** (D16):
|
|
|
|
| 축 | as-built | 목표 |
|
|
|---|---|---|
|
|
| DNS/connect | ❌ | 유한 |
|
|
| TLS handshake | ❌ (TLS 자체 부재) | 유한 |
|
|
| pool acquire | ❌ | 유한 |
|
|
| command | ✅ `commandTimeout` (기본 2s, ≤30s) | 유지 |
|
|
| capability overall deadline | ❌ | `≤ caller deadline` |
|
|
| blocking command | ❌ | `< connection lifecycle timeout` |
|
|
| shutdown | ⚠️ **`commandTimeout` 을 재사용** (`LettuceRedisRuntime.shutdownTimeout`) | 별도 축으로 분리 |
|
|
|
|
> **UNSUPPORTED_IMPL_DECISION**: **6개 신규 축의 기본값과 property 이름.** 근거 문서(`internal-design-doc` §31.6)가 "exact default 는 workload SLO 로 조정한다"고 명시하므로 값을 **주지 않는다**. trade-off — 시작값을 박아두면 구현자가 되묻지 않지만 SLO 를 재지 않은 숫자가 관성으로 굳고, 비워두면 정직하지만 착수 시 6번 되묻는다. **절충으로 "관계 불변식만 확정하고 값은 비운다"를 택한다** — startup assertion 이 `connect/command/acquire 각각 유한`, `capability overall deadline ≤ caller deadline`, `lease wait + work budget < caller deadline`, `blocking timeout < connection lifecycle timeout` 을 검증하고, 값 자체는 배포별 override 로 둔다. property 이름은 §7 의 신규 축과 함께 `env-keys.yaml` 등록 시 확정한다.
|
|
|
|
**connection 형태** (D17): 일반 non-blocking command 는 thread-safe shared native connection. blocking `XREAD` · Pub/Sub · connection affinity 가 필요한 transaction · 장기 maintenance · stateful command mode 는 전용 connection/pool 을 쓰고, blocking operation 이 일반 cache/rate connection 을 점유하지 못하게 한다. 실질 제어 축은 connection 개수가 아니라 in-flight/queue bound 다 — `LETTUCE-CO-C3` 이 `requestQueueSize` 가 **per-connection** 임을, `LETTUCE-CO-C4` 가 Cluster 전체 상한이 `requestQueueSize * ((node 수 * 2) + 1)` 로 배수화됨을 명시한다. as-built 는 단일 connection(`planned`).
|
|
|
|
**queue · replay** (D18 — 본 §에서 **가장 근거가 강하고 as-built 정합도 가장 높은** 항목):
|
|
|
|
| 옵션 | Lettuce 기본값 | as-built (`LettuceRedisRuntime.clientOptions()`) | 근거 |
|
|
|---|---|---|---|
|
|
| `autoReconnect` | `true` + 큐 command 재발행 | `true` | `LETTUCE-CO-C1` |
|
|
| `disconnectedBehavior` | `DEFAULT`(autoReconnect 시 accept) | **`REJECT_COMMANDS`** | `LETTUCE-CO-C2` |
|
|
| `requestQueueSize` | `Integer.MAX_VALUE` (사실상 무제한) | **유한** (`maximumQueuedCommands`, `1..4096`, 기본 8) | `LETTUCE-CO-C3` |
|
|
| replay 억제 | 기본 at-least-once("commands may be duplicated but not lost") | `replayFilter(ignored -> true)` | `LETTUCE-CO-C5` (단, `replayFilter` semantics 자체는 **미인용** — §Audit) |
|
|
| admission | (없음) | `RedisCommandAdmission` count+byte 이중 semaphore, 포화 시 `OVERLOADED`/`NOT_APPLIED` | `internal-code-fact` |
|
|
|
|
mutation certainty 는 `RedisCommandFailureException.Certainty` 로 표현한다 — timeout·connection 실패 시 **mutation 은 `INDETERMINATE`, read 는 `NOT_APPLIED`** (`actually-implemented`). retry-safe 연산도 driver replay 가 아니라 capability wrapper 가 total deadline 안에서 새 invocation 으로 재시도한다.
|
|
|
|
**read routing** (D19): coordination·session·idempotency·lease·rate = primary only. cache = 기본 primary, stale 허용 region 만 replica opt-in 하고 region descriptor 에 "무효화 직후 옛 replica 값을 읽을 수 있음"을 적는다. `planned` — as-built 에 read preference 개념 없음.
|
|
|
|
### 5. Security — TLS · ACL
|
|
|
|
> **Trace**: D20(TLS) · D21(ACL) · `REDIS-TLS-C1`~`C6` · `REDIS-ACL-C1`~`C7`
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: **클라이언트 TLS 활성화 메커니즘 전반.** 아래 표는 8행 중 7행이 **서버** config 이고, 클라이언트 축(TLS 스킴 선택 `rediss://` vs `RedisURI.Builder.withSsl()`, trust material 주입 형태 keystore/truststore vs PEM, mutual TLS 용 client certificate 배선, hostname verification 토글, **production plaintext 거부를 어디서 판정하는가**)은 **하나도 정해지지 않았다**. `REDIS-TLS-C3` 가 "TLS 활성 시 기본 mutual TLS" 를 말하므로 client certificate 배선은 선택이 아니라 필수인데 명세가 없다. trade-off — Lettuce `SslOptions` 를 직접 쓰면 의존성이 늘지 않지만 keystore 조립을 직접 해야 하고, Spring Data Redis 의 SSL 지원에 기대면 배선은 짧아지나 §구현 가이드 4 의 `UNSUPPORTED_IMPL_DECISION`(Spring Data Redis 도입 여부)에 종속된다. **source 노트 자신이 "클라이언트 대응 설정은 별도 official-doc 확보 필요"라고 명시했다** — Lettuce `SslOptions`/`RedisURI` 공식 문서 수집 전까지 §5 의 클라이언트 축 전체가 근거 없음(§Sources "추가 수집 대상" 1). hostname verification 은 그중 한 항목일 뿐이다.
|
|
> **착수 조건**: 위 공식 문서 확보 전까지 **클라이언트 TLS 축 착수 금지**. 아래 서버 축 표(`port 0`·`tls-replication`·`tls-cluster`·이미지 TLS 빌드 확인)는 근거가 확보돼 있으므로 이 게이트와 무관하게 진행 가능하다.
|
|
|
|
**TLS** (D20) — production 요구:
|
|
|
|
| 항목 | 값 | 근거 |
|
|
|---|---|---|
|
|
| 서버 비-TLS 포트 | `port 0` 로 완전히 닫고 `tls-port` 만 | `REDIS-TLS-C2` |
|
|
| 클라이언트 인증 | Redis 는 TLS 활성 시 **기본 mutual TLS** 이고 신뢰 루트 CA 로 검증된 인증서를 요구. `tls-auth-clients no` 완화는 production 금지 | `REDIS-TLS-C3` |
|
|
| replication 채널 | `tls-replication yes` **별도** 필요 | `REDIS-TLS-C4` |
|
|
| Cluster bus | `tls-cluster yes` **별도** 필요 | `REDIS-TLS-C5` |
|
|
| 빌드 전제 | TLS 는 Redis 6+ 의 **컴파일 타임 옵션**이므로 이미지가 TLS 빌드인지 확인이 선행 | `REDIS-TLS-C1` |
|
|
| 성능 | TLS 는 인스턴스 처리량을 낮춘다 — capacity 산정에 반영 | `REDIS-TLS-C6` |
|
|
| Sentinel | discovery 채널과 data-node 채널의 credential·trust material 을 **각각** 표현하고 production 에서 둘 다 검증 | `internal-design-doc` §32.4 |
|
|
| as-built | **SSL 설정 0건** (`grep withSsl\|verifyPeer\|useSsl` → 0). 현재 plaintext + password 만 | `internal-code-fact`(부재 확인 — **TLS 는 미구현**) |
|
|
|
|
**ACL** (D21):
|
|
|
|
| 사용자 | 용도 | 부여 방식 |
|
|
|---|---|---|
|
|
| `cache-runtime` | cache role 의 data command | `reset` 후 필요한 category·key pattern 만 (`REDIS-ACL-C5`) |
|
|
| `coordination-runtime` | coordination role | 동일 |
|
|
| `session-runtime` | session role | 동일 |
|
|
| `program-deployer` | Function library load/list/delete — **release workflow 전용** (상세 owner 는 [[raw/branch-notes/feature-redis-atomic-program-catalog-contract]]) | 분리 계정 |
|
|
| `operator-readonly` | 운영 조회 | 분리 계정 |
|
|
|
|
**positive grant 초안** (`internal-code-fact` 도출 — as-built 가 실제로 쓰는 명령):
|
|
|
|
| user | 필요한 command | 도출 근거 |
|
|
|---|---|---|
|
|
| `cache-runtime` | `SET` · `DEL` · `EVAL` · `EVALSHA` + bounded-get Lua 내부의 `GETRANGE`·`EXISTS` | `LettuceRedisRuntime` 이 호출하는 전부 — `commands.set`/`commands.del`/`commands.eval`/`commands.evalsha`, 그리고 `BOUNDED_GET_SCRIPT` 본문의 `redis.call('GETRANGE', …)`·`redis.call('EXISTS', …)`. **읽기는 `GET` 이 아니라 Lua 경유다** |
|
|
| `coordination-runtime` · `session-runtime` | 미도출 — 해당 capability(#068~#071·#069) 미구현이라 호출 명령 집합이 아직 없다 | — |
|
|
| `program-deployer` | `FUNCTION LOAD`/`LIST`/`DELETE` 또는 `SCRIPT LOAD` — 상세는 [[raw/branch-notes/feature-redis-atomic-program-catalog-contract]] 소유 | 위임 |
|
|
| `operator-readonly` | 미도출 | — |
|
|
|
|
> **UNSUPPORTED_IMPL_DECISION**: **command 단위 grant vs category 단위 grant.** 위 표는 command 단위 도출이고, `REDIS-ACL-C4` 의 `+@<category>` 는 카테고리 단위다. trade-off — command 단위는 최소 권한이 정확하지만 코드가 새 명령을 쓰는 순간 조용히 `NOPERM` 이 나고(D12 의 서버 정책 거부 경로), category 단위는 안전 여유가 있지만 의도보다 넓다. **`REDIS-ACL-C7` 이 경고하는 `@admin` 회피는 어느 쪽이든 성립**하므로 tie-break 근거가 없다.
|
|
>
|
|
> **검증 필요**: Lua 안에서 실행되는 `GETRANGE`/`EXISTS` 가 **호출 user 의 ACL 을 따르는지**는 수집한 공식 문서로 확인되지 않았다 — §Claims To Verify 참조. 따르지 않는다면 `cache-runtime` 의 grant 집합이 달라진다.
|
|
|
|
- 새 user 는 기본적으로 **접근 가능한 key pattern 이 하나도 없다**(`REDIS-ACL-C3`) — 즉 allowlist 방식이 프레임워크 기본값과 정합한다.
|
|
- `@admin`(= `CONFIG`·`DEBUG`·`SAVE`·`MONITOR`·`ACL`·`SHUTDOWN` 등)은 어떤 runtime 계정에도 부여하지 않는다(`REDIS-ACL-C7`) — 이것이 D4 의 "애플리케이션은 `CONFIG SET` 을 실행하지 않는다"를 **강제하는 실제 장치**다.
|
|
- ACL key pattern(`~<pattern>`, `REDIS-ACL-C2`)과 애플리케이션 key builder 는 같은 versioned prefix registry 에서 생성되어야 하고, hash tag·user input 으로 prefix 를 탈출할 수 없어야 한다 — conformance test 필요(`planned`).
|
|
- ACL category 는 Redis 버전업으로 확장될 수 있으므로 allowlist + **negative** integration test 를 쓴다.
|
|
- default user 는 production 에서 비활성화한다. **as-built 는 ACL username 을 쓰지 않는다**(`redisUri()` 가 `withPassword` 만) — 현재 default user 접속(`planned`).
|
|
|
|
> **과대해석 금지**: `REDIS-ACL-C6` 은 공식 문서가 보안 목적과 운영 안전성 목적을 **병렬로** 제시한다는 사실만 지지한다. "ACL 은 보안 경계가 아니라 실수 방지용"이라는 이분법으로 인용하면 overclaim 이다.
|
|
|
|
### 6. Role health contributor
|
|
|
|
> **Trace**: D22 · `REDIS-REPL-C2` · `internal-design-doc` §35.1~§35.3
|
|
>
|
|
> - **R3 OUT_OF_BRANCH_SCOPE**: **어떤 indicator 가 readiness group 에 포함되는가**는 본 branch 가 정하지 않는다 — owner 는 [[raw/branch-notes/feature-capability-provider-selection-contract]] D7(`readinessImpact`)이다. 본 §는 *role health contributor 가 무엇을 확인하는가*만 소유한다.
|
|
|
|
`PING` 성공이 보장하지 **않는** 것: write 가능 · 올바른 primary · Cluster slot coverage · persistence 정상 · `noeviction` 여유 · 필요한 Function version · serializer/key 호환 · ACL command 권한. `REDIS-REPL-C2`(replica 가 초기 동기화 중에도 옛 데이터셋으로 응답 가능)가 "응답한다 ≠ 최신이다"의 공식 근거다.
|
|
|
|
| 계층 | Redis 의존 | 확인 항목 |
|
|
|---|---|---|
|
|
| liveness | **없음** — Redis 장애로 pod 를 반복 재시작하면 connection storm 과 failover 를 악화시킨다 | — |
|
|
| readiness (required role) | 있음 | connection/auth/TLS · topology/primary · 최소 read/write 능력 · program digest · 최근 성공/오류 예산 · queue 포화 · role 고유 요구 |
|
|
| optional cache | 있음(비차단) | **ready 유지 + `DEGRADED` 표시** |
|
|
| 미사용 role | 없음 | health check 자체를 만들지 않는다 |
|
|
|
|
전부 `planned` — 현재 코드에 Redis health contributor 는 **0개**다.
|
|
|
|
### 7. Credential 조달 · rotation
|
|
|
|
> **Trace**: D23 · D20 · D21 · `REDIS-ACL-C1` · `REDIS-TLS-C3` · `internal-design-doc` §34.8·§34.9
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: **SPI 를 `internal-design-doc` §34.8 형태 그대로 채택할지.** 제안형은 `RedisCredentialMaterialProvider.resolve(SecretReference)` + `subscribe(reference, listener)` 2-method 이고 값 타입은 `VersionedRedisCredentialMaterial(version, expiresAt, username, password, trustMaterial)` 이다. trade-off — 그대로 채택하면 D6 의 `RedisKeyDigestMaterialProvider` 와 대칭이 맞아 두 SPI 를 같은 bridge 로 조립할 수 있으나, username·password·trustMaterial 을 한 record 에 묶으면 trust material 만 회전하는 경우에도 전체를 재조달해야 한다. 분리하면 유연하지만 rotation coordinator 가 두 축을 동기화해야 한다. **어느 쪽도 외부 근거 없음** — 설계 문서의 제안일 뿐이다.
|
|
> **착수 조건**: 무중단 rotation 이 실제 요구사항인지 확인 전까지 **rotation coordinator 착수 금지**(§Claims To Verify). registry 는 이미 `restart-only` 를 선언했으므로 재시작 창이 허용되면 8단계가 통째로 불필요하다. **SPI 인터페이스 정의와 resolve 결과 5분류는 선행 가능** — 어느 분기에서도 필요하다.
|
|
|
|
| 항목 | 계약 | 등급 |
|
|
|---|---|---|
|
|
| 소유 위치 | **Redis leaf 가 SPI 와 immutable value 를 소유**한다. app-bootstrap 이 환경별 구현(Vault/file/Kubernetes/managed secret)을 조립하거나 generic secret capability 를 이 SPI 에 bridge 한다. **Redis leaf 가 bootstrap 이나 특정 secret vendor 에 역의존하지 않는다** | `planned` |
|
|
| resolve 결과 | `Resolved` / `TemporarilyUnavailable` / `Expired` / `InvalidReference` / `PermissionDenied` 를 **구분**한다 — 일시 장애와 배선 결함을 합치지 않는다(D12 와 같은 원칙) | `planned` |
|
|
| 값 수명 | version + expiry 를 갖고 사용 후 파기 가능한 byte/char 표현으로 전달 | `planned` |
|
|
| 누설 금지 | secret 값 · reference 전체 · provider 예외 메시지를 log/metric 에 남기지 않는다. ⚠️ 현행 fail-open logger 가 raw exception message 를 기록하는 경로(`adapter/outbound/support/FailOpenDependencyLogger`)는 endpoint·credential 누설 가능성을 검토해 classified sanitized field 만 남기도록 바꾼다 | `planned` |
|
|
| rotation 주체 | listener 는 새 version 을 **알릴 뿐** event thread 에서 client 를 바꾸지 않는다. role runtime 의 **직렬화된 rotation coordinator** 가 새 factory 검증 → traffic switch → old connection drain 을 수행 | `planned` |
|
|
| rotation 8단계 | new credential/ACL 추가 → client dual-valid overlap → 새 factory/session drain → 새 credential 연결·명령 test → traffic switch → old connection drain → old credential revoke → stale client alert | `planned` |
|
|
| role 격리 | 한 global connection 을 즉시 끊어 **모든 role 이 동시에 outage 되지 않도록** role 별로 수행 | `planned` |
|
|
| 안전망 | event 만 믿지 않고 만료 전 **bounded periodic re-resolve** 를 둔다. subscription loss · 중복/순서 뒤바뀐 event · resolve timeout · 만료 material · 부분 role rotation 을 test | `planned` |
|
|
|
|
⚠️ **registry 와의 충돌**: `secrets-classification.yaml` 은 `APP_CACHE_REDIS_PASSWORD` 를 `rotation_policy: restart-only`, `APP_CACHE_REDIS_KEY_HMAC_SECRET` 을 `dual-read-restart-only` 로 **이미 선언**했다 — 둘 다 "무중단 아님"이다. 위 8단계 무중단 rotation 은 그 두 행의 **변경을 요구**하며, 그 registry 의 owner 는 [[raw/branch-notes/feature-secrets-config-source-contract]] 다. 본 branch 는 단독으로 바꾸지 않는다(§Audit `SECRET_ROTATION_POLICY_DRIFT`).
|
|
|
|
### 8. Client · topology metric
|
|
|
|
> **Trace**: D24 · D18 · D15 · `LETTUCE-CO-C3`·`C4` · `REDIS-CLUSTER-C5`·`C6` · `internal-design-doc` §35.4·§35.10
|
|
>
|
|
> - **R3 OUT_OF_BRANCH_SCOPE**: **metric 행 등록 메커니즘과 cardinality bound 표**는 [[raw/branch-notes/feature-metrics-alerting-contract]] 소유다(`metrics.yaml` 의 `Owner branch:` 헤더). 본 §는 *어떤 축을 측정할지* 만 정하고 행 등록은 그 branch 의 change set 절차를 경유한다.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: **metric 이름 규약.** `internal-design-doc` §35.4 의 제안형은 `redis.capability.operations` / `.duration` / `.inflight` / `.queue.depth` / `.timeouts` / `.indeterminate` 인데, `metrics.yaml` 헤더의 규약은 "Micrometer dot.case + unit suffix (`.seconds` | `.bytes` | `.total`)" 다 — 제안형 6개 중 suffix 규약을 만족하는 것은 하나도 없다. trade-off — 설계 문서 이름을 그대로 쓰면 문서 간 grep 이 쉽지만 registry 규약 위반이고, 규약에 맞추면(`redis.capability.operations.total` 등) 설계 문서와 이름이 갈린다. **규약 준수가 우세하나 결정 근거는 없다.**
|
|
|
|
측정 축 (전부 `planned` — 현재 `metrics.yaml` 의 Redis 관련 행은 `cache.gets.total`·`cache.invalidations.total` **2개뿐**):
|
|
|
|
| 축 | 항목 | 왜 필요한가 |
|
|
|---|---|---|
|
|
| connection | connect / reconnect · connection age · TLS·auth 실패 | D18 의 reconnect 동작과 D20·D21 의 실패를 구분 |
|
|
| admission | queued / rejected command · pool acquire·saturation · in-flight | `LETTUCE-CO-C3`(queue 초과 시 `RedisException`)이 계측 지점. `LETTUCE-CO-C4` 때문에 Cluster 에서는 **노드 수 배수**로 봐야 한다 |
|
|
| topology | `MOVED` / `ASK` **각각** · topology refresh / failure / age · sentinel failover | `REDIS-CLUSTER-C5`(MOVED = 영구 재배치)와 `REDIS-CLUSTER-C6`(ASK = 일회성)은 **의미가 다르므로 합쳐 세지 않는다** |
|
|
| command | timeout · `INDETERMINATE` 발생률 | D18 의 certainty 분류가 실제로 얼마나 자주 불확정인지 |
|
|
| program | `NOSCRIPT` · function digest mismatch · `BUSY`/slow program | 소비자는 #062 |
|
|
| cache schema | schema mismatch · corrupt (D12 — **miss 와 별도**) | D12 가 요구하나 현재 담을 행이 없다 |
|
|
|
|
**tag 규칙**: `deployment` · `role` · `capability` · `operation` · `outcome` · `topology` 로 bounded. **endpoint · key · tenant · user · session · owner token 은 tag 가 아니다** — `metrics.yaml` 헤더의 전역 금지 태그(`user_id`·`request_id`·`raw_url`·`ip_address` 등)와 같은 취지다.
|
|
|
|
**server INFO 는 애플리케이션이 export 하지 않는다** — `used_memory` · `evicted_keys` · replication lag · Cluster state 등은 운영 모니터링(operator) 축이다. 애플리케이션이 server INFO 전체를 고cardinality metric 으로 무분별하게 내보내지 않는다.
|
|
|
|
### 9. Role durability baseline · startup·shutdown 순서
|
|
|
|
> **Trace**: D25(role 별 persistence·replication 기대치) · D26(순서 불변식) · D4(정책 소유 경계) · `REDIS-REPL-C3`·`C5` · `internal-design-doc` §28.7·§36.1·§36.2
|
|
>
|
|
> - **R3 OUT_OF_BRANCH_SCOPE**: 앱 **전체**의 lifecycle phase 순서와 liveness/readiness/startup 3-endpoint 분리는 [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] 소유다. 본 §는 Redis 런타임 **내부** 순서만 정한다.
|
|
|
|
**role 별 durability 기대치** (D25 — IaC 가 provisioning 해야 할 입력 스펙. 값은 배포가, claim 은 본 branch 가 소유):
|
|
|
|
| Role | Persistence | Replication | 선언 가능한 claim |
|
|
|---|---|---|---|
|
|
| `cache` | optional | optional / replica read 허용 | 재생성 가능, 유실 허용 |
|
|
| `coordination` | 명시적 AOF · RPO | primary + replica | 저지연 상태. **유실은 여전히 가능** |
|
|
| `session` | 제품 RPO 에 맞는 AOF · HA | primary + replica | 세션 연속성은 best effort, 복구는 재인증 |
|
|
|
|
> ⛔ **금지 문구**: "AOF + replica = 절대 유실 없음". `REDIS-REPL-C3`("...there is always a window for data loss")와 `REDIS-REPL-C5`(`WAIT` 도 CP 를 주지 않음)가 이를 직접 부정한다. capability card·문서·면접 답변 어디에서도 이 표현을 쓰지 않는다.
|
|
|
|
**startup 순서** (D26 — 전부 `planned`. as-built 는 `RedisCacheAdapterConfig` 의 bean 생성 순서에 암묵 의존):
|
|
|
|
1. typed config 검증 (D15 topology exactly-one · D16 timeout 관계 · D3 co-location)
|
|
2. secret material 조달 (D23)
|
|
3. client resources 생성
|
|
4. topology / connect / auth (D20 TLS · D21 ACL)
|
|
5. program / schema capability (#062)
|
|
6. role attestation (D4 · D25)
|
|
7. provider binding (#060)
|
|
8. health / readiness 등록 (D22)
|
|
9. background refresh · watchdog · consumer 시작
|
|
|
|
> **background task 를 connection 검증 전에 시작하지 않는다** — 4단계 이전에 9단계가 돌면 검증 실패한 연결로 백그라운드가 먼저 트래픽을 만든다.
|
|
|
|
**graceful shutdown 순서** (D26 — 전부 `planned`):
|
|
|
|
1. readiness off / 신규 트래픽 drain
|
|
2. 신규 cache refresh · rate background 작업 중단
|
|
3. 신규 lease · idempotency long operation 중단
|
|
4. in-flight operation bounded wait
|
|
5. owner-safe lease release (best effort)
|
|
6. session save 완료
|
|
7. Pub/Sub · stream listener stop
|
|
8. 전용 connection / pool close
|
|
9. shared client resources close
|
|
|
|
> **release 응답이 없다고 key 를 blind delete 하지 않는다** — 5단계의 실패는 timeout 이지 소유권 상실이 아니다(D18 의 `INDETERMINATE` 와 같은 원칙).
|
|
>
|
|
> as-built 는 `LettuceRedisRuntime.close()` 가 `connection.close()` → `client.shutdown(Duration.ZERO, shutdownTimeout)` **2단계뿐**이고 Spring `@Bean(destroyMethod = "close")` 에 위임한다. background 작업·lease·session 이 아직 없어 현재는 충분하지만, D23 과 #069~#071 이 들어오면 부족하다.
|
|
|
|
**금지 명령 · 크기 없는 collection 연산** (D9·D13 확장): regular runtime 에서 `KEYS`, unbounded `HGETALL`·`SMEMBERS`·`LRANGE 0 -1`·`ZRANGE`, `COUNT`/deadline 없는 `XREAD` 를 금지한다. **모든 collection operation 은 최대 결과 개수와 byte budget 을 갖는다.** `SCAN` 은 maintenance/admin 경로 전용이며 bounded `COUNT` · rate limit · cancellation/deadline 을 갖고, 결과는 중복·누락이 있을 수 있으며 mutation 중 정확한 스냅샷이 아니다 — request handler 의 wildcard 무효화에 쓰지 않는다. 현재 as-built 는 collection API 자체가 없어 위반 여지가 없다(`RedisBinaryCommands` 는 `get`/`set`/`delete`/`eval`/`evalSha` 5개뿐).
|
|
|
|
**TTL sentinel 해석** (D5·D11 확장, `planned`): expirable capability key 에서 `TTL = -1` 은 **corruption/policy 위반**(만료 없는 key 가 생긴 것)이고 `TTL = -2` 는 **absent** 다. `-1` 을 "영구 성공"으로 두지 않고 capability 별 repair/quarantine + alert 를 수행한다. `Duration` → millisecond 변환 시 overflow · zero truncation · negative · provider 최대 · policy 최대를 검증하며 **`Duration` 을 `int` millisecond 로 축소하지 않는다**.
|
|
|
|
**test 계약**: 일반 test taxonomy(unit/contract/integration 분리, focused 레벨 Testcontainers 금지)는 [[raw/branch-notes/feature-test-taxonomy-fixture-contract]] 소유이며 본 branch 는 그 계약의 소비자다. 본 branch 고유 물리층 test 는 key builder property test(hash tag 1개 불변식) · codec contract test(N/N-1 dual-read) · Sentinel/Cluster/TLS/ACL integration · `redisServiceTest` lane 의 no-silent-skip 이며, 형제 [[raw/branch-notes/feature-redis-atomic-program-catalog-contract]] 의 §구현 가이드 "테스트 계약" 패턴을 따른다.
|
|
|
|
### 10. Configuration 이행 (as-built → 목표)
|
|
|
|
> **Trace**: D2 · D15 · D16 · D20 · D21 · `internal-code-fact`
|
|
>
|
|
> - **R3 OUT_OF_BRANCH_SCOPE**: **legacy 키 병행 메커니즘**과 **활성화 축(provider/mode/boolean) 통일**은 [[raw/branch-notes/feature-capability-provider-selection-contract]] D2·D3·D14 가 owner 다. 본 §는 *Redis 런타임 property 의 형태*만 다루고 활성화 규약을 재정의하지 않는다.
|
|
|
|
as-built prefix 는 `app.cache.redis.*` 이고 **단일 standalone deployment 를 가정**한다. 목표는 `ca-skeleton.providers.redis.deployments.<id>` + `roles.<role>` 구조다(`internal-design-doc` §32.1).
|
|
|
|
as-built env key (전부 `actually-implemented`, `docs/registries/env-keys.yaml` 등록됨):
|
|
|
|
| env key | 기본값 | 비고 |
|
|
|---|---|---|
|
|
| `APP_CACHE_REDIS_ENABLED` | `false` | 활성화 게이트 |
|
|
| `APP_CACHE_REDIS_CLIENT_MODE` | `managed` | `managed`(모듈 소유 Lettuce) \| `external`(프로젝트 제공 `RedisClient`) |
|
|
| `APP_CACHE_REDIS_HOST` / `_PORT` | — / `6379` | managed 활성 시 host 누락은 **startup fail-fast** (`localhost` 로 숨기지 않음) |
|
|
| `APP_CACHE_REDIS_PASSWORD` | — | `secrets-classification.yaml` 에 `secret`/`secret-manager` 등록 |
|
|
| `APP_CACHE_REDIS_KEY_HMAC_SECRET` | — | Base64, 디코드 ≥32 byte |
|
|
| `APP_CACHE_REDIS_COMMAND_TIMEOUT` | `2s` | ≤30s |
|
|
| `APP_CACHE_REDIS_MAXIMUM_QUEUED_COMMANDS` | `8` | `1..4096` |
|
|
| `APP_CACHE_REDIS_MAXIMUM_IN_FLIGHT_BYTES` | `16777216` | ≤256 MiB, `maximumValueBytes + 1024` 이상 |
|
|
| `APP_CACHE_REDIS_NAMESPACE_ENVIRONMENT` | `local` | key 의 `<env>` |
|
|
| `APP_CACHE_REDIS_SEMANTIC_REGION` | `default` | key 의 `<region>` |
|
|
| `APP_CACHE_REDIS_MAXIMUM_VALUE_BYTES` | `1048576` | `1..16 MiB` |
|
|
|
|
**신규 제안(registry 미등록)**: role/deployment 축(`roles.*`, `deployments.*`), TLS 축, ACL username 축, topology 축, timeout 5축, read preference. 이들은 `env-keys.yaml` 에 **행이 없으므로** 기존 값처럼 단정하지 않는다 — 도입 시 §32.7 의 한 change set(typed property + `application.yml` placeholder + env registry + `.env` example + binding/validation test + secret classification + 문서) 규율을 따른다.
|
|
|
|
> ⚠️ **registry owner drift**: `APP_CACHE_REDIS_CLIENT_MODE` 의 `owner_branch` 는 `redis-production-capability`, `KEY_HMAC_SECRET`·`MAXIMUM_QUEUED_COMMANDS`·`MAXIMUM_IN_FLIGHT_BYTES` 등은 `codex-phase-a-ci-recovery` 다 — **둘 다 llm-wiki 에 존재하지 않는 branch-note slug** 다. §Audit `ENV_KEY_OWNER_BRANCH_DRIFT` 참조.
|
|
|
|
<!-- section-id: edge-failure-dependency -->
|
|
## 엣지·실패·의존
|
|
|
|
- **실패·엣지 경로**
|
|
- **disconnected 상태의 command** — `REJECT_COMMANDS` + 유한 queue 로 즉시 거부한다. 이걸 놓치면 Lettuce 기본값(`Integer.MAX_VALUE` queue, `LETTUCE-CO-C3`)이 Redis 장애를 애플리케이션 heap 고갈로 바꾼다(D18)
|
|
- **reconnect 후 자동 replay** — 기본 동작이 at-least-once 라 "중복될 수 있다"(`LETTUCE-CO-C5`). `INCR`/token consume/idempotency claim/lease acquire 는 dedup·owner token 없이 자동 재전송되면 안 된다(D18)
|
|
- **timeout·cancel 후의 mutation 결과 불명** — 이미 서버에서 실행됐을 수 있다. as-built 는 mutation 을 `INDETERMINATE`, read 를 `NOT_APPLIED` 로 분류한다(`RedisCommandFailureException.Certainty`). 이걸 `FAILED` 로 단정하면 상위 capability 가 잘못된 보상 동작을 한다
|
|
- **admission 포화** — count 또는 byte semaphore 가 차면 `OVERLOADED`/`NOT_APPLIED`. cache 는 fail-open miss 로, correctness capability 는 fail closed 로 갈린다(정책 owner 는 project note §11)
|
|
- **oversized 외부 value** — 다른 writer 가 상한을 넘는 값을 넣어둔 경우. bounded Lua `GETRANGE` 가 상한+1 byte 에서 `CA_VALUE_TOO_LARGE` 를 던지고 `IncompatibleSchema` 로 격리한다 — **miss 로 위장하지 않는다**(D12, `actually-implemented`)
|
|
- **envelope digest 불일치·미래 version** — `CORRUPT_ENVELOPE` / `FUTURE_VERSION` 으로 분리. 프로그래밍 결함을 Redis unavailable 로 분류하지 않는다(D12). 다만 **손상 키 quarantine 과 별도 metric 은 미구현**이라 현재는 매 조회마다 같은 손상 키를 다시 읽는다
|
|
- **서버 정책 거부 (`OOM command not allowed` · `NOPERM`)** — 본 branch 결정이 **새로 만들어내는** 실패 경로다(D3·D4 가 `noeviction` 을 요구하고 D21 이 ACL 을 켜기 때문). ⚠️ as-built 는 이 경로를 결과 모델 **밖으로 내보낸다** — `LettuceRedisRuntime.execute()` 가 `RedisCommandExecutionException` 을 그대로 rethrow 하고(`catch (RedisCommandExecutionException e) { throw e; }`), `RedisStringCacheRegion` 은 `RedisCommandFailureException` 만 잡으므로 OOM·NOPERM 이 `CacheLookup`·`CacheRecordOutcome` 어디에도 매핑되지 않고 호출부로 raw 전파된다. 계약(D12): **OOM → `Unavailable(UNAVAILABLE, NOT_APPLIED)`**(용량 사건, 프로그래밍 결함 아님) / **`NOPERM` → 배선 결함으로 fail closed**(`IncompatibleSchema` 로 접지 않는다). `execute()` 의 `RedisCommandExecutionException` 분기 신설이 `planned`
|
|
- **`noeviction` role 의 OOM** — write 가 에러를 반환한다(`REDIS-EVICT-C2`). read 가 된다고 healthy 가 아니다 — session create/touch, idempotency claim/complete, lease acquire 가 각자 fail closed 로 갈린다
|
|
- **resharding 중 `MOVED`/`ASK`** — `MOVED` 는 slot 이 **영구 재배치**됐다는 뜻이라 클라이언트가 topology 를 갱신해야 하고(`REDIS-CLUSTER-C5`), `ASK` 는 **그 한 번의 query 만** 지정 노드로 보내고 이후는 계속 이전 노드로 보내는 일회성 리다이렉션이다(`REDIS-CLUSTER-C6`). 둘을 같게 처리하면 topology 가 갱신되지 않거나 반대로 과잉 갱신된다. 처리 주체는 클라이언트 라이브러리이며 **그 동작의 벤더 근거는 미확보**(§Audit A7) — topology refresh 지연 중에는 리다이렉션이 지속된다
|
|
- **evictable role 에 correctness 데이터가 섞임** — D3 의 co-location 거부가 유일한 방어선이다. 검증이 없으면 조용히 통과하고, 사고는 eviction 이 일어난 뒤에야 드러난다
|
|
- **replica read 의 stale 값** — 무효화 직후에도 옛 값을 읽을 수 있다(`REDIS-REPL-C1`·`C2`). primary-only 로 바꿔도 **failover 유실 창은 남는다**(`REDIS-REPL-C3`·`C5`)
|
|
- **Cluster 전환 시 slot 위반** — 원자 연산의 key 들이 다른 slot 이면 실패한다(`REDIS-CLUSTER-C3`). as-built 의 slotTag 는 digest 앞 8자라 **co-location 을 표현할 수 없다** — Cluster 진입 시점에 D7 미구현이 바로 문제가 된다
|
|
- **HMAC secret 교체** — 현재 version 축이 없어 secret 을 바꾸면 전체 key 가 무효화된다(D6). rotation mode 도입 전까지 secret 교체는 cache 전면 miss 를 의미한다
|
|
- **TLS 이미지 전제** — Redis TLS 는 컴파일 타임 옵션이다(`REDIS-TLS-C1`). 이미지가 TLS 빌드가 아니면 설정만으로는 켜지지 않는다
|
|
- **다른 계약 의존** (대상 브랜치 + 그 Decision ID)
|
|
- [[raw/branch-notes/feature-capability-provider-selection-contract]] (`WI-…-060`, **선행 필수**) — 활성화 SSOT(D2·D3), typed descriptor(D5), readiness 등급 R0~R3(D8), capability card(D9·D15), 실패 정책 매트릭스 파생(D12), `readinessImpact` → health group(D7), startup 실패 어휘(D10)를 그 branch 가 소유한다. 본 branch 의 D3(co-location 거부)·D22(health contributor)·§구현 가이드 10 은 그 계약의 **소비자**다. 그 branch 의 D8·D9 가 아직 `UNSUPPORTED_DECISION` 이므로 "readiness R1 강등" 표현은 확정값이 아니다
|
|
- [[raw/branch-notes/feature-redis-atomic-program-catalog-contract]] (`WI-…-062`, **본 branch 완료 후 착수**) — key model·codec 이 program descriptor 의 입력이다(D5·D6·D8·D11). ⚠️ 그 branch 의 D1·D2 는 **Spring Data Redis 의 `RedisScript`/`ScriptExecutor` 를 전제**하지만 as-built 는 `lettuce-core` 단독이다 — §Audit `SPRING_DATA_REDIS_PREMISE_CONFLICT`. 본 branch 가 raw Lettuce 유지로 확정하면 그 D1·D2 는 재작성 대상이다
|
|
- [[raw/branch-notes/feature-cache-strategy-registry-contract]] (`WI-…-067`) — TTL 4종·single-flight·SWR/stampede·전략 registry 는 그 branch 소유. 본 branch 는 **물리 형식**(key·envelope·크기 상한)만 소유하고 `maxmemory-policy` 의 배포 소유 경계(D4)를 제공한다
|
|
- [[raw/branch-notes/feature-session-auth-mode-contract]] (`WI-…-069`) · [[raw/branch-notes/feature-idempotency-ownership-protocol-contract]] (`WI-…-070`) · [[raw/branch-notes/feature-fenced-lock-coordination-contract]] (`WI-…-071`) · [[raw/branch-notes/feature-edge-rate-limit-policy-registry-contract]] (`WI-…-068`) — 네 branch 모두 D1 의 role 배정(session → session role, 나머지 → coordination role)과 D14 의 "cache codec 재사용 금지"를 상속한다. 각 codec 의 **실제 필드**는 그 branch 들이 정한다
|
|
- [[raw/branch-notes/feature-cachestore-multi-backend-router]] · [[raw/branch-notes/feature-cache-consistency-contract]] — 현행 `CacheStoreRouter`/`FailOpenCacheStore`/`app.cache.bindings.*` 의 owner. 본 branch 의 `CacheRegionPort` 경로와 **두 개의 cache 진입점이 공존**한다(`RedisCacheStore`(String) vs `RedisStringCacheRegion`(envelope)) — 정리 시점은 #067 의 주도권 이전과 묶인다
|
|
- [[raw/branch-notes/feature-env-driven-runtime-configuration]] — `APP_*` env registry 가 SSOT 이고 `verifyEnvKeys` 가 3-way drift 를 검사한다. §구현 가이드 10 의 신규 축은 registry 등록이 **선행**돼야 한다
|
|
- [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] **D10**(`Required vs Optional Dependency Matrix`) · **D9**(liveness/readiness/startup 의미 분리) · **D4**(graceful shutdown timeout sync) — `Required vs Optional Dependency Matrix` 를 그 branch 가 소유하고, 그 표의 Redis 행이 `startup validation = ping` · `unavailable → degraded ready (cache-aside fallback)` 로 **이미 확정**돼 있다. **D22 는 그 행의 `ping` 을 부정**하므로 startup validation 열을 refine 하는 관계다(`degraded ready` 는 일치). liveness/readiness/startup 3-endpoint 분리도 그 branch 소유 — §Audit `HEALTH_MATRIX_OWNER_CONFLICT`
|
|
- [[raw/branch-notes/feature-secrets-config-source-contract]] **D3**(secret reload 기본값 = no runtime reload, rotation = restart validation) · **D4**(3단계 classification) · **D6**(무중단이 필요한 secret 은 dual-bind) — `secrets-classification.yaml` 의 owner. `APP_CACHE_REDIS_PASSWORD`(`restart-only`)·`APP_CACHE_REDIS_KEY_HMAC_SECRET`(`dual-read-restart-only`) 두 행이 이미 "무중단 아님"을 선언했다. **D23 의 8단계 무중단 rotation 과 D6 의 rotation mode 는 그 두 행의 변경을 요구**한다 — §Audit `SECRET_ROTATION_POLICY_DRIFT`
|
|
- [[raw/branch-notes/feature-metrics-alerting-contract]] **D2**(Micrometer dot.case + unit suffix 명명 — D24 의 `redis.capability.*` 제안형이 이 규약을 만족하지 않는다) · **D8**(cardinality bounds — D24 의 tag 집합이 따라야 할 SSOT) · **D9**(registry timer 행 규약) — `metrics.yaml` 행과 cardinality bound 표의 owner. **D24 의 측정 축은 그 branch 의 change set 절차를 경유**해야 등록된다. 현재 Redis 관련 행은 2개뿐이라 D12·D24 가 요구하는 축을 담을 자리가 없다
|
|
- [[raw/branch-notes/feature-integration-adapter-templates]] **D1**(optional adapter = disabled-default module) · **D2**(`@ConditionalOnProperty` 로 bean 등록 제어 — as-built `RedisCacheAdapterConfig` 가 이 패턴) · **D9**(required/optional 분류 owner 와 fail-open/closed 정책 owner 분리) — `APP_CACHE_REDIS_ENABLED` 의 `owner_branch` 이고 "Redis unavailable → cache-miss graceful degrade(fail-open)" 정책 행을 소유한다. §구현 가이드 9 의 prefix 재구조화가 그 branch 소유 키를 건드리며, cache fail-open 정책은 project note §11 · #060 D12 · 이 branch 3곳이 후보라 **단일 owner 확정이 필요**하다(§Claims To Verify)
|
|
|
|
## Audit & Findings
|
|
|
|
> `/branch-spec`(2026-07-28) 이 ca-tmpl 코드·registry·설계 문서를 대조하며 발견한 **drift**. 본 branch 가 단독으로 고칠 수 없는 항목은 권고만 남기고 owner 를 명시한다.
|
|
|
|
| # | 라벨 | 발견 | 근거 | 권고 owner |
|
|
|---|---|---|---|---|
|
|
| A1 | `GROUND_TRUTH_PATH_DRIFT` | `/branch-spec` 명령 문서와 llm-wiki 다수 문서가 ca-tmpl 을 `/home/donghyeon/workspace/ca-tmpl/` 로 가리키지만, **실제 최신 저장소는 `/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/`** 다. 전자에는 `LettuceRedisRuntime`·`RedisKeyBuilder`·`RedisProgramCatalog` 등 Redis 클래스가 **하나도 없고**(cache-redis 가 9개 파일 seam 상태), `2026-07-26-redis-production-capability-design.md` 자체도 없다 | `find`/`grep` 양쪽 실행 결과. 후자만 cache-redis 51개 파일(test 13) 보유 | 이미 `docs/superpowers/specs/2026-07-28-...-decomposition-design.md` §13 D2 가 같은 사실을 기록했으나 **명령 문서·`.claude/commands/branch-spec.md` 는 미갱신**. 사용자/harness 소유 |
|
|
| A2 | `SPRING_DATA_REDIS_PREMISE_CONFLICT` | `internal-design-doc` §31.1·§31.3 과 project note §34 optional stack 표는 Redis client 를 "Lettuce (**Spring Data Redis**)" 로 규정하지만, **as-built 는 `io.lettuce:lettuce-core` 단독**이고 코드도 `io.lettuce.core.RedisClient` 를 직접 쓴다 | `src/adapter/outbound/cache-redis/build.gradle` — 의존성 4개 중 `spring-data-redis` **없음**. `LettuceRedisRuntime` 이 raw Lettuce API 사용 | **[[raw/branch-notes/feature-redis-atomic-program-catalog-contract]] D1·D2 가 이 전제 위에 서 있다** — 그 branch 의 §Claims To Verify 가 이미 "#061 이 raw 드라이버만 쓰기로 확정하면 D1·D2 가 무너진다"고 기록했다. 본 branch §구현 가이드 4 의 `UNSUPPORTED_IMPL_DECISION` 으로 남기고 사용자 확정 대기 |
|
|
| A3 | `ENVELOPE_FIELD_GAP` | as-built envelope 는 목표 필드의 절반이다 — codec 식별자·payload version(별도 축)·compressed flag·writtenAt·soft expiry·**hard expiry** 가 없다 | `redis/RedisCacheEnvelopeCodec.java` 의 `CONTENT_HEADER_BYTES` 구성 vs `internal-design-doc` §12.2 | 본 branch(D11). 특히 hard expiry 부재로 **clock/TTL drift 를 관측할 수단이 현재 0** |
|
|
| A4 | `ENV_KEY_OWNER_BRANCH_DRIFT` | Redis 런타임 env key 의 `owner_branch` 가 llm-wiki 에 **존재하지 않는 slug** 를 가리킨다 — `APP_CACHE_REDIS_CLIENT_MODE` → `redis-production-capability`, `KEY_HMAC_SECRET`·`MAXIMUM_QUEUED_COMMANDS`·`MAXIMUM_IN_FLIGHT_BYTES` → `codex-phase-a-ci-recovery`. 나머지 host/port/TTL 은 `feature-cache-consistency-contract` 소유 | `docs/registries/env-keys.yaml` L1245·L1295·L1319·L1331 | 본 branch 가 Redis **런타임** key 의 owner 로 이관 제안. 다만 registry 변경 절차는 [[raw/branch-notes/feature-contract-registry-governance]] 소관이므로 여기서 단독 변경하지 않는다 |
|
|
| A5 | `CACHE_ENTRYPOINT_DUPLICATION` | cache 진입점이 **두 개** 공존한다 — `RedisCacheStore`(`CacheBackend`, plain String, `app.cache.bindings.*` 라우팅)와 `RedisStringCacheRegion`(`CacheRegionPort`, envelope·TTL·negative). 둘 다 `app.cache.redis.enabled=true` 에서 동시에 bean 이 된다 | `RedisCacheAdapterConfig` 가 `redisCacheBackend` 와 `redisStringCacheRegion` 을 **둘 다** 정의 | [[raw/branch-notes/feature-cache-strategy-registry-contract]] 의 주도권 이전(H3·H4)과 묶어 정리. 본 branch 는 물리 형식 owner 로서 **envelope 경로가 정본**임을 선언만 한다 |
|
|
| A6 | `HUB_FILE_COUNT_STALE` | project note §F-CAP 는 `adapter/outbound/cache-redis` 를 "43개 파일(test 13)" 로 적었으나 실제(build 제외)는 **51개 파일, test 13** 이다 | `find src/adapter/outbound/cache-redis -type f -not -path '*/build/*' \| wc -l` → 51 | 저위험. project note owner 가 다음 갱신 시 정정 |
|
|
| A7 | `LETTUCE_TOPOLOGY_REFRESH_UNSOURCED` / `LETTUCE_REPLAY_FILTER_UNSOURCED` | Lettuce 공식 페이지 수집에서 **(a) Cluster topology refresh(periodic/adaptive) 설정**과 **(b) `replayFilter` semantics** 의 verbatim 인용을 확보하지 못했다. as-built 는 `replayFilter(ignored -> true)` 를 쓰고 있으나 그 API 가 그렇게 동작한다는 벤더 진술이 없다 | `raw/official-docs/client-options-disconnected-behavior-queue-lettuce-official.md` 의 Usage Boundaries — 요청 항목 중 topology refresh 미발견, replay filter 는 paraphrase 만이라 폐기 | 추가 수집 필요(§Sources). 그때까지 D15 의 topology refresh 와 D18 의 replay 억제는 **메커니즘 근거 없음** |
|
|
| A8 | `TLS_CLIENT_AXIS_UNSOURCED` (구 `TLS_HOSTNAME_VERIFICATION_UNSOURCED`) | Redis 서버 TLS 공식 문서에 "hostname verification" 이 **0회** 등장한다 (클라이언트 책임이라 서버 문서의 주제가 아님). 더 넓게는 **클라이언트 TLS 축 전체**(스킴 선택·trust material 주입 형태·client certificate 배선·plaintext 거부 판정 지점)에 근거가 없다 | `raw/official-docs/redis-tls-encryption-in-transit.md` 의 self-grep 부수 확인 + 그 노트의 "클라이언트 대응 설정은 별도 official-doc 확보 필요" 진술 | §구현 가이드 5 의 `UNSUPPORTED_IMPL_DECISION` + 착수 금지 게이트. Lettuce `SslOptions`/`RedisURI` 공식 문서 수집 시 승급 |
|
|
| A9 | `HEALTH_MATRIX_OWNER_CONFLICT` | [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] D10 의 `Required vs Optional Dependency Matrix` 가 Redis 행을 `startup validation = ping` 으로 **이미 확정**했는데, 본 branch D22 는 "`PING` 성공은 health 가 아니다"로 그 열을 **부정**한다. `unavailable → degraded ready` 부분은 두 노트가 일치한다 | `raw/branch-notes/feature-runtime-health-lifecycle-contract.md` 의 해당 표 Redis 행 + 그 노트의 "dependency taxonomy 표의 owner 는 본 branch" 선언 | **사용자 확정 필요** — (a) 그 표의 Redis 행 startup validation 열을 본 branch 가 refine 하는 것으로 두고 그 노트에 역참조를 넣을지, (b) Redis 행 자체의 owner 를 본 branch 로 이관할지. 확정 전까지 D22 는 `refines` 관계로 표기 |
|
|
| A10 | `SECRET_ROTATION_POLICY_DRIFT` | `secrets-classification.yaml` 이 `APP_CACHE_REDIS_PASSWORD` → `restart-only`, `APP_CACHE_REDIS_KEY_HMAC_SECRET` → `dual-read-restart-only` 로 **이미 선언**(둘 다 무중단 아님). 본 branch D23(8단계 무중단 rotation)·D6(rotation mode 4종)이 그 두 행의 변경을 요구한다. [[raw/branch-notes/feature-secrets-config-source-contract]] D3 도 "secret reload 기본값 = no runtime reload" 로 같은 방향이다 | ca-tmpl `docs/registries/secrets-classification.yaml` 의 두 행. HMAC 행의 `owner_branch` 는 `codex-phase-a-ci-recovery`(A4 와 같은 유형의 무효 slug) | registry owner 는 secrets branch. 본 branch 는 **요구만** 기록하고 단독 변경하지 않는다. D23 의 선택 조건이 "재시작 창 허용 시 registry 현행 값 유지" 분기를 이미 포함한다 |
|
|
| A11 | `METRIC_NAMING_CONVENTION_CONFLICT` | `internal-design-doc` §35.4 의 metric 이름 제안형 6개(`redis.capability.operations`/`.duration`/`.inflight`/`.queue.depth`/`.timeouts`/`.indeterminate`) 중 [[raw/branch-notes/feature-metrics-alerting-contract]] D2 의 "dot.case + unit suffix(`.seconds`\|`.bytes`\|`.total`)" 규약을 만족하는 것이 **0개**다 | `metrics.yaml` 헤더의 Naming 주석 + 설계 문서 §35.4 목록 | D24 의 Open Risk 로 기록. 이름 확정은 metrics branch 와 합의 후 — 규약 준수(`redis.capability.operations.total` 등)가 우세하나 결정 근거는 없다 |
|
|
|
|
<!-- section-id: claims-to-verify -->
|
|
## 검증해야 할 주장 / Claims To Verify
|
|
|
|
| Claim | Why uncertain | How to verify | Status |
|
|
|---|---|---|---|
|
|
| 본 branch 가 소유할 관심사가 sibling branch 결정과 겹치지 않는다 | D-row 22개가 #060·#062·#067~#071 과 인접한다. 특히 D22(health contributor) vs #060 D7(`readinessImpact`), D3(강등) vs #060 D8(등급 어휘) | `/sync` 실행 — owner 중복·재진술 검출 | `needs-confirmation` |
|
|
| Spring Data Redis 를 도입하지 않아도 Sentinel·Cluster·Spring Session 을 배선할 수 있다 | as-built 는 raw Lettuce 단독이고(A2) 설계 문서는 Spring Data Redis 를 전제한다. 두 경로의 비용 차이를 실측한 적이 없다 | Sentinel 최소 배선을 raw Lettuce 로 spike → 코드량·auto-config 배제 작업량 비교 | `needs-confirmation` |
|
|
| `replayFilter(ignored -> true)` 가 실제로 pending replay 를 억제한다 | 벤더 verbatim 근거 미확보(A7). 현재 BOM 이 고른 Lettuce 버전의 semantics 를 코드가 가정하고 있다 | reconnect integration test — 연결 절단 중 mutation 을 큐에 넣고 재연결 후 **재전송되지 않음**을 확인. `redisServiceTest` lane(`redis-service` 태그) 활용 | `needs-confirmation` |
|
|
| co-location 거부 5조합이 production startup 을 실제로 막는다 | 검증 로직이 **아직 없다**(D3 전부 `planned`). 규칙만 문서에 있으면 조용히 통과한다 | 5조합 각각에 대한 negative contract test + local `allow-unsafe-colocation` 경로의 readiness 강등 test | `planned` |
|
|
| slot tag 가 원자 연산의 key group 을 실제로 같은 slot 에 모은다 | as-built 의 slotTag 는 digest 앞 8자라 **분산만 된다**(D7 미구현). Cluster 전환 시 `CROSSSLOT` 실패로 드러난다 | Cluster 통합 test — 같은 태그 성공 / 다른 slot multi-key 는 expected failure. key builder property test 로 "hash tag 정확히 1개" 불변식 검증 | `planned` |
|
|
| HMAC secret 을 교체해도 서비스가 계속된다 | rotation mode·version 축이 없어 현재는 secret 교체 = 전체 key 무효화(D6) | rotation mode 도입 후 dual-read 창에서 old `hv` hit → new key refresh 를 확인하는 test | `planned` |
|
|
| envelope 에 hard expiry 를 넣으면 clock drift 를 관측할 수 있다 | 현재 필드가 없어(A3) TTL 이 만료의 유일한 근거다. drift 가 실제로 얼마나 나는지 측정한 적 없다 | hard expiry 추가 후 `writtenAt` 대비 실제 TTL 잔량을 metric 으로 노출하고 real Redis lane 에서 관측 | `planned` |
|
|
| 손상 entry 를 quarantine 하지 않아도 안전하다 | 현재 `IncompatibleSchema` 를 돌려줄 뿐 키를 지우지 않아 **매 조회마다 같은 손상 키를 다시 읽는다** | 손상 값 주입 후 반복 조회 → source 호출 횟수와 metric 을 확인. 허용 가능하면 문서화, 아니면 quarantine 구현 | `needs-confirmation` |
|
|
| `@admin` 미부여만으로 `CONFIG SET` 경로가 실제로 막힌다 | ACL 이 아직 미적용(default user 접속)이고, ACL category 는 Redis 버전업으로 확장될 수 있다(D21) | negative integration test — runtime 계정으로 `CONFIG SET`·`FLUSHALL`·`KEYS` 시도가 거부되는지 | `planned` |
|
|
| Lua 안에서 실행되는 `GETRANGE`/`EXISTS` 가 **호출 user 의 ACL** 을 따른다 | 수집한 ACL 공식 문서(`REDIS-ACL-C1`~`C7`)에 script 내부 명령의 ACL 적용 서술이 **없다**. 따르지 않으면 §구현 가이드 5 의 `cache-runtime` grant 집합이 달라진다 | Redis script/ACL 공식 문서 추가 수집 + `redisServiceTest` lane 에서 `+eval` 만 부여한 user 로 bounded-get 호출 → 성공/`NOPERM` 관측 | `needs-confirmation` |
|
|
| Redis health 판정의 SSOT 가 한 곳이다 | [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] 의 dependency matrix Redis 행(`ping`)과 본 branch D22(`PING` ≠ health)가 **충돌**한다(§Audit A9). 두 노트가 각자 판정 방식을 정의하면 구현자가 어느 쪽을 따를지 알 수 없다 | `/sync` 로 두 노트 대조 + 사용자가 refine/이관 중 하나를 확정 | `needs-confirmation` |
|
|
| cache fail-open 정책의 owner 가 한 곳이다 | 후보가 3곳이다 — project note §11 매트릭스, [[raw/branch-notes/feature-capability-provider-selection-contract]] D12(매트릭스 파생), [[raw/branch-notes/feature-integration-adapter-templates]](`APP_CACHE_REDIS_ENABLED` owner + "Redis unavailable → graceful degrade" 행) | `/sync` 로 3자 대조 → 단일 owner 확정 후 나머지는 포인터로 축약 | `needs-confirmation` |
|
|
| 무중단 credential rotation 이 실제로 필요하다 | registry 는 이미 `restart-only`/`dual-read-restart-only` 로 "재시작 허용"을 선언했다(§Audit A10). 무중단이 요구사항인지 설계 문서의 이상론인지 확인되지 않았다 | 배포 방식(rolling deploy 주기·재시작 허용 창)을 사용자에게 확인 → 필요 없으면 D23 의 8단계를 registry 값에 맞춰 축소 | `needs-confirmation` |
|
|
| `redis.capability.*` metric 이름이 registry 규약과 정합한다 | `metrics.yaml` 헤더는 "dot.case + unit suffix(`.seconds`\|`.bytes`\|`.total`)" 를 요구하는데 설계 문서 제안형 6개 중 만족하는 것이 **0개**다(D24) | [[raw/branch-notes/feature-metrics-alerting-contract]] 와 이름 규약 합의 후 `metrics.yaml` 행 등록 | `planned` |
|
|
|
|
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
|
|
|
> `/coverage` 2회차(2026-07-28) 반영. governing: [[raw/project-notes/ca-skeleton-operational-contract]] §25 Owner Map "Redis role·연결·키·코덱" 행 + 분해 설계 §4.2 #2. 2회차는 `internal-design-doc`(ca-tmpl redis 설계)의 replication·memory·time·lifecycle·test 절까지 대조 범위를 넓혔다.
|
|
> 1회차 `missing` 2건(credential SPI · client/topology metric) → **D23·D24 로 편입**(2회차에서 "진짜 결정"으로 검증됨).
|
|
> 2회차 `missing` 2건(role 별 persistence/replication baseline · startup·shutdown 순서) → **D25·D26 + §구현 가이드 9 로 편입**.
|
|
> ⚠️ 이 표는 2회차 결과에 편입분을 반영한 것이며 **3회차 `/coverage` 로 재검증되지 않았다**(루프 천장 2회 — 다음 세션에서 재실행).
|
|
|
|
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
|
|--------|------|-------|--------|------|
|
|
| 3-role 분리 · role binding · co-location 거부 · 배포정책 소유경계 | covered-here | — | — | D1~D4 |
|
|
| key model — canonical shape · digest · hash tag · version 3축 · bounds · 대량 무효화 | covered-here | — | — | D5~D9 |
|
|
| payload — `byte[]` 원칙 · codec SPI · envelope · schema/손상 분리 · 크기·압축 · codec 분리 | covered-here | — | — | D10~D14 |
|
|
| topology 배타 sum type | covered-here | — | — | D15 |
|
|
| topology refresh (periodic/adaptive) | covered-here (근거 미확보) | — | ⚪ Advisory | D15 Open Risk + §Audit A7 |
|
|
| timeout 7축 · connection 형태 · queue·replay | covered-here | — | — | D16~D18 |
|
|
| read routing (primary only vs replica opt-in) | covered-here | — | — | D19 |
|
|
| TLS (서버 축) | covered-here | — | — | D20 |
|
|
| TLS (클라이언트 축 — 스킴·trust material·client cert·plaintext 거부 판정) | covered-here (근거 미확보) | — | ⚪ Advisory | §구현 가이드 5 `UNSUPPORTED_IMPL_DECISION` + §Audit A8 |
|
|
| ACL least-privilege | covered-here | — | — | D21 |
|
|
| role 별 health contributor | covered-here | — | — | D22 (⚠️ owner 충돌 §Audit A9) |
|
|
| **연결 credential 조달 SPI · rotation 프로토콜** | covered-here | — | — | **D23 + §구현 가이드 7** (1회차 `missing` → 편입, 2회차 검증 완료) |
|
|
| **client·topology 런타임 metric** | covered-here | — | — | **D24 + §구현 가이드 8** (1회차 `missing` → 편입, 2회차 검증 완료. ⚠️ 이름 규약 충돌 §Audit A11) |
|
|
| **role 별 persistence·replication baseline** | covered-here | — | — | **D25 + §구현 가이드 9** (2회차 `missing` → 편입, **3회차 미검증**) |
|
|
| **Redis 런타임 startup·graceful shutdown 순서** | covered-here | — | — | **D26 + §구현 가이드 9** (2회차 `missing` → 편입, **3회차 미검증**) |
|
|
| 위험 collection 연산 일반 금지 (unbounded `HGETALL`·`SMEMBERS`·`LRANGE`·`ZRANGE`·`XREAD`) · `SCAN` 규율 | covered-here | — | — | §구현 가이드 9 (D9·D13 확장) |
|
|
| TTL sentinel(`-1`/`-2`) 해석 · `Duration`→ms 변환 bound | covered-here | — | — | §구현 가이드 9 (D5·D11 확장) |
|
|
| 일반 test taxonomy (unit/contract/integration 분리, Testcontainers 정책) | delegated | [[raw/branch-notes/feature-test-taxonomy-fixture-contract]] | OK | §구현 가이드 9 "test 계약" |
|
|
| 네트워크 격리 (VPC·private endpoint·egress allowlist) | delegated | IaC/배포 소유 | OK | §범위 제외 (D4 와 동일 성격) |
|
|
| 관리 작업 audit log | delegated | [[raw/branch-notes/feature-operational-error-observability-foundation]] | OK | §범위 제외 |
|
|
| capability 별 **의미** 계약 (TTL·stampede·rate 알고리즘·session·idempotency·fenced lock) | delegated | #067 · #068 · #069 · #070 · #071 | OK | §범위 제외 + §엣지 의존 |
|
|
| Lua/Function 카탈로그 · descriptor · `NOSCRIPT` 재적재 | delegated | [[raw/branch-notes/feature-redis-atomic-program-catalog-contract]] | OK | §범위 제외 (⚠️ §Audit A2 전제 충돌) |
|
|
| 활성화 SSOT · descriptor · readiness 등급 · capability card · 실패정책 파생 | delegated | [[raw/branch-notes/feature-capability-provider-selection-contract]] | OK | §범위 제외 + `depends_on` |
|
|
| cache 진입점 이원화 정리 | delegated | [[raw/branch-notes/feature-cache-strategy-registry-contract]] | OK | §Audit A5 |
|
|
| registry 스키마 · owner 이관 절차 | delegated | [[raw/branch-notes/feature-contract-registry-governance]] | OK | §범위 제외 (§Audit A4·A10 은 권고만) |
|
|
| metric 행 등록 메커니즘 · cardinality bound | delegated | [[raw/branch-notes/feature-metrics-alerting-contract]] | OK | §구현 가이드 8 R3 + §엣지 의존 |
|
|
| secret 분류 · rotation policy registry | delegated | [[raw/branch-notes/feature-secrets-config-source-contract]] | OK | §Audit A10 + §엣지 의존 |
|
|
|
|
## 마주친 문제
|
|
|
|
아직 없음.
|
|
|
|
## 묶음 (이 branch에서 파생된 자료)
|
|
|
|
<!-- GENERATED: branches:start -->
|
|
<!-- GENERATED: branches:end -->
|
|
|
|
## 관련 일일 노트
|
|
|
|
해당 없음.
|
|
|
|
## 완료 후 정리
|
|
|
|
- PR 링크:
|
|
- 리뷰 메모:
|
|
- 머지 결과 / 배포 환경:
|
|
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
|
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|