Files
document-haness/.run/redis/redis-module-package-boundaries.md
T

19 KiB

Redis 모듈 해부: Gradle leaf에서 app-bootstrap까지

Redis 코드 상세 시리즈 02/20 · 전체 지도 · 이전: Redis를 범용 클라이언트가 아니라 정책 경계로 다루기 · 다음: app.redis.enabled에서 capability bean까지: Spring 조립 코드 읽기

이 글이 답하는 코드 질문

Redis 구현은 설계 문서에서 여러 SDK 모듈처럼 보이지만, 실제 Gradle 그래프에서는 :adapter:outbound:cache-redis 하나입니다. 그렇다면 API, Lettuce 구현, raw, admin, extension 사이의 경계는 어디에서 강제될까요? 이 글은 다음 질문에 답합니다.

  • Redis leaf는 19개 모듈 레지스트리에서 어떤 위치를 차지합니까?
  • leaf가 참조할 수 있는 프로젝트와 app-bootstrap이 조립하는 프로젝트는 어떻게 다릅니까?
  • 한 Gradle 프로젝트 안의 SDK 하위 모듈은 어떤 package 규칙으로 분리됩니까?
  • Spring Boot는 leaf에 있는 auto-configuration을 어떻게 찾습니까?

기준은 source HEAD 3b5aee50e33c44c02d08c94bb39ad34814482010입니다.

먼저 보는 파일 지도

파일 입력 출력·역할 다음에 볼 곳
modules.json module id, Gradle path, 허용 의존, runtime membership 19개 leaf의 선언 settings.gradle
settings.gradle modules.json 레지스트리 검증 후 include된 Gradle project 각 leaf의 build.gradle
cache-redis/build.gradle 허용된 project edge와 외부 라이브러리 Redis leaf compile/runtime classpath sdk package와 topology test task
app-bootstrap/build.gradle runtime composition membership 실제 애플리케이션에 Redis leaf 포함 Spring component scan과 auto-configuration
AutoConfiguration.imports auto-configuration class 이름 RedisSdkAutoConfiguration 발견 app.redis.enabled 조건
RedisSdkModuleBoundaryTest sdk 아래 Java source tree package 존재 여부와 import 위반 목록 package별 구현

Gradle leaf가 생기는 순서

settings.gradle은 디렉터리를 재귀 탐색해 project를 추측하지 않습니다. 먼저 modules.json을 읽고 root field가 정확히 runtime_compositions, modules인지 검사합니다. runtime composition은 app-bootstrap, sample-portfolio 두 개여야 하고 module 수는 정확히 19개여야 합니다. 이 검증은 settings.gradle의 초기화 코드에 있습니다.

각 module entry도 id, gradle_path, source_path, allowed_dependencies, runtime_memberships 다섯 field만 허용합니다. 중복 id, 중복 Gradle path, 저장소 밖으로 빠져나가는 source path, 존재하지 않는 directory, 알 수 없는 runtime membership은 설정 단계에서 실패합니다. 검증을 통과한 항목만 includeprojectDir 지정으로 Gradle project가 됩니다.

flowchart LR
    A[modules.json] --> B[settings.gradle schema 검증]
    B -->|정상| C[19개 project include]
    B -->|위반| X[Gradle 설정 실패]
    C --> D[:adapter:outbound:cache-redis]
    D --> E[:app-bootstrap runtime graph]

Redis 항목은 modules.json 115행에서 확인할 수 있습니다.

  • id는 adapter-outbound-cache-redis입니다.
  • Gradle path는 :adapter:outbound:cache-redis입니다.
  • 허용 project 의존은 domain-core, application-core, shared-contract, adapter-outbound-support입니다.
  • runtime membership은 app-bootstrap 하나입니다. sample-portfolio에는 Redis leaf가 들어가지 않습니다.

여기서 runtime_memberships는 “이 leaf를 어느 실행 조합이 포함해야 하는가”라는 architecture 선언입니다. 실제 classpath edge는 별도로 app-bootstrap/build.gradle이 만듭니다. app-bootstrap 의존 선언implementation project(':adapter:outbound:cache-redis')를 포함합니다. 레지스트리 membership과 build dependency가 같은 방향을 가리키는 구조입니다.

leaf의 허용 의존과 실제 의존

Redis leaf의 project dependency는 cache-redis/build.gradle 13행에 세 개가 선언되어 있습니다.

선언 왜 필요한가 현재 읽을 때 주의할 점
application-core cache, lease, idempotency semantic port 구현 SDK package 자체의 공개 API 의존과 semantic adapter 의존을 구분해야 합니다.
shared-contract rate-limit port와 health contract leaf 전체의 의존이며 모든 SDK package에서 허용된다는 뜻은 아닙니다.
adapter:outbound:support outbound 공통 지원 modules.json에서 허용된 edge입니다.

외부 의존은 Spring Boot auto-configuration/health, Lettuce, Reactor, SLF4J입니다. 공개 reactive API가 Reactor type을 signature에 쓰므로 reactor-core를 직접 선언합니다. 반대로 Spring Data Redis와 Micrometer는 의도적으로 없습니다. 그 이유와 zero-import 기대는 build.gradle 33행에 적혀 있습니다.

이 부재는 두 가지 경계를 만듭니다.

  1. Redis 명령은 Spring Data의 문자열 중심 표면을 통과하지 않고 자체 typed API와 command policy를 통과합니다.
  2. SDK가 MeterRegistry를 직접 알지 않습니다. 관찰값을 sink에 넘기는 지점과 실제 metric backend 조립을 분리합니다.

다만 두 번째 경계에는 현재 공백이 있습니다. RedisObservation type과 실행기 sink seam은 구현되어 있지만, app-bootstrap에서 Micrometer/OTel sink를 만드는 production bean은 확인되지 않습니다. package 경계를 “관측이 완성됐다”는 뜻으로 읽으면 안 됩니다.

한 leaf 안의 package 모듈

설계의 SDK 모듈은 별도 Gradle project가 아니라 dev.caskeleton.adapter.outbound.cache.redis.sdk 아래 package로 구현됩니다. 그 결정은 build.gradle 머리말RedisSdkModuleBoundaryTest 설명이 함께 고정합니다.

테스트의 DESIGNED_MODULES`70행부터 22개 package 경계를 열거합니다.

  • 공개 표면: api, api/key, api/codec, api/command, api/error, api/operations, api/reactive
  • Lettuce 구현: lettuce, lettuce/codec, lettuce/command, lettuce/connection, lettuce/observability, lettuce/operations
  • 정책·topology 지원: config, cluster
  • 격리 표면: programmability, raw, admin
  • extension: extensions/json, extensions/search, extensions/timeseries, extensions/probabilistic

NOT_YET_IMPLEMENTED_MODULES는 현재 빈 목록입니다. 따라서 테스트는 22개 package directory가 모두 존재해야 통과합니다. 이것은 directory와 경계가 있다는 계약이지, 모든 interface가 production bean으로 조립됐다는 계약은 아닙니다.

SDK 밖에는 semantic adapter package도 있습니다. cache, ratelimit, lease, idempotency, keyspace가 그 예입니다. 이들은 provider-neutral port를 Redis runtime에 연결하며 app-bootstrapRedisCapabilityConfig가 선택적으로 bean을 만듭니다.

import 방향을 강제하는 규칙

가장 엄격한 경계는 sdk.api입니다. FORBIDDEN_IMPORTS는 API package가 다음을 import하지 못하게 합니다.

  • Spring, Lettuce, Micrometer
  • sdk.lettuce, cluster, programmability, raw, admin, config, extensions

그 밖에도 Lettuce package는 raw/admin/extensions를, cluster와 programmability는 raw/admin을, raw와 admin은 서로를 import하지 못합니다. apiPackageDoesNotDependOnDrivers()는 source의 import 문을 읽어 위반을 모읍니다.

Reactive type도 api/reactive와 구현에만 머물러야 합니다. reactorIsConfinedToReactivePackages()는 다른 공개 API에 Reactor import가 들어오면 실패합니다.

두 개의 source scan은 API 모양 자체를 제한합니다.

이 테스트들은 Java compiler나 ArchUnit의 complete type graph가 아니라 정규식 기반 source scan입니다. fully qualified type 사용이나 새로운 문법 형태가 규칙 의도를 우회하지 않는지 review가 여전히 필요합니다.

Spring runtime 진입점

Redis leaf가 app-bootstrap classpath에 들어온 뒤에는 두 경로가 작동합니다.

첫째, SDK 기반 bean은 AutoConfiguration.importsRedisSdkAutoConfiguration을 Spring Boot에 등록합니다. 이 class는 app.redis.enabled=true일 때만 설정 binding, credential resolution, client, runtime owner, health contributor를 만듭니다.

둘째, semantic capability는 app-bootstrap package의 RedisCapabilityConfig가 맡습니다. 이 configuration도 global switch를 요구하고, cache/rate-limit/lease/idempotency selector마다 port bean을 따로 만듭니다.

따라서 호출 순서는 다음과 같습니다.

sequenceDiagram
    participant G as Gradle runtime graph
    participant B as Spring Boot
    participant A as RedisSdkAutoConfiguration
    participant C as RedisCapabilityConfig
    G->>B: cache-redis leaf를 classpath에 포함
    B->>A: AutoConfiguration.imports 발견
    A->>A: app.redis.enabled 조건 평가
    A-->>B: settings/client/owner/health bean
    B->>C: component scan으로 bootstrap config 발견
    C-->>B: 선택된 semantic port bean

정상 분기와 실패 분기

정상적인 Redis-off 배포에서는 leaf가 classpath에 있어도 SDK bean이 생기지 않습니다. module membership은 “코드를 사용할 수 있음”이고 app.redis.enabled는 “이번 deployment에서 runtime을 만든다”입니다.

Redis-on 배포에서는 settings가 검증된 뒤 client와 owner가 생깁니다. role selector가 Redis를 가리킬 때만 해당 semantic port가 추가됩니다.

다음은 request-time 전에 실패합니다.

  • registry schema, module 수, path, dependency id가 어긋나면 Gradle 설정이 실패합니다.
  • leaf dependency가 registry 허용 범위를 벗어나면 architecture 검증 대상이 됩니다.
  • SDK package가 금지 import를 추가하면 module boundary test가 실패합니다.
  • app.redis.enabled=true인데 settings/credential/topology 전제조건이 맞지 않으면 Spring context가 실패합니다.
  • global switch가 꺼져 있는데 role selector가 Redis를 고르면 RedisActivationValidator가 모순을 보고합니다.

테스트가 고정하는 계약

RedisSdkModuleBoundaryTest는 package inventory, import 방향, Reactor 격리, 임의 문자열 명령 금지, Java native serialization 금지를 고정합니다. 이 테스트는 실제 package source를 정렬해 읽으므로 scan 자체가 비어 있는 경우도 sourceScanIsDeterministic()에서 잡습니다.

RedisCapabilityCompositionTest는 runtime owner만 있는 경우와 selector별 port가 있는 경우를 구분합니다. 이 테스트는 연결을 열지 않으므로 bean graph 계약입니다.

실제 topology 연결은 redisTopologyTest라는 별도 opt-in task입니다. cache-redis/build.gradle 68행은 standalone, Sentinel, Cluster, TLS lane을 구분하고, 기본 testredis-topology tag를 제외합니다. 이번 문서 작업에서는 이 real-server lane을 실행하지 않았습니다.

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

  • 22개 designed package가 모두 존재하지만 이것은 production DI 완성을 뜻하지 않습니다. aggregate RedisOperations/ReactiveRedisOperations, command guard/executor/translator의 production 조립은 확인되지 않습니다.
  • RedisConnectionRegistry는 source와 단위 테스트가 있으나 production 생성 지점은 없습니다. 현행 connection pool과 shutdown은 RedisRuntimeOwner가 담당합니다.
  • RedisStartupProbeRedisCapabilityProbe도 production bean/호출자가 없습니다. 따라서 server version, command presence, write durability가 실제 startup에서 확인된다고 말할 수 없습니다.
  • auto-configuration import는 SDK 기반 bean만 찾습니다. semantic port는 app-bootstrap의 component scan에 의존합니다.
  • sample-portfolio runtime membership에는 Redis leaf가 없습니다. repository에 Redis 코드가 있다는 사실만으로 두 runtime composition 모두 Redis를 포함한다고 읽으면 안 됩니다.

다음에 열어볼 source와 관련 글

다음 순서로 읽으면 경계에서 조립으로 자연스럽게 이어집니다.

  1. modules.json Redis entry
  2. cache-redis/build.gradle
  3. RedisSdkModuleBoundaryTest
  4. AutoConfiguration.imports
  5. RedisCapabilityConfig

시리즈에서 이어지는 주제는 Spring 조립, 설정·credential, topology factory, connection lifecycle, health·observability입니다.

시리즈에서 이어 읽기