Files
document-haness/.run/redis/redis-settings-secrets-credentials.md
T

198 lines
24 KiB
Markdown

# Redis 설정은 어떻게 실패하는가: 바인딩·검증·Secret·Credential 추적
> **Redis 코드 상세 시리즈 04/20** · [전체 지도](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-backend-policy-boundary.md) · 이전: [app.redis.enabled에서 capability bean까지: Spring 조립 코드 읽기](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-spring-composition.md) · 다음: [하나의 설정에서 세 topology로: RedisTopologyClientFactory 코드 읽기](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-topology-client-factory.md)
## 이 글이 답하는 코드 질문
Redis 설정에는 endpoint, topology, timeout, pool ceiling, TLS, ACL account가 함께 들어갑니다. 이 값들은 언제 binding되고, 어느 단계에서 거절되며, `secret://...` reference는 어떻게 실제 username/password가 될까요? 이 글은 Spring property에서 `RedisURI`에 전달될 credential까지의 경로와 현재 environment/secret registry drift를 구분합니다.
## 코드 지도
| 코드 | 입력 | 출력 | 실패 위치 |
|---|---|---|---|
| [`RedisSdkSettings`](/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:9) | `app.redis.*` | typed 설정과 warning 목록 | `validate()` |
| [`RedisSdkAutoConfiguration.redisSdkSettings()`](/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:83) | Spring binder | bound settings bean | binding failure |
| [`RedisCredentialResolver`](/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) | purpose + `secret://<source>/<name>` | optional `RedisCredentials` | malformed/unresolved reference |
| [`RedisResolvedCredentials`](/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:354) | role별 credential | immutable role map + Sentinel credential | client factory 이전 |
| [`SecretSourceConfig`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/SecretSourceConfig.java:8) | strategy + environment | application `SecretSource` | backend 생성 |
| [`SecretSourceValidator`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/SecretSourceValidator.java:11) | profile, role selectors, secret source | prod secret contract | singleton 초기화 종료 시점 |
| [`env-keys.yaml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/env-keys.yaml:1885) | 환경 키 계약 | 분류·기본값·required_when | registry test/build gate |
| [`secrets-classification.yaml`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/secrets-classification.yaml:78) | secret 이름 | source·rotation·masking 계약 | registry contract test |
## Redis-off에서는 binding도 하지 않습니다
`RedisSdkSettings`에는 일부 유효한 local default가 있지만 class 자체에는 `@ConfigurationProperties`가 없습니다. 이유는 [`class 설명](/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:17)에 적혀 있습니다. application-wide scan이 이 type을 발견하면 Redis를 쓰지 않는 deployment도 값을 binding하고 검증하게 됩니다.
실제 등록은 `app.redis.enabled=true` 조건 아래의 [`redisSdkSettings()`](/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:83)만 합니다. switch가 없거나 false이면 다음 모두 생략됩니다.
- `app.redis.*` binding
- cross-field validation
- credential reference resolution
- raw allowlist와 TLS material 읽기
- client/event loop/runtime owner 생성
[`disabledIgnoresMalformedRedisConfiguration()`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkAutoConfigurationTest.java:71)은 switch-off 상태에서 Cluster non-zero database, 빈 nodes, zero timeout 같은 값도 context에 영향을 주지 않는다고 고정합니다.
## bind → validate → resolve 순서
Spring은 factory method가 settings 객체를 반환한 다음 configuration property를 채웁니다. 그래서 factory method 안에서 `validate()`를 부르면 아직 default만 검사하게 됩니다. 별도 validation bean이 settings에 의존하는 이유입니다.
```mermaid
sequenceDiagram
participant B as Spring Binder
participant S as RedisSdkSettings
participant V as SettingsValidation bean
participant R as RedisCredentialResolver
participant SS as RedisSecretSource
participant F as TopologyClientFactory
B->>S: app.redis.* binding
V->>S: validate()
S-->>V: warnings 또는 IllegalStateException
V->>V: raw policy resource probe
R->>SS: reference의 name resolve
SS-->>R: secret 또는 empty
R-->>F: role별 username/password
```
[`redisSdkSettingsValidation()`](/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:101)은 warning을 log하고 raw gateway가 켜졌다면 allowlist resource가 실제로 읽히는지 확인합니다. `redisResolvedCredentials()`는 이 validation bean을 parameter로 받아 순서를 강제합니다.
## `RedisSdkSettings.validate()`가 거절하는 것
핵심 cross-field rule은 [`validate()`](/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:58)에 모여 있습니다.
### topology와 namespace
- Cluster에서 database가 0이 아니면 거절합니다.
- 음수 database와 빈 node 목록을 거절합니다.
- Sentinel이면 `app.redis.sentinel.master-name`이 필요합니다.
- namespace의 `environment`, `service`, `domain``RedisKeyRules.requireToken()`을 통과해야 합니다.
Standalone node가 정확히 하나인지, `host:port` 문법인지 여부는 settings가 아니라 topology factory가 검사합니다. settings validation이 성공해도 client factory 단계에서 실패할 수 있습니다.
### timeout과 limit
fast, collection, script, batch, admin timeout은 모두 양수이며 30초 이하여야 합니다. fast timeout이 5초를 넘으면 failure가 아니라 warning입니다. 기본값은 [`Timeouts` field](/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:188)에서 각각 500ms, 2s, 1s, 2s, 3s입니다.
blocking `maxBlock`은 양수여야 하고 blocking/transaction connection ceiling도 1 이상이어야 합니다. key/value/stream/hash/batch/scan/offline queue/bitmap limit은 모두 양수이며 key byte limit은 `RedisKeyRules.MAX_KEY_BYTES`를 넘을 수 없습니다. capacity의 in-flight command/byte/reply ceiling도 양수여야 합니다.
여기서 양수 검증과 runtime 적용을 구분해야 합니다. [`limits.offlineQueueCommands`](/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:250)는 기본값이 1,000이고 1 미만이면 거절되지만, production main source에서 [`getOfflineQueueCommands()`](/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:344)를 호출하는 코드는 없습니다. Lettuce의 실제 `requestQueueSize`는 이 값이 아니라 [`capacity.maximumInFlightCommands`](/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:307)를 사용합니다. 두 기본값도 각각 1,000과 64로 다릅니다.
### TLS와 lifecycle
mTLS client certificate를 지정했는데 client key reference가 없으면 실패합니다. TLS가 켜졌지만 hostname verification을 끄면 warning입니다. lifecycle은 nonblank client name, positive connect/TLS-handshake/acquire/shutdown/drain timeout, nonnegative quiet period, `quietPeriod <= shutdownTimeout`을 요구합니다. 이 규칙은 [`Lifecycle.validate()`](/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:568)에 있습니다.
현재 `tlsHandshakeTimeout``acquireTimeout`도 binding과 validation은 되지만 production 사용처가 getter 외에는 확인되지 않습니다. owner는 pool 포화 시 즉시 거절하며 acquire timeout 동안 대기하지 않습니다. 이들 setting과 `offlineQueueCommands`를 runtime에 적용된 값으로 설명하면 안 됩니다.
### raw, admin, advanced
raw gateway가 켜지면 nonblank policy resource와 raw 전용 credential reference가 필요합니다. admin plane이 켜지면 admin credential reference가 필요합니다. advanced operation이 꺼진 상태에서 advanced policies를 설정하면 실패합니다.
raw resource의 nonblank 검사는 settings가 하고, 존재/가독성 검사는 [`requireRawPolicyResource()`](/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:119)가 합니다. 기본 raw path는 `classpath:redis-sdk/raw-command-allowlist.yml`이지만 해당 이름의 resource를 leaf가 제공하지 않습니다. raw를 실제로 켤 때는 존재하는 resource로 명시해야 합니다.
### authentication
application credential reference가 없으면 기본적으로 startup failure입니다. local anonymous Redis를 쓰려면 `app.redis.authentication.anonymous-access-accepted=true`를 명시해야 하고, 이 경우 warning을 남깁니다. advanced credential이 없으면 script가 application account로 fallback한다는 warning을 남깁니다. [`Authentication.validate()`](/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:406)가 이 두 trade-off를 구분합니다.
## credential reference 해석
허용 문법은 `secret://<source>/<name>`입니다. named ACL user를 지정하려면 source segment를 `<user>@<source>`로 씁니다.
예를 들어 `secret://ca-skeleton-application@environment/APP_REDIS_PASSWORD`는 다음으로 분해됩니다.
- scheme: `secret://`
- ACL username: `ca-skeleton-application`
- source label: `environment`
- secret name: `APP_REDIS_PASSWORD`
[`RedisCredentialResolver.resolve()`](/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:45)는 reference가 blank이면 `Optional.empty()`를 반환합니다. scheme이 다르거나 source/name separator가 없으면 configuration error입니다. secret source가 null/blank 값을 반환하면 connection 생성 전 startup failure입니다.
source segment에 `@`가 없으면 username은 `default`입니다. 이 동작은 [`usernameOf()`](/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:100)에 있습니다. `RedisCredentials.toString()`은 password를 `***`로 바꿔 출력합니다.
`source` 문자열은 현재 backend routing에 쓰이지 않습니다. resolver는 마지막 path name만 `secretSource.apply(name)`에 넘깁니다. 즉 `secret://vault/NAME`이라고 써도 `vault` backend를 자동 선택하지 않습니다. 실제 backend는 application의 `SecretSourceConfig`가 선택합니다.
## 역할별 credential과 fallback
[`redisResolvedCredentials()`](/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:156)은 다음 순서로 account를 해석합니다.
1. `APPLICATION`
2. `ADVANCED`
3. `PUBSUB`
4. admin enabled일 때 `ADMIN`
5. raw enabled일 때 `RAW`
6. Sentinel mode일 때 별도 Sentinel control credential
설정되지 않은 advanced/pubsub role은 map에 들어가지 않습니다. topology factory의 role router가 해당 lane을 application client로 보냅니다. admin과 raw는 enabled 상태에서 reference가 필수이므로 암묵적으로 application account에 내려가지 않습니다.
Sentinel credential은 data primary account와 다릅니다. Sentinel control plane이 primary 위치를 조회할 때 쓸 credential이고 application credential은 발견된 primary에 명령을 보낼 때 씁니다.
## application SecretSource와 prod validator
기본 application backend는 [`EnvironmentSecretSource`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/EnvironmentSecretSource.java:10)입니다. Spring `Environment`에서 key를 읽고 null/blank를 empty로 바꿉니다. [`SecretSourceFactory`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/SecretSourceFactory.java:13)의 enum switch에는 현재 `ENVIRONMENT`만 있습니다.
`RedisCapabilityConfig.redisSdkSecretSource()`가 application `SecretSource`를 SDK interface에 연결합니다. 따라서 정상적인 `app-bootstrap` 실행에서는 SDK의 `System.getenv()` fallback 대신 configured backend를 사용합니다.
[`SecretSourceValidator.afterSingletonsInstantiated()`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/SecretSourceValidator.java:60)은 prod profile에서 두 검사를 합니다.
- property source에 `__LOCAL_DEV_` prefix 값이 있으면 거절합니다.
- `REQUIRED_PROD_SECRETS` 중 현재 Redis role에 필요한 secret이 없으면 거절합니다.
Redis secret은 global switch와 role selector가 모두 맞을 때만 요구됩니다. cache, rate-limit, session, idempotency, lease prefix를 따로 판정하며 알 수 없는 Redis role은 Redis-on일 때 fail-closed로 요구합니다.
## environment/secret registry drift
현재 production code와 registry 사이에는 중요한 불일치가 있습니다.
첫째, SDK와 topology tests는 application credential 예시로 `APP_REDIS_PASSWORD`를 사용합니다. 그러나 `env-keys.yaml``secrets-classification.yaml`에는 `APP_REDIS_PASSWORD` entry가 확인되지 않습니다. 대신 classification registry는 [`APP_CACHE_REDIS_PASSWORD`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/secrets-classification.yaml:78), [`APP_RATE_LIMIT_REDIS_PASSWORD`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/secrets-classification.yaml:116), [`APP_SESSION_REDIS_PASSWORD`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/secrets-classification.yaml:152) 같은 이전 role별 이름을 유지합니다.
둘째, `SecretSourceValidator.REQUIRED_PROD_SECRETS`도 이 role별 legacy secret 이름을 요구합니다. 반면 SDK는 `app.redis.authentication.credential-reference`에 적힌 임의의 `<name>`을 해석합니다. validator는 실제 reference target을 읽지 않습니다.
그 결과 prod deployment가 `APP_REDIS_PASSWORD`를 올바르게 주입하고 reference를 그 이름으로 설정해도, 선택한 role에 따라 `APP_CACHE_REDIS_PASSWORD``APP_RATE_LIMIT_REDIS_PASSWORD`가 없다는 별도 startup failure를 만날 수 있습니다. 반대로 registry가 요구한 role별 password가 있어도 SDK reference가 다른 이름을 가리키면 SDK resolver에서 실패합니다.
셋째, `env-keys.yaml``app.redis.*` typed settings가 `application.yml`에 없고 generated configuration metadata와 대조된다고 설명합니다. 이 구조는 intentional입니다. 따라서 `application.yml``APP_REDIS_NODES` placeholder가 없다는 사실 자체는 drift가 아닙니다. 문제는 credential material의 실제 reference target과 prod required-secret 목록이 서로 다른 SSOT를 가진다는 점입니다.
넷째, `APP_REDIS_LIFECYCLE_ACQUIRE_TIMEOUT``APP_REDIS_LIFECYCLE_TLS_HANDSHAKE_TIMEOUT`은 [`env-keys.yaml` runtime 설정 구간](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/env-keys.yaml:2394)에 등록되어 있지만 현행 runtime 적용 코드를 찾지 못했습니다. [`APP_REDIS_LIMITS_OFFLINE_QUEUE_COMMANDS`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/docs/registries/env-keys.yaml:2165)도 public configuration으로 등록되어 binding·validation되지만, 값을 바꿔도 현행 Lettuce `requestQueueSize`는 바뀌지 않습니다. 실제 queue ceiling의 입력은 `capacity.maximumInFlightCommands`입니다.
## 정상·실패 분기 요약
| 단계 | 정상 | 실패 |
|---|---|---|
| switch 조건 | off이면 완전 생략 | off + Redis role은 activation validator failure |
| binding | typed value로 변환 | duration/enum/type binding 오류 |
| settings validation | warning 또는 validated settings | cross-field `IllegalStateException` |
| resource validation | raw/TLS resource 읽기 가능 | startup failure |
| credential parse | optional role 또는 parsed reference | literal/malformed reference 거절 |
| secret resolve | nonblank secret | connection 전에 `resolved to nothing` |
| prod secret contract | selected role secret 존재 | legacy required list와 실제 reference drift 가능 |
이 구간의 failure는 command가 전송되기 전이므로 execution certainty는 `NOT_SENT` 성격입니다. 실제 authentication 실패는 connection이 lazy하게 열릴 때 발생할 수 있습니다. reference resolution 성공은 server가 password를 받아들였다는 증명이 아닙니다.
## 테스트가 고정하는 계약
[`RedisSdkSettingsTest`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkSettingsTest.java:1)는 topology/database, timeout, lane ceiling, TLS, raw/admin, authentication warning과 failure를 직접 고정합니다.
[`RedisSdkAutoConfigurationTest`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkAutoConfigurationTest.java:130)는 configured role별 secret source 호출 횟수와 unresolved/malformed reference의 startup failure를 확인합니다. [`configuredAccountsAreResolvedPerRole()`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkAutoConfigurationTest.java:330)는 application/advanced/pubsub account map을 고정합니다.
[`RequiredWhenIsEnforcedTest`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RequiredWhenIsEnforcedTest.java:35)는 env registry의 `required_when` 조건을 context failure와 대조합니다. [`SecretsClassificationRegistryTest`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/SecretsClassificationRegistryTest.java:1)는 validator의 required secret list와 classification registry를 1:1로 맞춥니다. 이 테스트들은 두 registry가 서로 일치함을 보이지만 SDK reference target과의 일치까지 보이지는 않습니다.
[`SecretSourceValidatorTest`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/runtime/SecretSourceValidatorTest.java:1)는 prod/local sentinel과 role별 조건을 고정합니다.
## 현재 한계와 다음 source 순서
- credential rotation은 restart-only입니다. runtime refresh/dual credential handover가 조립되지 않았습니다.
- `secret://`의 source segment는 현재 backend selector가 아니라 문법·username carrier입니다.
- `RedisStartupProbe` production 조립이 없어 reference resolution 뒤 실제 authentication과 server fact 확인은 lazy connection/request에 남습니다.
- prod required secret validator와 실제 SDK credential reference target은 정렬되지 않았습니다.
- lifecycle acquire/TLS handshake timeout과 `limits.offlineQueueCommands`는 registry와 settings에는 있으나 runtime 적용이 확인되지 않습니다. 현행 Lettuce `requestQueueSize`는 별도 capacity setting을 사용합니다.
다음에는 [`RedisSdkSettings.validate()`](/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:58), [`RedisCredentialResolver.resolve()`](/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:45), [`redisResolvedCredentials()`](/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:156), [`SecretSourceValidator`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/SecretSourceValidator.java:94) 순서로 읽으면 됩니다.
관련 시리즈 주제는 topology별 URI/client 생성과 role별 lane routing입니다.
## 시리즈에서 이어 읽기
- 이전 글: [app.redis.enabled에서 capability bean까지: Spring 조립 코드 읽기](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-spring-composition.md)
- 다음 글: [하나의 설정에서 세 topology로: RedisTopologyClientFactory 코드 읽기](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-topology-client-factory.md)
- 전체 흐름: [Redis를 범용 클라이언트가 아니라 정책 경계로 다루기](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-backend-policy-boundary.md)
- 운영 흐름: [Redis를 켠다는 말의 운영적 의미: 단일 활성화 스위치에서 Sentinel 쓰기 손실 검증까지](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-platform-sre-operations.md)