131 KiB
title, source_type, status, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, id, kind, project, work_item, inherits, refines, overrides, depends_on, imports, delegates, accepts_delegations, contract_packet
| title | source_type | status | branch | parent_branch | related_projects | governing_docs | tags | created | target_merge | status_label | id | kind | project | work_item | inherits | refines | overrides | depends_on | imports | delegates | accepts_delegations | contract_packet | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| branch / feature-redis-runtime-role-isolation-contract | branch-note | raw | feature-redis-runtime-role-isolation-contract |
|
|
|
2026-07-28 | in-progress | BR-CA-SKELETON-OPERATIONAL-CONTRACT-061 | project-work-item | ca-skeleton-operational-contract | WI-CA-SKELETON-OPERATIONAL-CONTRACT-061 |
|
|
|
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로 채운다.
부모 (필수)
- 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.0WI-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
브랜치 계약 패킷
project Work Item 에서 내려온 실행 계약의 snapshot. 여기에는 pinned pointer + 1줄 요약 + branch 적용점만 쓰고 상세를 복제하지 않는다.
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: 3-role 연결·키·코덱·health 계약 test 와 incompatible co-location 거부 test 가 통과한다
상속한 프로젝트 결정
| 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 |
브랜치 지역 결정
상세 근거와 선택 조건은 아래
결정-근거 매핑절의 동일 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 |
선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|
가져온 프로젝트 계약
| Ref | Owner | 요약 | Branch 적용 |
|---|
목표
-
WI-CA-SKELETON-OPERATIONAL-CONTRACT-061의 완료 조건을 구현한다: 3-role 연결·키·코덱·health 계약 test 와 incompatible co-location 거부 test 가 통과한다 -
이슈:
-
PR:
범위
포함 범위
- 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 의 직접 원인이다):
- Lettuce / Spring Data Redis 의 SSL 옵션 — hostname verification. Redis 서버 TLS 문서에는 이 개념이 0회 등장한다(§Audit A8) → D20 의 해당 부분 승급용
- Lettuce Cluster topology refresh (periodic / adaptive 설정) — 이번 회차 fetch 에서 verbatim 미발견(§Audit A7) → D15 승급용
- Lettuce
replayFiltersemantics — 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(대안 AF) · §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
/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종 +
RedisKeyDigestMaterialProviderSPI (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 RedisCredentialMaterialProviderSPI + 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_*의 직접 원인): LettuceSslOptions/RedisURITLS · Lettuce Cluster topology refresh · LettucereplayFiltersemantics · Lua 내부 명령의 ACL 적용 — 등급:needs-confirmation /depth+/coverage3회차 재실행 (본 세션은 루프 천장 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) 현행RedisClientseam 에 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 로 삼지 않는다" 와 충돌한다.
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-tmpldocs/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 는 없다 — 단계를 건너뛰어도 현재는 검출되지 않는다 |
구현 가이드
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 클래스 대부분이 없다 — §AuditGROUND_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:
kindallowlist 의 실제 값 집합(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):
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.Decodedsealed)는 전자에 가깝다 — 정합만 보면 전자가 우세하나 결정 근거는 없다.
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(CacheLookupsealed /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-C1C5·REDIS-CLUSTER-C4·REDIS-REPL-C1C5
- 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)이 늘고 unqualifiedRedisConnectionFactory위험이 생긴다. 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전제)를 전부 가른다 — §AuditSPRING_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-C1C6·REDIS-ACL-C1C7
- UNSUPPORTED_IMPL_DECISION: 클라이언트 TLS 활성화 메커니즘 전반. 아래 표는 8행 중 7행이 서버 config 이고, 클라이언트 축(TLS 스킴 선택
rediss://vsRedisURI.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 — LettuceSslOptions를 직접 쓰면 의존성이 늘지 않지만 keystore 조립을 직접 해야 하고, Spring Data Redis 의 SSL 지원에 기대면 배선은 짧아지나 §구현 가이드 4 의UNSUPPORTED_IMPL_DECISION(Spring Data Redis 도입 여부)에 종속된다. source 노트 자신이 "클라이언트 대응 설정은 별도 official-doc 확보 필요"라고 명시했다 — LettuceSslOptions/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 생성 순서에 암묵 의존):
- typed config 검증 (D15 topology exactly-one · D16 timeout 관계 · D3 co-location)
- secret material 조달 (D23)
- client resources 생성
- topology / connect / auth (D20 TLS · D21 ACL)
- program / schema capability (#062)
- role attestation (D4 · D25)
- provider binding (#060)
- health / readiness 등록 (D22)
- background refresh · watchdog · consumer 시작
background task 를 connection 검증 전에 시작하지 않는다 — 4단계 이전에 9단계가 돌면 검증 실패한 연결로 백그라운드가 먼저 트래픽을 만든다.
graceful shutdown 순서 (D26 — 전부 planned):
- readiness off / 신규 트래픽 drain
- 신규 cache refresh · rate background 작업 중단
- 신규 lease · idempotency long operation 중단
- in-flight operation bounded wait
- owner-safe lease release (best effort)
- session save 완료
- Pub/Sub · stream listener stop
- 전용 connection / pool close
- 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 다. §AuditENV_KEY_OWNER_BRANCH_DRIFT참조.
엣지·실패·의존
- 실패·엣지 경로
- disconnected 상태의 command —
REJECT_COMMANDS+ 유한 queue 로 즉시 거부한다. 이걸 놓치면 Lettuce 기본값(Integer.MAX_VALUEqueue,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 noevictionrole 의 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 빌드가 아니면 설정만으로는 켜지지 않는다
- disconnected 상태의 command —
- 다른 계약 의존 (대상 브랜치 + 그 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단독이다 — §AuditSPRING_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) vsRedisStringCacheRegion(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 소유 — §AuditHEALTH_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 는 그 두 행의 변경을 요구한다 — §AuditSECRET_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-builtRedisCacheAdapterConfig가 이 패턴) · 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)
- raw/branch-notes/feature-capability-provider-selection-contract (
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 등)가 우세하나 결정 근거는 없다 |
검증해야 할 주장 / 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 자동 생성 — 있을 때)
/coverage2회차(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회차missing2건(credential SPI · client/topology metric) → D23·D24 로 편입(2회차에서 "진짜 결정"으로 검증됨). 2회차missing2건(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에서 파생된 자료)
관련 일일 노트
해당 없음.
완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- wiki 추출 대상 (verified만,
wiki/projects/로만 추출): - 추출하지 않을 항목 (planned / documented-only / abandoned):