# Redis를 켠다는 말의 운영적 의미: 단일 활성화 스위치에서 Sentinel 쓰기 손실 검증까지 > **Redis 코드 상세 시리즈 20/20** · [전체 지도](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-backend-policy-boundary.md) · 이전: [Redis 테스트가 증명하는 것과 증명하지 않는 것](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-testing-topology-ci.md) Redis를 애플리케이션에 붙이는 일은 호스트와 비밀번호를 설정하는 것으로 끝나지 않습니다. 캐시는 Redis가 잠시 끊겨도 원본 저장소로 우회할 수 있지만, 세션·멱등성·요청 제한·분산 lease는 같은 장애를 전혀 다르게 해석해야 합니다. Sentinel은 primary를 승격해 가용성을 회복하지만, 교체된 primary가 자신이 교체됐다는 사실을 늦게 알아차리면 이미 성공으로 응답한 쓰기가 사라질 수 있습니다. Cluster에서는 여러 키가 같은 slot에 있어야 하고, blocking 명령과 일반 명령을 한 connection pool에 섞으면 한 종류의 부하가 전체 요청을 멈출 수 있습니다. 이 글은 `clean-architecture-backend-template`의 Redis 모듈을 플랫폼·SRE 관점에서 해부합니다. 핵심 질문은 “어떤 Redis 명령을 제공하는가”보다 다음에 가깝습니다. - Redis를 쓰지 않는 배포는 Redis 설정과 리소스에서 정말 자유로운가? - Redis를 쓰는 역할은 무엇이며, 장애 시 pod를 계속 서비스에 남겨도 되는가? - 잘못된 topology, credential, TLS, ACL, timeout, capacity 설정은 언제 실패하는가? - timeout과 failover 뒤 쓰기를 안전하게 재시도할 수 있는가? - 실서버 검증과 CI 행렬이 실제로 무엇을 증명하며, 무엇은 아직 증명하지 못했는가? - 이 저장소를 운영 배포 템플릿으로 쓰려면 어떤 공백을 별도로 메워야 하는가? ## 먼저 구분할 세 가지 근거 이 글은 근거의 강도를 섞지 않습니다. 1. **현 HEAD 확인**은 커밋 `3b5aee50e33c44c02d08c94bb39ad34814482010`의 코드, 설정, 테스트, Compose, workflow를 직접 읽어 확인한 내용입니다. root 작업 세션에서 `:adapter:outbound:cache-redis:test` 기본 테스트는 성공했습니다. 이 task는 `redis-topology` 태그를 제외하며, Standalone·Sentinel·Cluster·TLS topology lane은 실행하지 않았습니다. 2. **저장소의 과거 실측 기록**은 `docs/redis/`와 테스트 주석에 남아 있는 이전 실서버 실행 결과입니다. 수치와 결론을 그대로 구분해 인용하지만, 이번 세션에서 재현했다고 주장하지 않습니다. 3. **워크플로 정의**는 GitHub Actions가 어떤 행렬을 실행하도록 작성됐는지를 뜻합니다. 행렬에 Redis 7.2·7.4·8.2가 들어 있다는 사실만으로 모든 조합이 통과했다고 보지 않습니다. 이 구분은 특히 버전 지원과 Sentinel 쓰기 손실을 읽을 때 중요합니다. 저장소 문서 사이에도 시점 차이가 있기 때문입니다. ## 현재 기술 기준선과 문서 드리프트 현 HEAD의 빌드 기준선은 다음과 같습니다. | 항목 | 현 HEAD 값 | 근거 | | --- | --- | --- | | Java | 21 | [`src/build.gradle`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/build.gradle:281) | | Gradle | 9.0.0 | [`gradle-wrapper.properties`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/gradle/wrapper/gradle-wrapper.properties:3) | | Spring Boot | 4.0.0 | [`src/build.gradle`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/build.gradle:12) | | Lettuce | `6.8.1.RELEASE` | [`gradle.lockfile`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/gradle.lockfile:44) | | Reactor | 3.8.0 | [`gradle.lockfile`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/gradle.lockfile:56) | | Netty | 4.2.17.Final | [`src/build.gradle`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/build.gradle:410) | | 최소 Redis 버전 | 7.2.0 | [`RedisCapabilityProbe.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisCapabilityProbe.java:58) | Redis leaf는 Spring Data Redis를 사용하지 않고 Lettuce와 Reactor를 직접 의존합니다. typed API, command catalog, admission guard를 우회하는 범용 command surface를 만들지 않으려는 선택입니다. Micrometer core도 leaf에서 제외하고 관측 이벤트를 composition root 쪽으로 내보냅니다. 자세한 의존성 의도는 [`cache-redis/build.gradle`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/build.gradle:6)에 적혀 있습니다. 여기서 첫 번째 드리프트가 보입니다. [`support-matrix.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/redis/support-matrix.md:21)는 Lettuce를 6.8.2로 고정했다고 쓰지만 실제 lock은 `6.8.1.RELEASE`입니다. 운영 기준선은 문서의 설명보다 lockfile을 우선해야 합니다. 업그레이드 검토에서도 “문서상 버전”이 아니라 dependency lock diff를 출발점으로 삼아야 합니다. ## 1. 전역 스위치는 하나이고, 역할 선택기는 그 아래에 있습니다 이 구조의 가장 중요한 정책은 `APP_REDIS_ENABLED`가 유일한 전역 activation switch라는 점입니다. 기본값은 `false`입니다. ```yaml app: redis: enabled: ${APP_REDIS_ENABLED:false} ``` 전역 스위치가 꺼져 있으면 Redis 설정을 바인딩하지 않습니다. cross-field validation, credential 해석, raw policy와 TLS material 읽기, client·connection·thread·health contributor 생성도 하지 않습니다. Redis를 사용하지 않는 배포가 잘못된 Redis 설정 때문에 시작에 실패하지 않게 한 것입니다. 이 동작은 [`application.yml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/resources/application.yml:586)과 [`RedisSdkAutoConfiguration.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkAutoConfiguration.java:32)에서 확인할 수 있습니다. 역할 selector는 Redis 자체를 켜는 스위치가 아닙니다. 어떤 application port를 Redis 구현으로 조립할지 정합니다. | 역할 | selector와 Redis 값 | 기본값 | 장애 분류 | 현재 조립 상태 | | --- | --- | --- | --- | --- | | cache | `ca-skeleton.capabilities.cache.bindings.default=redis` | `disabled` | optional, 성능 저하 | `RedisCacheRegionAdapter` 조립 | | session | `ca-skeleton.security.auth-mode=redis-session` | `jwt` | correctness predicate에 포함 | Redis repository와 최초 인증 mechanism이 없어 선택 불가 | | idempotency | `ca-skeleton.capabilities.idempotency.provider=redis` | `jdbc` | correctness | owner·operation-aware V2 store와 executor 조립; same-attempt 중복 실행·renew 공백 존재 | | rate limit | `ca-skeleton.capabilities.rate-limit.provider=redis` | `disabled` | correctness | `fail-closed` 정책만 지원 | | lease | `ca-skeleton.capabilities.lease.provider=redis` | `disabled` | readiness상 correctness | adapter는 efficiency-only이며 fencing을 제공하지 않음 | 현 HEAD에서 실제 semantic provider가 조립되는 역할은 cache, idempotency, rate limit, lease의 **4/5**입니다. `redis-session`은 selector가 존재하더라도 `redisVersionedSessionRepository` producer가 없고, snapshot이 없는 요청에서 인증된 `Authentication` 객체를 최초로 만드는 production mechanism도 확인되지 않아 사용할 수 없습니다. 기본 selector와 세부 정책은 [`application.yml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/resources/application.yml:323), selector 전체 목록은 [`RedisActivationValidator.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/RedisActivationValidator.java:26)에서 확인할 수 있습니다. 전역 스위치가 `false`인데 역할 하나가 Redis를 선택하면 startup validator가 모순된 selector를 모두 모아 한 번에 실패시킵니다. 역할 selector가 Redis를 암묵적으로 켜지도 않고, missing bean 오류가 첫 요청까지 밀리지도 않습니다. ```text APP_REDIS_ENABLED=false APP_IDEMPOTENCY_PROVIDER=redis ``` 위 조합은 “idempotency bean이 없다”가 아니라 “Redis가 꺼졌지만 idempotency가 Redis를 선택했다”는 설정 오류로 시작 단계에서 종료됩니다. ### cache와 correctness 역할을 다르게 다루는 이유 cache가 끊기면 보통 원본 저장소를 더 많이 읽어 응답이 느려집니다. 이때 pod를 readiness에서 제거하면 남은 pod의 부하가 커져 장애를 악화시킬 수 있습니다. 반면 idempotency가 사라지면 같은 결제가 재처리될 수 있고, rate limit이 사라지면 quota를 강제하지 못하며, session이 사라지면 인증 상태의 정합성이 무너집니다. 따라서 코드는 cache를 optional로, session·idempotency·rate-limit·lease를 correctness로 분류합니다. 기준과 selector는 [`RedisCorrectnessRoles.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisCorrectnessRoles.java:6)에 모여 있습니다. lease에는 주의가 필요합니다. readiness 분류는 보수적으로 correctness 쪽에 두지만, 실제 adapter 계약은 “efficiency only”이며 fencing token을 제공하지 않습니다. 따라서 데이터베이스 쓰기처럼 correctness-sensitive한 임계 구역을 Redis lease 하나로 보호하면 안 됩니다. [`RedisCapabilityConfig.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/redis/RedisCapabilityConfig.java:218)의 계약을 readiness 명칭보다 우선해 해석해야 합니다. ## 2. 부팅은 bind가 아니라 검증 파이프라인입니다 활성화된 Redis의 부팅 순서는 다음처럼 정리할 수 있습니다. ```mermaid flowchart LR A[APP_REDIS_ENABLED] --> B[role selector 모순 검사] B --> C[app.redis 설정 bind] C --> D[cross-field validation] D --> E[secret reference 해석] E --> F[TLS / raw policy resource 검사] F --> G[topology별 client 생성] G --> H[connection lane과 capacity 구성] H --> I[semantic adapter 조립] I --> J[optional / required health 구성] ``` ### 설정은 Redis가 켜졌을 때만 존재합니다 `RedisSdkSettings`는 애플리케이션 전체의 `@ConfigurationPropertiesScan` 대상이 아니라 conditional auto-configuration 안에서만 등록됩니다. Redis가 켜지면 `app.redis`를 바인딩하고, 이후 validation bean이 cross-field 규칙을 실행합니다. raw gateway를 켰다면 allowlist resource의 존재와 가독성까지 확인한 뒤에야 client를 만듭니다. 기본 raw allowlist 위치는 모듈이 실제로 제공하지 않으므로, raw를 활성화하면서 resource를 명시하지 않으면 startup failure가 됩니다. 관련 순서는 [`RedisSdkAutoConfiguration.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkAutoConfiguration.java:73)에 구현돼 있습니다. 세부 `APP_REDIS_*` 키를 기본 `application.yml`이나 `.env`에 모두 나열하지 않은 것도 같은 정책입니다. Redis를 쓰지 않는 배포가 Redis 설정을 운반하지 않게 하고, configuration metadata와 env registry가 속성 계약을 맞춥니다. 이 정책은 [`env-keys.yaml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/env-keys.yaml:1885)에 명시돼 있습니다. ### topology와 namespace 기본값 현 HEAD의 주요 기본값은 다음과 같습니다. | 설정 | 기본값 | 운영 의미 | | --- | --- | --- | | mode | `STANDALONE` | topology fallback은 없음 | | nodes | `localhost:6379` | standalone은 정확히 한 노드만 허용 | | database | `0` | Cluster는 DB 0만 허용 | | namespace | `local:sample-service:shared` | 모든 capability가 한 namespace 규칙을 공유 | | acknowledged write loss accepted | `false` | 구현·테스트된 durability probe의 opt-out 기본값. 현 production에는 probe가 미조립 | 근거는 [`RedisSdkSettings.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkSettings.java:23)와 env registry의 [`mode`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/env-keys.yaml:2180), [`namespace`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/env-keys.yaml:2196), [`nodes`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/env-keys.yaml:2241) 항목입니다. namespace는 `{environment}:{service}:{domain}`의 한 규칙으로 모든 capability에 적용됩니다. per-capability prefix 조립을 제거한 이유는 ACL의 `~pattern`과 애플리케이션이 실제 생성하는 key prefix가 어긋나는 일을 막기 위해서입니다. cache의 외부 식별자는 HMAC-SHA256으로 digest하고, namespace를 HMAC material에 함께 묶습니다. staging dump의 digest가 production과 일대일 대응하지 않게 하는 조치입니다. 구현은 [`RedisCapabilityConfig.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/redis/RedisCapabilityConfig.java:302)에 있습니다. ### secret은 값이 아니라 reference로 전달합니다 credential 설정에는 비밀번호 자체가 아니라 다음 형식의 포인터가 들어갑니다. ```text secret:/// secret://@/ ``` 첫 번째 형식은 ACL username을 `default`로 봅니다. 두 번째 형식은 named ACL user를 명시합니다. resolver는 `secret://` 외 scheme, 잘못된 경로, 빈 해석 결과를 모두 startup error로 처리하고, `toString()`에서도 password를 `***`로 가립니다. [`RedisCredentialResolver.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisCredentialResolver.java:7)를 참고하면 됩니다. application credential이 없으면 기본적으로 실패합니다. 의도적으로 anonymous Redis를 쓸 때만 `APP_REDIS_AUTHENTICATION_ANONYMOUS_ACCESS_ACCEPTED=true`로 trade-off를 기록합니다. advanced, pub/sub, admin, raw, Sentinel control credential은 역할별 reference를 둘 수 있습니다. 설정 계약은 [`env-keys.yaml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/env-keys.yaml:2394)과 [`Sentinel credential`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/env-keys.yaml:2693)에 있습니다. ### production secret validator에서 발견되는 현재 불일치 현 HEAD에는 두 종류의 secret 계약이 공존합니다. - Redis SDK는 `app.redis.authentication.*-credential-reference`를 해석합니다. - `SecretSourceValidator`는 prod profile에서 `APP_CACHE_REDIS_PASSWORD`, `APP_RATE_LIMIT_REDIS_PASSWORD` 같은 이전 role 단위 secret과 HMAC material을 검사합니다. 또한 `application.yml`은 idempotency와 lease의 `key-hmac-secret-reference`를 선언하고 validator도 이 secret을 요구하지만, 현 `RedisCapabilitySettings.Idempotency`와 `.Lease` 및 composition code는 이 필드를 소비하지 않습니다. rate-limit도 별도 HMAC secret을 실제 조립에 사용하지 않습니다. cache만 HMAC secret을 해석합니다. 근거는 [`application.yml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/resources/application.yml:348), [`SecretSourceValidator.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/SecretSourceValidator.java:31), [`RedisCapabilityConfig.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/redis/RedisCapabilityConfig.java:95)입니다. 따라서 production 배포 전에 다음을 정리해야 합니다. 1. SDK credential reference가 가리키는 secret과 prod validator의 legacy password key를 하나의 계약으로 통합합니다. 2. idempotency·lease·rate-limit key HMAC secret을 실제 구현에 연결하거나, 사용하지 않는 설정과 필수 secret 요구를 제거합니다. 3. env registry와 generated configuration metadata가 이 결정을 같은 이름과 조건으로 표현하게 합니다. 이 상태를 그대로 두면 “필수 secret을 주입했지만 runtime이 쓰지 않는” 설정과 “runtime이 필요한 credential reference인데 prod validator의 목록에는 없는” 설정이 동시에 생길 수 있습니다. ## 3. topology는 선택이고 fallback이 아닙니다 runtime deployment mode는 `STANDALONE`, `SENTINEL`, `CLUSTER` 세 가지입니다. TLS는 네 번째 topology가 아니라 standalone 형태에서 transport를 검증하는 qualification lane입니다. [`RedisTopologyClientFactory.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisTopologyClientFactory.java:40)는 선언한 mode에서 다른 mode로 fallback하지 않습니다. Sentinel로 선언했는데 Sentinel prerequisite가 빠졌다면 standalone으로 연결해 일단 부팅하지 않습니다. 그렇게 하면 첫 promotion 전까지는 정상처럼 보이다가, promotion 후 교체된 primary에 계속 쓸 수 있기 때문입니다. ### Standalone - 정확히 한 `host:port`만 허용합니다. - 여러 endpoint를 넣으면 어느 노드를 쓸지 임의로 고르지 않고 실패합니다. - primary promotion 개념이 없으므로 replicated write durability 검사 대상이 아닙니다. 구현은 [`RedisTopologyClientFactory.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisTopologyClientFactory.java:153)에 있습니다. ### Sentinel - Sentinel endpoint와 monitored master name으로 primary를 찾습니다. - data node account와 Sentinel control account를 분리할 수 있습니다. - Sentinel node 목록이 없으면 일반 `nodes` 목록을 Sentinel endpoint로 사용합니다. - write durability를 확인하는 `RedisStartupProbe` 구현과 단위 테스트가 있습니다. 다만 현 production composition에는 연결되지 않았습니다. 실제 Sentinel client 조립은 [`RedisTopologyClientFactory.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisTopologyClientFactory.java:178), 아직 조립되지 않은 검사 객체는 [`RedisStartupProbe.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisStartupProbe.java:40)에 있습니다. ### Cluster - seed node에서 cluster topology를 발견합니다. - `maxRedirects` 기본값은 5입니다. - periodic refresh 기본값은 30초이며 adaptive refresh trigger를 모두 켭니다. - cluster node membership validation을 활성화합니다. - database는 0만 허용합니다. - `CommandPolicyGuard` 구현과 테스트는 multi-key 요청이 서로 다른 slot을 가리키면 전송 전에 거절합니다. 현 production semantic adapter에는 이 guard가 조립되지 않았습니다. production client 설정은 [`RedisTopologyClientFactory.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisTopologyClientFactory.java:207), 미조립 cross-slot admission 구현은 [`CommandPolicyGuard.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/CommandPolicyGuard.java:188)에 있습니다. ### TLS TLS 기본값은 비활성화이고 hostname verification 기본값은 `true`입니다. private CA라면 trust material resource를 지정할 수 있고, client certificate를 지정하면 client key도 반드시 있어야 합니다. material은 classpath resource와 filesystem path를 모두 처리하며 읽을 수 없는 material은 연결 시점이 아니라 startup에 실패합니다. 설정은 [`RedisSdkSettings.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkSettings.java:476), client 적용은 [`RedisTopologyClientFactory.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisTopologyClientFactory.java:290)에 있습니다. ## 4. connection lane은 성능 최적화가 아니라 장애와 권한의 격리선입니다 Redis 연결은 여섯 lane으로 나뉩니다. | lane | 용도 | 기본 credential role | 기본/주요 한도 | | --- | --- | --- | --- | | `REGULAR` | 일반 non-blocking 명령 | application | in-flight command 64 | | `BLOCKING` | blocking pop·stream read | application | connection 32, server block 최대 30초 | | `TRANSACTION` | `MULTI`부터 `EXEC`까지 독점 | application | connection 16 | | `SCRIPT` | 등록된 Lua/script 실행 | advanced | regular capacity ceiling 사용 | | `PUBSUB` | subscribe lifecycle | pub/sub | buffer 1,024, overflow는 error | | `ADMIN` | read-only 진단 | admin | enabled일 때 2 | lane 정의와 credential mapping은 [`RedisConnectionKind.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisConnectionKind.java:6), pool ceiling 조립은 [`RedisSdkAutoConfiguration.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkAutoConfiguration.java:263)에 있습니다. blocking 명령은 server-side block 동안 connection을 점유합니다. transaction은 `MULTI`와 `EXEC` 사이에 connection을 독점합니다. subscribe 상태의 connection은 일반 명령을 처리할 수 없습니다. admin은 다른 권한을 사용합니다. 이를 한 pool에 섞으면 blocking consumer 포화가 cache get을 멈추거나, 진단 권한이 request path로 새어 나갑니다. 별도 credential reference가 설정된 역할마다 별도 Lettuce client와 event loop가 생깁니다. advanced와 Pub/Sub credential이 없으면 application account로 fallback하지만 경고 범위는 서로 다릅니다. advanced fallback은 startup warning을 남기고, Pub/Sub fallback은 현재 경고를 남기지 않습니다. raw와 admin은 enabled 상태에서 전용 credential이 없으면 fallback하지 않고 startup이 실패합니다. 따라서 단일 account 배포는 가능하지만 startup warning만 보고 모든 역할의 권한 분리를 확인했다고 판단하면 안 됩니다. client-per-role 조립은 [`RedisTopologyClientFactory.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisTopologyClientFactory.java:116), 검증 범위는 [`RedisSdkSettings.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkSettings.java:108)와 [`RedisSdkSettings.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkSettings.java:398)에 있습니다. 운영자는 lane별로 서로 다른 saturation 신호를 읽어야 합니다. blocking lane이 포화됐지만 regular traffic이 정상이라면 Redis 전체 장애가 아니라 consumer 동시성 산정 문제입니다. blocking pool은 요청률이 아니라 동시에 대기할 consumer 수로 산정합니다. 이 운영 해석은 [`operations.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/redis/operations.md:65)에 기록돼 있습니다. ## 5. ACL과 TLS는 client-side policy의 마지막 방어선입니다 SDK가 command catalog와 permit으로 요청을 거르더라도 Redis account가 넓으면 실수나 우회 경로가 마지막 경계에서 막히지 않습니다. qualification fixture는 다음 named account를 둡니다. - `ca-skeleton-application`: 일반 read/write, transaction, pub/sub의 허용된 범위 - `ca-skeleton-application-advanced`: `SCRIPT LOAD`, `EVALSHA`, function 등 script 경로 - `ca-skeleton-raw-gateway`: 승인된 raw 범위 - `ca-skeleton-admin-readonly`: `INFO`, `SLOWLOG`, `MEMORY USAGE`, `CONFIG GET`, `ACL DRYRUN` 등 read-only 진단 - replication, Sentinel, cluster bootstrap 전용 계정 fixture는 `default` user를 끄고 account별 비밀번호와 key/channel pattern을 적용합니다. 실제 ACL은 [`all-accounts.acl`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/infra/redis-sdk/acl/all-accounts.acl:1)에 있습니다. 이 파일의 `fixture-*` password는 throwaway qualification container용이며 배포 템플릿이 아닙니다. 운영 credential은 앞서 설명한 `secret://` reference로 해석해야 합니다. 여기에도 문서 드리프트가 있습니다. [`infra/redis-sdk/acl/README.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/infra/redis-sdk/acl/README.md:7)는 비밀번호 material을 파일에 두지 않는다고 설명하지만, 현 ACL fixture에는 실제로 `fixture-*` 값이 있습니다. 반대로 상위 [`infra/redis-sdk/README.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/infra/redis-sdk/README.md:39)는 이 값이 test fixture라고 정확히 설명합니다. 보안 검토에서는 상위 README의 범위를 적용하되, 하위 README는 갱신해야 합니다. TLS qualification lane은 plaintext port를 `0`으로 꺼서 TLS 설정이 잘못됐는데 평문으로 fallback하는 거짓 성공을 막습니다. CA와 server key는 시작 시 named volume에 생성하며 repository에 private key를 커밋하지 않습니다. hostname에는 `localhost`와 `127.0.0.1` SAN을 넣고, client는 생성된 CA를 전달받아 검증합니다. Compose는 [`infra/redis-sdk/tls/compose.yml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/infra/redis-sdk/tls/compose.yml:1)에서 확인할 수 있습니다. 다만 이 lane은 `alpine/openssl:latest`를 사용합니다. image digest가 고정되지 않아 certificate generation 환경이 바뀔 수 있습니다. CI manifest가 Redis image digest를 보존하더라도 certificate helper image까지 같은 수준으로 재현하려면 tag 또는 digest 고정이 필요합니다. ## 6. command admission은 구현·테스트됐지만 production path에는 아직 연결되지 않았습니다 `CommandPolicyGuard`와 관련 테스트는 Redis에 보내기 전 다음 순서로 요청을 검사하는 계약을 구현합니다. ```text command catalog → 서버 capability와 최소 버전 → risk와 permit provenance → namespace → Cluster slot → request/reply 예상 budget → connection lane → timeout → invocation → 일부 typed decoder의 관측 reply 검사 → batch의 decoded-shape 근사 측정 → exception translation → telemetry ``` 현 HEAD의 구현 순서는 [`CommandPolicyGuard.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/CommandPolicyGuard.java:24)에 있습니다. application이 permit interface를 임의로 구현했다고 해서 승인하지 않고, 누가 어떤 policy에 대해 발급했는지를 검증합니다. R2 operation은 permit과 `OperationBudget`을 함께 요구하며 multi-key fan-out에는 별도 multi-key permit이 필요합니다. 이 순서가 모든 reply의 실제 byte ceiling을 뜻하지는 않습니다. 기본 `GET`, script, function, raw, admin, extension은 관측한 reply byte를 decoder 전에 공통 검사하지 않습니다. extension은 policy name이 있을 때만 budget을 가지며 null-policy path에는 budget 자체가 없습니다. batch는 wire bytes가 아니라 decode된 result shape를 근사해 누적합니다. 따라서 설정된 reply ceiling을 모든 surface의 memory 보호선으로 간주하면 안 됩니다. 그러나 main source에서 `CommandPolicyGuard`나 이를 사용하는 executor를 생성하는 production composition은 확인되지 않습니다. 현재 네 semantic adapter는 `RedisRuntimeOwner`에서 lane을 빌려 gateway를 직접 호출합니다. 따라서 이 절의 capability·permit·namespace·slot·budget·timeout 검사는 **구현되고 테스트된 SDK 계약**이지, 현 production request path의 보장이 아닙니다. `OperationBudget`은 다음 네 값을 호출자가 명시하게 합니다. ```java new OperationBudget(maxElements, maxRequestBytes, maxReplyBytes, timeout) ``` 해당 R2 typed API 계약은 이를 생략하거나 무한대로 default할 수 없게 설계됐습니다. 호출자가 Redis 작업에 허용할 최대 비용을 선언하게 합니다. 다만 production semantic adapter가 이 admission path를 사용한다고 볼 조립 근거는 없습니다. 계약은 [`OperationBudget.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/OperationBudget.java:6)에 있습니다. ### 기본 timeout profile | profile | 기본 timeout | 대상 | | --- | ---: | --- | | `FAST` | 500ms | single-key get/set, membership, score | | `COLLECTION` | 2s | bounded range, scan page, set algebra | | `SCRIPT` | 1s | 등록된 script/function | | `BATCH` | 2s | pipeline과 명시적 batch | | `ADMIN` | 3s | read-only 진단 | | `BLOCKING` | server block + 2s | blocking command | 값은 [`TimeoutProfile.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/TimeoutProfile.java:11)와 [`RedisSdkSettings.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkSettings.java:151)에 있습니다. 구현된 guard path에서는 blocking command가 server block 시간을 양수의 유한값으로 선언해야 하며, 설정된 최대 30초를 넘으면 전송 전에 거절됩니다. effective client timeout에는 2초 margin을 더합니다. 이 enforcement 역시 production에는 미조립입니다. [`CommandPolicyGuard.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/CommandPolicyGuard.java:231)를 참고하면 됩니다. ### 기본 size와 cardinality 한도 | 한도 | 기본값 | | --- | ---: | | key | 512 bytes | | value | 1 MiB | | stream payload | 256 KiB | | hash field | 512 KiB | | collection 결과 | 1,000 elements | | scan page | 500 elements | | batch | 500 commands | | request | 4 MiB | | reply | 16 MiB | | `offlineQueueCommands` 설정 | 기본 1,000, 현재 production client option에서 미사용 | | 실제 Lettuce request queue | `maximumInFlightCommands`와 같은 기본 64 | | bitmap bit index | 10,000,000 | 설정은 [`RedisSdkSettings.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkSettings.java:239)에 있습니다. 이 가운데 `offlineQueueCommands=1_000`은 현재 validation과 getter/setter에만 남아 있고 production client option에는 소비되지 않습니다. 실제 Lettuce `requestQueueSize`는 `maximumInFlightCommands`에 연결되므로 기본값은 64입니다. connection capacity의 나머지 기본값은 in-flight bytes 4 MiB, reply 16 MiB입니다. [`RedisTopologyClientFactory.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisTopologyClientFactory.java:290)와 [`RedisSdkSettings.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkSettings.java:680)를 함께 봐야 합니다. 여기서 registry 드리프트도 확인됩니다. `APP_REDIS_CAPACITY_MAXIMUM_IN_FLIGHT_BYTES`와 `APP_REDIS_CAPACITY_MAXIMUM_REPLY_BYTES`는 code default가 4 MiB와 16 MiB인데 env registry의 default는 `null`입니다. 플랫폼이 registry를 바탕으로 Helm values나 secret/config schema를 생성한다면 실제 runtime default와 다른 계약을 배포할 수 있습니다. [`env-keys.yaml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/env-keys.yaml:2465)을 코드와 함께 수정해야 합니다. ### 연결이 끊겼을 때 queue를 키우지 않습니다 Lettuce의 disconnected queue에 쓰기를 쌓았다가 reconnect 후 몰아서 재생하면 outage 중 발생한 작업과 재생 작업의 상대 순서가 불명확해집니다. 이 모듈은 기본적으로 disconnected 상태에서 command를 거절하고, request queue size를 in-flight command ceiling으로 제한하며 auto-reconnect는 유지합니다. caller가 오류를 보고 재시도·보상 여부를 정하게 합니다. 적용 코드는 [`RedisTopologyClientFactory.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisTopologyClientFactory.java:290)에 있습니다. ## 7. 실행 확실성 모델도 production 조립 여부를 구분해야 합니다 write timeout 뒤 가장 위험한 대응은 무조건 재시도하는 것입니다. client가 reply를 받지 못했을 뿐 server에는 write가 적용됐을 수 있습니다. 이 모듈의 `ExecutionCertainty`와 translator는 실패를 다음 네 단계로 모델링하고 테스트합니다. | `ExecutionCertainty` | 의미 | 자동 재시도 | | --- | --- | --- | | `CONFIRMED_SUCCESS` | server가 성공 응답 | 하지 않음 | | `CONFIRMED_FAILURE` | server가 명시적으로 거절, 적용되지 않음 | pipeline이 임의 재시도하지 않음 | | `SAFE_TO_RETRY_FAILURE` | server에 도달하지 않았음이 증명됨 | 허용 | | `AMBIGUOUS_FAILURE` | 실행됐을 수도 있고 아닐 수도 있음 | command가 retry-safe일 때만 허용 | 정의는 [`ExecutionCertainty.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/ExecutionCertainty.java:6)에 있습니다. `LettuceExceptionTranslator` 구현은 non-idempotent write의 timeout, connection loss, 분류할 수 없는 in-flight failure를 `RedisAmbiguousExecutionException`으로 바꿉니다. `NOREPLICAS`, `OOM`, `MISCONF`, `EXECABORT`, `READONLY`처럼 server가 명시적으로 거절한 오류는 definite rejection으로 분류합니다. ACL 오류, `CROSSSLOT`, redirect, busy, `NOSCRIPT`도 안정된 SDK exception hierarchy로 번역하고 raw server message 대신 error code만 남깁니다. 그러나 이 translator를 생성해 현재 semantic adapter에 연결하는 production composition도 확인되지 않습니다. 자세한 분류는 [`LettuceExceptionTranslator.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslator.java:26)에 있습니다. 따라서 다음은 현재 runtime이 모두 강제한다고 볼 수 있는 목록이 아니라, 저장소가 정의한 failure-semantics 원칙이자 production 조립의 완료 조건입니다. - non-idempotent write의 ambiguous failure는 재시도가 아니라 조회·대사·보상 대상입니다. - SDK는 cross-slot command를 자동 분할하지 않습니다. shared hash tag로 key를 같은 slot에 배치해야 합니다. - collection, stream, index 전체 읽기를 제공하지 않습니다. 모든 읽기에 bound가 필요합니다. - 현재 rate-limit·lease·idempotency semantic script는 첫 요청에서 `SCRIPT LOAD`된 뒤 `EVALSHA`로 실행됩니다. `NOSCRIPT`이면 digest cache를 비우고 script를 한 번만 다시 load·평가합니다. 따라서 advanced account에는 request path에서도 `SCRIPT LOAD` 권한이 필요합니다. caller가 임의 script body를 전달할 수 없다는 정책과 server가 first-use에 script를 load한다는 동작은 별개입니다. - Redis function library는 request path에서 load하지 않는 배포 artifact입니다. - transaction은 rollback이 아닙니다. `EXEC` reply를 잃으면 transaction 전체가 실행됐는지 ambiguous할 수 있습니다. - Pub/Sub은 at-most-once입니다. reconnect 중 message replay가 필요하면 consumer group 기반 stream과 idempotent consumer를 사용해야 합니다. 운영 제한의 원문은 [`operations.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/redis/operations.md:25), transaction queue semantics는 [`QueueingRedisCommandExecutor.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/QueueingRedisCommandExecutor.java:16)에 있습니다. ## 8. Sentinel은 성공으로 응답한 쓰기도 잃을 수 있습니다 이 절의 수치는 **이번 조사에서 재실행한 결과가 아니라 저장소의 과거 실측 기록**입니다. 저장소 기록에 따르면 Redis 7.4 Sentinel lane에서 replica가 승격된 뒤 기존 primary가 약 11초 동안 자신이 교체됐음을 인지하지 못했습니다. client는 기존 primary에 계속 write했고, server는 2,086건에 `+OK`를 반환했습니다. 이후 기존 primary가 새 primary에서 resync하면서 이 write가 폐기됐고, client가 본 command failure는 한 건뿐이었습니다. 이 손실은 client-side metric이나 retry로 감지할 수 없습니다. server가 성공으로 응답했으므로 driver, SDK, caller 모두 `CONFIRMED_SUCCESS`로 볼 수밖에 없습니다. 이 기록은 [`operations.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/redis/operations.md:36), 더 자세한 run 설명은 [`support-matrix.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/redis/support-matrix.md:104)에 남아 있습니다. 서버의 모든 primary 후보에 다음을 적용한 기록도 있습니다. ```conf min-replicas-to-write 1 min-replicas-max-lag 1 ``` 같은 promotion에서 acknowledged-and-discarded write는 2,086건에서 1건으로 줄고, 2,020건이 `NOREPLICAS`로 명시적으로 거절됐다고 문서는 기록합니다. silent loss를 caller가 대응할 수 있는 visible failure로 바꾼 것입니다. Sentinel Compose는 primary와 replica가 역할을 바꾸더라도 두 설정을 모두 유지하도록 공통 node definition에 넣습니다. [`sentinel/compose.yml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/infra/redis-sdk/sentinel/compose.yml:24)을 참고하면 됩니다. 한 번은 이 설정을 시작 시 primary였던 노드에만 적용해 첫 promotion은 통과했지만 반대 방향 promotion에서 acknowledged write 2,099건이 손실됐다는 기록도 있습니다. “현재 primary”가 아니라 **primary가 될 수 있는 모든 노드**에 적용해야 하는 이유입니다. [`support-matrix.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/redis/support-matrix.md:123)에 당시 수정 경위가 있습니다. 현 HEAD에는 이 경험을 검사하는 `RedisCapabilityProbe.requireWriteDurability`와 `RedisStartupProbe`가 구현돼 있고 단위 테스트도 있습니다. 이 검사는 Sentinel과 Cluster 같은 replicated mode에서 다음 조건을 요구하도록 설계됐습니다. - `min-replicas-to-write >= 1` - `min-replicas-max-lag >= 1` - 또는 손실을 의도적으로 수용하는 `app.redis.acknowledged-write-loss-accepted=true` 구현상 `CONFIG GET` 권한이 없어 값을 확인할 수 없는 경우도 보장을 입증하지 못한 것으로 보고 실패합니다. 다만 현 `RedisSdkAutoConfiguration`과 application composition은 이 probe를 생성하거나 호출하지 않습니다. 그러므로 **현 production 시작 과정은 이 조건을 자동으로 거절하지 않습니다.** 조립이 추가되기 전에는 배포 파이프라인이나 외부 정책 검사에서 같은 조건을 검증해야 합니다. 검사 로직은 [`RedisCapabilityProbe.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisCapabilityProbe.java:97), server fact 수집은 [`RedisStartupProbe.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisStartupProbe.java:76)에 있습니다. 두 설정으로도 `min-replicas-max-lag`만큼의 잔여 window는 남습니다. 저장소의 [`operations.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/redis/operations.md:61)는 개별 write에 Redis `WAIT`를 사용하는 대안을 적지만, 현재 command catalog에는 `WAIT`가 없고 typed·semantic 실행 표면도 없습니다. 미분류 명령은 default-deny이므로 이 SDK에서는 지금 적용할 수 없습니다. 이 대안이 필요하면 command 분류, typed API, ACL, production composition, Sentinel qualification을 먼저 추가해야 하며, 현재 운영 절차는 `min-replicas-*` 검증과 ambiguous write 대사에 한정해야 합니다. ## 9. health와 readiness는 “Redis가 한 대인가”가 아니라 “어떤 역할인가”를 묻습니다 health probe는 driver connection의 `isOpen()` flag를 믿지 않고 regular lane을 빌려 실제 `PING` round trip을 수행합니다. TCP가 단절을 아직 감지하지 못한 순간에도 실제 응답 여부를 확인하려는 선택입니다. health detail에는 mode, state, 예외 class name만 넣고 endpoint, username, key, payload를 넣지 않습니다. 구현은 [`RedisHealthContributor.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisHealthContributor.java:12)에 있습니다. 활성 역할에 따라 contributor가 달라집니다. - cache만 사용하면 `redisOptional`이 생성됩니다. Redis가 끊기면 `DEGRADED`이지만 readiness를 내리지 않습니다. - correctness 역할이 하나라도 Redis를 선택하면 `redisRequired`가 생성됩니다. Redis가 끊기면 `DOWN`이며 readiness group에 포함됩니다. `redisRequired=UP`은 timeout 안에 `PING` 한 번이 성공했다는 **reachability 신호**입니다. semantic script, 전체 ACL scope, module capability, `CONFIG GET`, `min-replicas-*`를 검증하지 않으며 미조립 startup probe를 대신하지 않습니다. `redis-session` selector가 required contributor를 만들 수 있다는 사실도 session provider가 존재한다는 증거가 아닙니다. readiness group membership은 정적으로 `redisRequired`를 적지 않습니다. 동일한 correctness predicate를 읽는 environment post-processor가 contributor가 실제 생성될 때만 기존 readiness include 목록에 추가합니다. membership validation을 끄지 않기 때문에 오타나 존재하지 않는 contributor는 startup에서 드러납니다. [`RedisReadinessGroupPostProcessor.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/RedisReadinessGroupPostProcessor.java:15)와 [`RedisSdkAutoConfiguration.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkAutoConfiguration.java:286)를 함께 보면 흐름이 명확합니다. ### 운영 신호의 cardinality 정책 관측 이벤트는 command family, deployment mode, latency, 성공/실패와 ambiguity를 다루며 key, field, member, value를 metric label로 올리지 않습니다. tenant identifier가 dashboard로 새거나 label cardinality가 무한히 늘어나는 일을 막습니다. 따라서 “어느 command family가 느린가”는 metric으로 답하고, “어느 key가 hot한가”는 admin plane의 `SLOWLOG`와 특정 key의 `MEMORY USAGE`로 조사합니다. 아래 표는 SDK가 정의한 신호의 해석입니다. 미조립 guard·translator에서 나오는 신호가 관찰되지 않는다고 해서 위반이나 ambiguous execution이 없었다고 판단하면 안 됩니다. | 신호 | 해석 | 1차 대응 | | --- | --- | --- | | `RedisCommandRejectedException` | SDK가 전송 전에 bound·policy 위반을 거절 | reason에 나온 budget, permit, namespace를 수정 | | `RedisCrossSlotException` | multi-key가 여러 slot에 분산 | shared hash tag 설계 점검 | | `RedisAmbiguousExecutionException` | write 적용 여부 불명 | 자동 재시도 중단, 대사·보상 | | `RedisCapabilityUnavailableException` | server capability와 선언 불일치 | version, module, startup probe 확인 | | `SentinelFailoverObserver.ambiguousWriteCount` | promotion 근처 non-retry-safe write | 건별 reconciliation workload 산정 | | `ClusterTopologyObserver.reshardingObserved` | `ASK`·`TRYAGAIN` 관찰 | migration 종료까지 latency 편차 감시 | 저장소의 alert 해석표는 [`operations.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/redis/operations.md:3)에 있습니다. ## 10. deterministic test와 real topology qualification을 분리합니다 기본 Gradle `test`는 `redis-topology` tag를 제외합니다. 설정, policy, typed API, key rendering, slot 계산, exception translation, composition은 빠른 deterministic test로 검증하고, Sentinel promotion·Cluster redirect·ACL·TLS처럼 실제 server와 driver가 결정하는 동작은 별도 lane으로 보냅니다. 태그 분리는 [`cache-redis/build.gradle`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/build.gradle:44)에 있습니다. 현 HEAD에는 59개의 Redis module test class가 있고, 실 Redis server를 사용하는 topology class는 다음 여덟 개입니다. root 작업 세션에서 기본 `:adapter:outbound:cache-redis:test`는 성공했지만, 아래 topology class를 선택하는 lane은 실행하지 않았습니다. - `LiveRedisSemanticPortsTest` - `LiveRedisCompositionTest` - `RedisTopologyContractTest` - `LiveRedisTlsTest` - `LiveRedisClusterTransactionTest` - `LiveRedisClusterTest` - `LiveRedisGuardrailTest` - `LiveRedisSentinelPromotionTest` 이 lane은 Testcontainers를 test class 안에서 띄우는 방식이 아니라 `infra/redis-sdk//compose.yml`로 외부 topology를 시작하고 endpoint를 Gradle property로 전달합니다. ### lane별 qualification 범위 | lane | fixture | 핵심 검증 | task의 최소 실행 건수 | | --- | --- | --- | ---: | | standalone | Redis 1대 | composition, semantic port, ACL, guardrail | 20 | | sentinel | data node 2대 + Sentinel 3대 | promotion, reconnect, write-loss bound | 20 | | cluster | primary 3대 + replica 3대 | slot, cross-slot, redirect, transaction | 24 | | tls | plaintext-off standalone | CA trust, hostname verification, command over TLS | 4 | `redisTopologyTest`는 단순히 tag를 선택하지 않습니다. 알 수 없는 mode, 필수 endpoint·Sentinel master·TLS trust material 누락, 발견한 test 0건, 필수 class 누락, 최소 건수 미달, skip 한 건 이상을 모두 실패로 처리하고 매번 다시 실행합니다. [`cache-redis/build.gradle`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/build.gradle:85)에 gate가 구현돼 있습니다. ### fixture가 운영 배포를 뜻하지는 않습니다 qualification Compose에는 의도적인 제약이 있습니다. - 모든 data node가 AOF와 snapshot을 끕니다. - standalone은 replication과 persistence를 검증하지 않습니다. - Sentinel과 Cluster는 topology가 광고한 주소를 host의 test client가 그대로 접근하도록 host networking과 고정 포트를 씁니다. - Sentinel은 7010·7011과 27010~27012, Cluster는 7100~7105와 bus port 17100~17105를 점유합니다. - TLS 인증서는 하루짜리이고 mTLS client authentication은 fixture에서 끕니다. 즉 이 Compose는 topology behavior qualification 도구이지 production durability template가 아닙니다. lane의 목적과 port는 [`infra/redis-sdk/README.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/infra/redis-sdk/README.md:67), 실제 fixture는 [`standalone`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/infra/redis-sdk/standalone/compose.yml:1), [`sentinel`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/infra/redis-sdk/sentinel/compose.yml:1), [`cluster`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/infra/redis-sdk/cluster/compose.yml:1), [`tls`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/infra/redis-sdk/tls/compose.yml:1)에서 확인할 수 있습니다. ## 11. CI 행렬은 “정의”와 “증거”를 나눠 읽어야 합니다 일반 quality workflow의 `redis-sdk` job은 다음을 실행하도록 정의돼 있습니다. ```bash ./gradlew \ :shared-contract:edgeRateLimitContractTest \ :adapter:outbound:cache-redis:check \ verifyCleanArchitectureDependencies \ verifyEnvKeys \ verifyPublicPathSnapshot \ verifyConfigurationPropertiesProcessor \ --no-daemon --stacktrace ``` 정의 위치는 [`.github/workflows/ci-quality-gates.yml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/.github/workflows/ci-quality-gates.yml:82)입니다. release gate는 이 `redis-sdk` job을 요구하지만 별도 topology workflow의 결과를 직접 `needs`로 묶지는 않습니다. 따라서 일반 release gate 성공과 모든 real topology lane의 최신 성공은 같은 명제가 아닙니다. 별도 `redis-sdk-topology` workflow는 다음 행렬을 **실행하도록 정의**합니다. - Redis 관련 PR: standalone 7.4 - nightly 및 release-candidate: standalone·Sentinel·Cluster의 7.2, 7.4, 8.2 - nightly 및 release-candidate: TLS의 7.4, 8.2 각 job은 topology, Redis version, commit SHA, workflow run ID, Redis image digest를 manifest로 남기고 JUnit 결과와 함께 90일 보존하도록 정의돼 있습니다. workflow는 [`.github/workflows/redis-sdk-topology.yml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/.github/workflows/redis-sdk-topology.yml:31)에 있습니다. 그러나 workflow에 행이 있다는 사실은 통과 이력이 아닙니다. 현 [`support-matrix.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/redis/support-matrix.md:53)는 7.4의 standalone·Sentinel·Cluster 과거 evidence만 명시하고 7.2와 8.2는 declared but not certified라고 적습니다. 반면 [`infra/redis-sdk/README.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/infra/redis-sdk/README.md:1)는 TLS를 포함한 네 lane 모두 7.4에서 실행됐다고 기록합니다. 즉 TLS에는 infra README의 과거 실행 기록이 있지만 support matrix의 certified table에는 TLS row가 없습니다. 승인 source를 하나로 정하고 artifact로 대조하기 전에는 TLS 7.4도 certified로 강화하지 않습니다. 따라서 지원 버전 승인은 다음 증거를 함께 확인해야 합니다. 1. 해당 commit의 topology artifact가 존재합니다. 2. manifest의 topology, Redis version, image digest가 승인 대상과 일치합니다. 3. JUnit XML에 skip이 없고 Gradle minimum test floor를 충족합니다. 4. `support-matrix.md`의 certified row와 test class가 artifact와 일치합니다. 5. 문서 행만 있고 artifact가 없으면 “declared”로 남깁니다. ## 12. 플랫폼 운영 runbook ### 배포 전 확인 순서 1. **역할을 먼저 정합니다.** 현 HEAD에서 조립되는 cache·idempotency·rate-limit·lease 4개 중 필요한 역할을 정합니다. `redis-session`은 Redis repository와 최초 인증 mechanism을 모두 구현하고 end-to-end로 검증하기 전까지 선택하지 않습니다. 2. **전역 스위치를 맞춥니다.** 역할이 Redis를 선택하면 `APP_REDIS_ENABLED=true`가 필요합니다. 3. **namespace를 고정합니다.** environment, service, domain이 ACL `~pattern`과 일치하는지 확인합니다. 4. **topology를 명시합니다.** standalone, Sentinel, Cluster 중 하나를 선택하고 endpoint의 의미가 data node인지 Sentinel인지 구분합니다. 5. **credential role을 설계합니다.** application, advanced, pub/sub, admin, raw, Sentinel control account의 실제 분리가 필요한지 결정하고 reference를 secret backend에 연결합니다. 6. **TLS를 검증합니다.** hostname verification을 기본적으로 유지하고 private CA material의 mount path와 읽기 권한을 확인합니다. 7. **replicated write durability를 확인합니다.** primary가 될 수 있는 모든 노드에서 `min-replicas-to-write`와 `min-replicas-max-lag`를 조회합니다. 8. **timeout과 capacity를 서비스 SLO에 맞게 조정합니다.** 늘리기 전에 느린 command를 숨기는지, outage queue를 키우는지 검토합니다. 9. **readiness 구성을 확인합니다.** cache-only 배포는 `redisOptional`, correctness 역할 배포는 `redisRequired`가 의도대로 존재해야 합니다. `redisRequired=UP`은 `PING` reachability만 뜻하므로 capability·ACL·`min-replicas-*`는 별도로 검증합니다. 10. **멱등성 effect 경계를 확인합니다.** Redis V2의 same retained attempt가 action을 다시 실행할 수 있고 long-running action의 processing lease도 현재 renew되지 않습니다. effect 자체의 idempotency, effect-point CAS 또는 outbox 같은 별도 경계가 없다면 correctness capability로 승인하지 않습니다. 11. **대상 버전·topology artifact를 확인합니다.** workflow 정의가 아니라 실제 manifest와 JUnit result를 확인합니다. ### 로컬 qualification 실행 다음 명령은 저장소 루트에서 각각 독립적으로 실행할 수 있습니다. 이번 작업에서는 기본 module test만 성공했으며 topology lane은 실행하지 않았습니다. Standalone: ```bash # 저장소 루트에서 실행 REDIS_VERSION=7.4 docker compose -f infra/redis-sdk/standalone/compose.yml up -d --wait (cd src && ./gradlew :adapter:outbound:cache-redis:redisTopologyTest \ -Predis.topology.host=localhost \ -Predis.topology.port=6379 \ -Predis.topology.mode=standalone \ --console=plain) ``` Sentinel: ```bash # 저장소 루트에서 실행 REDIS_VERSION=7.4 docker compose -f infra/redis-sdk/sentinel/compose.yml up -d --wait (cd src && ./gradlew :adapter:outbound:cache-redis:redisTopologyTest \ -Predis.topology.host=localhost \ -Predis.topology.port=27010 \ -Predis.topology.mode=sentinel \ -Predis.topology.master=skeleton \ --console=plain) ``` Cluster: ```bash # 저장소 루트에서 실행 REDIS_VERSION=7.4 docker compose -f infra/redis-sdk/cluster/compose.yml up -d --wait (cd src && ./gradlew :adapter:outbound:cache-redis:redisTopologyTest \ -Predis.topology.host=localhost \ -Predis.topology.port=7100 \ -Predis.topology.mode=cluster \ --console=plain) ``` TLS: ```bash # 저장소 루트에서 실행 REDIS_VERSION=7.4 docker compose -f infra/redis-sdk/tls/compose.yml up -d --wait docker compose -f infra/redis-sdk/tls/compose.yml \ cp redis:/tls/ca.crt /tmp/redis-lane-ca.pem (cd src && ./gradlew :adapter:outbound:cache-redis:redisTopologyTest \ -Predis.topology.host=127.0.0.1 \ -Predis.topology.port=6390 \ -Predis.topology.mode=tls \ -Predis.topology.trust-material=/tmp/redis-lane-ca.pem \ --console=plain) ``` 종료할 때는 실제로 실행한 lane만 지정합니다. `down -v`는 해당 테스트 fixture의 volume과 데이터까지 제거합니다. ```bash # 저장소 루트에서 실행 REDIS_LANE=standalone # sentinel, cluster, tls 중 실행한 lane으로 변경 case "${REDIS_LANE}" in standalone|sentinel|cluster|tls) ;; *) echo "unsupported Redis lane: ${REDIS_LANE}" >&2; exit 2 ;; esac docker compose -f "infra/redis-sdk/${REDIS_LANE}/compose.yml" down -v ``` 원본 명령과 topology별 endpoint 설명은 [`infra/redis-sdk/README.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/infra/redis-sdk/README.md:27)와 [`infra/redis-sdk/README.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/infra/redis-sdk/README.md:67)에 있습니다. ### 장애 시 분기 1. `redisOptional=DEGRADED`이고 correctness 역할이 없다면 pod를 제거하기 전에 원본 저장소 부하와 cache bypass율을 확인합니다. 2. `redisRequired=DOWN`이면 신규 traffic을 받지 않게 하고 Redis endpoint와 TLS 상태를 확인합니다. 반대로 `UP`이어도 확인된 것은 `PING` reachability뿐이므로 ACL·capability·durability는 별도 검사 결과를 봅니다. 3. `NOREPLICAS`가 증가하면 write를 억지로 재시도하기보다 replica 연결·lag와 `min-replicas-*`를 복구합니다. 이는 silent loss를 막는 의도된 거절입니다. 4. ambiguous write가 발생하면 command family별 reconciliation 절차를 실행합니다. increment, charge, enqueue 같은 non-idempotent write는 단순 재시도하지 않습니다. 5. `ASK`·`TRYAGAIN`과 resharding observer가 함께 보이면 slot migration 진행 상태와 tail latency를 확인합니다. 6. blocking lane만 포화되면 consumer 수와 max connection을 비교하고 regular lane 상태를 별도로 봅니다. 7. `NOSCRIPT`가 발생하면 semantic script는 request path에서 한 번 자동으로 reload·재평가됩니다. 계속 실패하면 caller가 반복 재시도하지 말고 advanced credential의 `SCRIPT LOAD`·`EVALSHA` ACL, Redis의 script cache flush·restart, 배포된 script source와 digest 상태를 확인합니다. ## 13. 업그레이드와 rollback gate Redis server나 Lettuce 버전 변경은 일반 dependency bump로 다루기 어렵습니다. command metadata, reply shape, ACL category, driver failover behavior가 함께 달라질 수 있기 때문입니다. 저장소의 [`upgrade-guide.md`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/redis/upgrade-guide.md:1)는 다음 순서를 요구합니다. ### 1단계: command metadata diff 새 server가 보고하는 모든 command를 `redis-command-policy.yml`과 비교합니다. 새 command가 자동 허용되지는 않지만, upstream에서 기존 command의 risk가 달라졌는데 local catalog가 오래된 경우를 찾아야 합니다. ### 2단계: ACL regression 모든 account와 SDK가 발행할 수 있는 command 조합을 `ACL DRYRUN`으로 확인합니다. Redis version이 command의 ACL category를 바꾸면 첫 실요청에서야 권한 오류가 날 수 있습니다. ### 3단계: serializer golden bytes 새 코드의 round-trip만 보지 말고 이전 version이 쓴 byte를 새 version이 decode하는지 확인합니다. 저장 형식 변경은 topology test와 별도의 data migration 문제입니다. ### 4단계: support matrix와 topology evidence `support-matrix.md`를 갱신하고 standalone·Sentinel·Cluster·TLS 중 claim하는 lane을 실제로 실행합니다. 더 높은 version number가 이전 behavior를 자동으로 보장하지 않습니다. ### 5단계: rollback material 기록 변경 전 다음을 보존합니다. - 이전 Redis server image와 digest - 이전 Lettuce lock version - 등록된 모든 script의 `SCRIPT LOAD` digest - topology별 JUnit evidence와 manifest rollback 후 이전 script digest가 다시 resolve되는지 확인해야 합니다. process가 이전 server에 없는 digest를 cache하면 모든 scripted call이 `NOSCRIPT`로 실패할 수 있습니다. data shape가 바뀌는 upgrade는 이 gate의 범위 밖이므로 별도 migration·backfill·rollback plan이 필요합니다. ## 14. 현재 저장소가 운영 배포에 남겨 둔 공백 이 모듈은 application-side guardrail과 qualification에는 많은 결정을 담고 있지만, production Redis 자체를 배포하는 저장소는 아닙니다. ### Redis가 기본 application Compose에 없습니다 루트 [`docker-compose.yml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docker-compose.yml:26)과 [`docker-compose.local.yml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docker-compose.local.yml:16)은 application과 PostgreSQL 중심이며 Redis service를 제공하지 않습니다. local compose가 읽는 `.env`에서도 Redis와 역할 selector는 기본적으로 비활성화돼 있습니다. 즉 개발자가 `APP_REDIS_ENABLED=true`만 켜도 함께 시작되는 Redis는 없습니다. 별도 instance나 qualification lane을 준비해야 합니다. ### Redis용 Helm·Kubernetes·Kustomize 배포 정의가 없습니다 현 HEAD의 저장소 전체를 확인했지만 Redis용 chart, StatefulSet, Service, PDB, NetworkPolicy, PVC, backup job은 없습니다. 따라서 플랫폼 계층에서 최소한 다음을 별도로 소유해야 합니다. - topology별 workload와 service discovery - persistence와 storage class - backup, restore, point-in-time 요구 - memory limit, `maxmemory`, eviction policy - replica placement, anti-affinity, PDB - TLS certificate 발급·rotation과 secret mount - ACL user·password rotation - `min-replicas-*`의 모든 primary 후보 적용 - monitoring, alert, maintenance와 resharding runbook ### qualification fixture는 durability를 검증하지 않습니다 Standalone·Sentinel·Cluster·TLS fixture는 모두 AOF와 snapshot을 끕니다. container 종료 후 데이터 보존, disk full, AOF rewrite, RDB restore, backup consistency를 검증하지 않습니다. host networking과 고정 포트를 쓰는 Sentinel·Cluster lane은 로컬 qualification에 맞춘 선택이며 multi-tenant CI runner나 desktop 환경에서 port conflict가 날 수 있습니다. ### Lease replay handle이 server lease보다 오래 살아 있다고 판단할 수 있습니다 same-attempt acquire replay에서 Lua는 TTL을 연장하지 않고 현재 PTTL을 반환합니다. 그러나 adapter는 그 PTTL을 버리고 request TTL로 local validity를 다시 만듭니다. Redis key가 곧 만료되더라도 replay handle은 더 오래 `ACTIVE`라고 판단할 수 있고, `observedServerExpiry`도 실제 server PTTL이 아닌 local 계산값입니다. 이는 fencing 부재와 별개의 local-validity 공백입니다. [`LeaseScripts.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/lease/LeaseScripts.java:35), [`RedisDistributedLeaseAdapter.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/lease/RedisDistributedLeaseAdapter.java:149) ### Idempotency V2는 same-attempt action을 한 번으로 합치지 못합니다 같은 owner·operation의 record가 이미 `EXECUTING`이어도 claim은 `REPLAYED_ACQUIRE`를 반환할 수 있고, executor는 `ALREADY_STARTED_SAME_OPERATION`이나 inspect의 `EXECUTING_SAME_OPERATION`을 action 실행 권한으로 해석합니다. 따라서 같은 retained attempt의 두 Java invocation이 action을 중복 실행할 수 있습니다. 또한 Redis renew는 `EXECUTING -> EXECUTING` transition이라 target-state 선검사에서 `ALREADY`로 끝나 `leaseUntil`, Redis TTL, revision을 갱신하지 않습니다. effect 자체가 idempotent하거나 effect-point CAS·outbox가 없다면 이 조립만으로 exactly-once 또는 correctness를 승인하면 안 됩니다. [`IdempotencyExecutorV2.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/application-core/src/main/java/dev/caskeleton/application/idempotency/v2/IdempotencyExecutorV2.java:128), [`IdempotencyScripts.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/idempotency/IdempotencyScripts.java:67), [`RedisIdempotencyStoreAdapter.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/idempotency/RedisIdempotencyStoreAdapter.java:181) ### Redis session 구현이 완결되지 않았습니다 `redis-session` selector와 filter configuration은 있지만 `redisVersionedSessionRepository` bean의 실제 producer를 찾을 수 없습니다. web config test도 Redis repository 대신 `MapSessionRepository`를 주입합니다. 이 repository만 추가해도 완성되지는 않습니다. session branch는 CSRF, `IF_REQUIRED`, fixation migration, primitive context repository를 설정하지만 snapshot이 없는 요청에서 인증된 `Authentication` 객체를 최초로 만드는 production login mechanism은 확인되지 않습니다. 따라서 현 조립 상태는 5개 역할 중 4개이며, session은 persistence와 최초 인증 두 공백을 해결하고 end-to-end로 검증할 때까지 blocked입니다. correctness predicate가 `redisRequired`를 readiness에 넣더라도 provider나 인증 경로의 존재를 증명하지 않습니다. consumer 쪽 요구는 [`AuthenticationModeCompositionConfig.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/security/AuthenticationModeCompositionConfig.java:22), web 설정은 [`RedisSessionWebConfig.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/auth/RedisSessionWebConfig.java:11), security branch는 [`SecurityConfig.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/auth/SecurityConfig.java:102), test fixture는 [`RedisSessionWebConfigTest.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/auth/RedisSessionWebConfigTest.java:37)에서 확인할 수 있습니다. ### raw credential isolation은 composition 연결을 재검토해야 합니다 현 HEAD는 raw credential을 해석해 `RedisCredentialRole.RAW` client를 만들 수 있지만, `RedisConnectionKind`에는 RAW lane이 없고 `RAW_GATEWAY` command access는 `REGULAR` lane으로 분류됩니다. 또한 `LettuceRedisRawGateway`의 production bean composition을 찾을 수 없습니다. 즉 설정·ACL fixture에 표현된 raw account가 실제 runtime path에 연결되는지는 완결된 조립 근거가 부족합니다. [`RedisConnectionKind.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/RedisConnectionKind.java:49), [`RedisSdkAutoConfiguration.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkAutoConfiguration.java:182), [`LettuceRedisRawGateway.java`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/raw/LettuceRedisRawGateway.java:17)를 함께 검토해야 합니다. ### README와 registry를 code보다 먼저 믿으면 안 됩니다 현 module README는 client, semantic port, health가 아직 없다고 설명하지만 실제 현 HEAD에는 구현과 테스트가 있습니다. topology mode 설명도 TLS lane을 빠뜨립니다. support matrix의 Lettuce 6.8.2 기록은 실제 6.8.1 lock과 다르고, 일부 cache env key는 registry에서 orphaned라고 표시됐지만 `application.yml`이 계속 사용합니다. 운영 문서 갱신 전까지 우선순위는 다음처럼 두는 편이 안전합니다. ```text dependency lock / runtime code / executable test gate > generated metadata와 env registry > README와 과거 계획 문서 ``` 문서도 build gate의 일부여야 하지만, 현재는 서로 다른 시점의 사실이 섞여 있습니다. ## 마무리: Redis 운영 계약은 성공 경로보다 거절 경로에 드러납니다 이 Redis 모듈의 중심은 빠른 get/set wrapper가 아닙니다. Redis를 사용하지 않는 배포에는 리소스를 만들지 않고, 사용하는 배포에는 역할과 topology를 명시하게 합니다. cache와 correctness 역할에 서로 다른 readiness 정책을 적용하고 실제 `PING` reachability를 조립한 부분은 현 production 동작입니다. capability·permit·namespace·slot·budget·timeout admission과 실행 확실성 translator, Sentinel durability probe는 구현과 테스트가 있지만 production path에는 아직 연결되지 않았습니다. 동시에 production deployment는 아직 완성품이 아닙니다. Redis용 Helm/Kubernetes, persistence, backup/restore, eviction과 resource 정책, credential rotation이 없고, session persistence·최초 인증과 일부 secret·raw composition 계약에는 공백이 있습니다. Lease replay의 local validity와 Idempotency V2의 same-attempt 중복 실행·renew도 운영 승인 전에 보완하거나 상위 effect 경계로 제한해야 합니다. CI workflow가 넓은 version matrix를 정의하지만 실제 certification은 artifact와 support matrix가 함께 증명해야 합니다. Lettuce도 문서의 6.8.2가 아니라 lockfile의 `6.8.1.RELEASE`가 현재 기준입니다. 플랫폼 팀이 이 템플릿을 채택할 때의 완료 조건은 “애플리케이션이 Redis에 연결됐다”가 아닙니다. 4/5 capability 상태와 session 차단을 명시하고, 역할별 failure policy, 모든 primary 후보의 durability 설정, ACL과 TLS, lane별 capacity, 실제 topology evidence, 복구 가능한 persistence, upgrade와 rollback artifact를 하나의 운영 계약으로 맞춰야 합니다. 여기에 현재 미조립인 capability·durability probe와 command guard를 production path에 연결하고 검증하는 작업도 포함됩니다. ## 시리즈에서 다시 찾기 - 전체 지도: [Redis를 범용 클라이언트가 아니라 정책 경계로 다루기](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-backend-policy-boundary.md) - 이전 글: [Redis 테스트가 증명하는 것과 증명하지 않는 것](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-testing-topology-ci.md) - 런타임 조립: [app.redis.enabled에서 capability bean까지](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-spring-composition.md) - 장애 판정: [같은 Redis 장애가 DEGRADED와 DOWN으로 갈리는 코드](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-health-readiness-observability.md)