Files
document-haness/.run/redis/redis-spring-composition.md
T

23 KiB

app.redis.enabled에서 capability bean까지: Spring 조립 코드 읽기

Redis 코드 상세 시리즈 03/20 · 전체 지도 · 이전: Redis 모듈 해부: Gradle leaf에서 app-bootstrap까지 · 다음: Redis 설정은 어떻게 실패하는가: 바인딩·검증·Secret·Credential 추적

이 글이 답하는 코드 질문

app.redis.enabled=true는 Redis 기능 전체를 켜는 selector가 아닙니다. 이 값은 공통 SDK runtime을 만들 권한이고, cache·rate-limit·lease·idempotency·session은 각자의 selector를 가집니다. 이 글은 Spring context refresh 동안 어떤 조건과 method가 어떤 bean을 만드는지, 그리고 현재 5개 semantic role 중 왜 4개만 production 조립되는지를 추적합니다.

코드 지도

클래스·리소스 입력 출력 다음 호출
AutoConfiguration.imports classpath RedisSdkAutoConfiguration 등록 global switch 조건
RedisSdkAutoConfiguration app.redis.*, secret source, resource loader settings, credentials, client, owner, health beans topology factory
RedisCapabilityConfig owner, settings, capability selector/settings 4종 semantic port와 V2 executor request-time adapter
RedisCapabilitySettings ca-skeleton.capabilities.* cache/rate-limit/lease/idempotency 세부 설정 각 bean factory method
RedisActivationValidator global switch와 5개 role selector 정상 종료 또는 startup failure 없음
SecretSourceConfig secret source strategy, environment SecretSource, 두 startup validator Redis secret bridge

객체 생성 시점: 두 composition root

SDK 쪽 auto-configuration은 @ConditionalOnPropertyapp.redis.enabled=true를 요구합니다. 값이 false이거나 property가 없으면 이 클래스가 제공하는 bean은 만들어지지 않습니다.

bootstrap 쪽 RedisCapabilityConfig도 같은 global condition을 사용합니다. 두 class의 책임은 다릅니다.

  • RedisSdkAutoConfiguration: provider 공통 runtime을 만듭니다.
  • RedisCapabilityConfig: deployment가 선택한 provider-neutral semantic port를 그 runtime 위에 만듭니다.

이 분리는 Redis onRedis가 어떤 역할을 맡음을 같은 뜻으로 만들지 않습니다. global switch만 켜고 role을 하나도 고르지 않으면 client와 owner는 있지만 semantic port는 없습니다. noRoleComposesNoPort()가 이 상태를 고정합니다.

context refresh 호출 순서

sequenceDiagram
    participant E as Environment
    participant S as RedisSdkAutoConfiguration
    participant V as Settings validation
    participant F as TopologyClientFactory
    participant O as RedisRuntimeOwner
    participant C as RedisCapabilityConfig
    participant A as RedisActivationValidator
    E->>S: app.redis.enabled 평가
    S->>V: bind RedisSdkSettings 후 validate
    V->>S: warnings 또는 예외
    S->>S: credential reference resolve
    S->>F: validated settings + credentials
    F-->>S: RedisRuntimeClient
    S->>O: lane limit + drain timeout
    C->>C: role selector별 semantic bean 생성
    A->>E: off + Redis role 모순 검사
    A-->>E: 정상 또는 모든 모순을 묶은 startup failure

세부 순서는 bean dependency로 고정됩니다.

  1. redisSdkSettings()가 mutable settings 객체를 만들고 @ConfigurationProperties(prefix="app.redis")로 binding합니다.
  2. redisSdkSettingsValidation()validate()와 raw allowlist resource 검사를 실행합니다.
  3. redisResolvedCredentials()는 validation bean에 의존하므로 검증 뒤 reference를 해석합니다.
  4. redisRuntimeClient()가 topology factory를 호출합니다. client object와 event-loop resource는 이때 생기지만 lane connection은 아직 열리지 않습니다.
  5. redisRuntimeOwner()가 여섯 lane의 ceiling과 drain timeout을 받습니다.
  6. RedisCapabilityConfig의 조건이 맞는 factory method만 semantic bean을 만듭니다.

실제 Redis TCP connection은 request-time에 owner가 처음 borrow()할 때 RedisRuntimeClient.openLane()을 호출하며 lazy하게 열립니다. 따라서 bean graph가 성공했다는 사실만으로 endpoint 접속과 인증 성공을 증명하지 않습니다.

context close에는 두 client shutdown 경로가 겹칩니다

생성 dependency 때문에 context 종료 시 redisRuntimeOwner()이 client bean보다 먼저 destroy됩니다. owner의 explicit close()는 lane을 drain한 뒤 내부에서 runtime client를 닫습니다. 그러나 redisRuntimeClient()는 destroy inference를 끄지 않은 일반 @Bean입니다. 반환 type인 RedisRuntimeClientAutoCloseable을 확장하고 public no-arg close()를 노출합니다. Spring이 다음으로 client bean의 inferred destroy를 실행하면 같은 client의 close()가 다시 호출될 수 있습니다.

따라서 auto-configuration 주석의 “owner before client”는 종료 순서를 설명하지만 client shutdown authority가 owner 하나뿐임을 보장하지는 않습니다. owner state가 CLOSED인지 확인하는 context test와 종료 후 Lettuce thread가 남지 않는 live test는 있지만, runtime client close 횟수를 세는 context-level test는 없습니다.

SecretSource bridge가 필요한 이유

SDK는 RedisSecretSource라는 작은 interface만 압니다. bean이 없으면 process environment를 직접 읽는 fallback을 씁니다.

애플리케이션은 별도의 SecretSource를 composition root에서 선택합니다. redisSdkSecretSource()는 이를 method reference로 SDK에 연결합니다. 이 bridge가 없으면 향후 secret manager backend를 선택해도 Redis만 process environment를 직접 읽게 됩니다.

4/5 semantic composition

현재 RedisCapabilityConfig가 production bean으로 만드는 역할은 네 가지입니다.

Cache

ca-skeleton.capabilities.cache.bindings.default=redis이면 redisDefaultCacheRegion()이 실행됩니다. method는 cache TTL을 검증하고, key HMAC secret을 해석한 뒤 RedisCacheRegionAdapter<String, byte[]>CacheRegionPort로 반환합니다.

정상 출력은 cache port 하나입니다. soft TTL이 hard TTL보다 크거나 hard TTL이 floor보다 작거나 command timeout/key version이 유효하지 않으면 bean creation이 실패합니다. HMAC secret reference가 없거나 secret을 찾지 못해도 startup failure입니다.

Rate limit

ca-skeleton.capabilities.rate-limit.provider=redis이면 redisEdgeRateLimitPort()RedisEdgeRateLimitAdapter를 만듭니다.

policiesOf()는 policy가 하나도 없으면 실패하고, defaultPolicyId가 map에 없으면 실패합니다. algorithm은 fixed-window, sliding-counter, token-bucket만 받습니다. failure policy는 현재 fail-closed만 지원하며 다른 값은 policyOf()에서 거부합니다.

Lease

ca-skeleton.capabilities.lease.provider=redis이면 redisDistributedLeasePort()RedisDistributedLeaseAdapter를 반환합니다. 이 port는 efficiency용 lease이며 fencing을 제공하지 않습니다. 조립 성공을 distributed lock correctness로 확대하면 안 됩니다.

Idempotency V2

ca-skeleton.capabilities.idempotency.provider=redis이면 redisIdempotencyStore()가 owner-safe IdempotencyStorePortV2를 만듭니다. 이어 idempotencyExecutorV2()가 같은 selector 아래 provider-neutral V2 executor를 만듭니다.

Session 공백

다섯 번째 selector ca-skeleton.security.auth-mode=redis-session은 activation validator와 correctness health predicate에는 들어 있습니다. 그러나 RedisCapabilityConfig에는 session repository를 만드는 method가 없습니다. production source에는 snapshot이 없는 요청에서 인증된 Authentication 객체를 최초로 만드는 form login, HTTP Basic, custom authentication filter나 login endpoint도 확인되지 않습니다. 즉 Redis session을 선택하면 global runtime 조건과 readiness 조건에는 반영되지만 SessionRepositoryspringSessionRepositoryFilter로 이어지는 persistence 경로와 최초 인증 경로는 완성되지 않습니다. 이것이 4/5 composition입니다.

selector와 global switch의 모순 처리

RedisActivationValidator.REDIS_SELECTING_VALUES는 다음 다섯 selector를 압니다.

역할 Redis를 선택하는 값
default cache binding redis
rate limit provider redis
idempotency provider redis
lease provider redis
auth mode redis-session

global switch가 true이면 validator는 즉시 끝납니다. false이면 selector를 모두 검사해 모순을 정렬하고 하나의 requiredAdapterDisabled startup failure로 묶습니다. 첫 번째 missing bean에서 멈추는 대신 잘못된 설정을 한 번에 보여 줍니다. afterSingletonsInstantiated()가 이 동작을 구현합니다.

중요한 순서상의 특성이 있습니다. RedisCapabilityConfig 자체는 switch-off일 때 존재하지 않으므로 semantic bean을 만들지 않습니다. validator는 별도 SecretSourceConfig에서 unconditional bean으로 생성되어 모순을 설명합니다. SecretSourceConfig.redisActivationValidator()를 보면 이 연결이 보입니다.

정상·거절·timeout 분기

정상

  • switch off + Redis role 없음: Redis settings도 bean도 만들지 않고 시작합니다.
  • switch on + role 없음: validated runtime과 optional health contributor만 만듭니다.
  • switch on + 1개 이상 role: 공통 owner 위에 선택된 semantic bean만 만듭니다.
  • switch on + 4개 구현 role: cache, rate-limit, lease, idempotency V2가 동시에 한 namespace를 씁니다.

startup 거절

  • switch off + Redis role: activation validator가 설정 모순으로 거절합니다.
  • switch on + invalid settings/credential/resource/topology: SDK bean dependency chain에서 거절합니다.
  • rate-limit 선택 + policy 없음/unknown default/unsupported algorithm: rate-limit bean creation에서 거절합니다.
  • cache 선택 + TTL/HMAC 설정 오류: cache bean creation에서 거절합니다.

request-time timeout과 unavailable

조립 class는 command를 전송하지 않습니다. request-time timeout, ambiguous execution, typed unavailable은 semantic adapter와 command executor의 책임입니다. 다만 connection은 lazy하므로 잘못된 endpoint나 password가 context refresh 뒤 첫 borrow/command에서 드러날 수 있습니다. production startup probe가 조립되지 않은 현재 상태에서는 이 차이가 남습니다.

테스트가 고정하는 계약

RedisSdkAutoConfigurationTest는 absent/off switch에서 settings조차 없고 malformed Redis property도 무시되는 것을 고정합니다. on 상태에서는 settings binding, credential role별 resolution, topology mode, owner lifecycle, raw/admin fail-fast를 확인합니다. theRuntimeOwnerFollowsTheContext()는 종료 뒤 owner state만 확인하므로 client의 exactly-once close를 고정하지 않습니다.

RedisCapabilityCompositionTest는 server 없이 bean graph만 검사합니다.

RedisActivationValidatorTest는 다섯 role 각각과 다중 모순 보고를 고정합니다.

real-server 행동은 LiveRedisCompositionTest에 있지만 기본 test에서 제외되는 opt-in topology lane입니다. 이번 작성에서는 실행하지 않았습니다.

현재 구현 공백과 잘못 읽기 쉬운 지점

  • app.redis.enabled=true는 semantic capability가 존재한다는 뜻이 아닙니다. selector와 bean을 따로 확인해야 합니다.
  • session selector는 validator와 readiness에는 포함되지만 session repository production bean과 인증된 Authentication 객체를 최초로 만드는 production mechanism은 없습니다. 두 공백을 모두 해결하고 end-to-end 인증·session persistence를 검증해야 합니다.
  • aggregate RedisOperationsReactiveRedisOperations facade, command guard/executor/translator의 production DI도 확인되지 않습니다. semantic adapter는 RedisRuntimeOwner와 직접 조립됩니다.
  • RedisStartupProbe/RedisCapabilityProbe는 production bean과 server-fact collector가 없습니다. context refresh 성공은 endpoint reachability나 server capability 확인이 아닙니다.
  • owner destroy가 runtime client를 닫은 뒤 client bean의 inferred destroy가 같은 close()를 다시 부를 수 있습니다. lifecycle authority와 exactly-once 보장이 production bean graph와 context test에서 명확하지 않습니다.
  • capability settings의 validation은 한 곳에서 일괄 실행되지 않습니다. 예를 들어 cache validation은 cache bean factory가 호출될 때 실행되고, rate-limit은 policiesOf()에서 검증됩니다.
  • idempotency는 V2 store와 executor가 조립되지만 기존 V1 inbound bridge가 자동으로 V2를 쓰는지는 별도 문제입니다.

다음에 열어볼 source와 관련 글

  1. RedisSdkAutoConfiguration
  2. RedisCapabilityConfig
  3. RedisActivationValidator
  4. RedisCapabilityCompositionTest

이어지는 시리즈 주제는 설정·Secret·Credential, topology factory, connection lane lifecycle, health/readiness, semantic capability별 request 흐름입니다.

시리즈에서 이어 읽기