177 lines
19 KiB
Markdown
177 lines
19 KiB
Markdown
# Redis 모듈 해부: Gradle leaf에서 app-bootstrap까지
|
|
|
|
> **Redis 코드 상세 시리즈 02/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-backend-policy-boundary.md) · 다음: [app.redis.enabled에서 capability bean까지: Spring 조립 코드 읽기](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-spring-composition.md)
|
|
|
|
## 이 글이 답하는 코드 질문
|
|
|
|
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`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/config/architecture/modules.json:1) | module id, Gradle path, 허용 의존, runtime membership | 19개 leaf의 선언 | `settings.gradle` |
|
|
| [`settings.gradle`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/settings.gradle:9) | `modules.json` | 레지스트리 검증 후 `include`된 Gradle project | 각 leaf의 `build.gradle` |
|
|
| [`cache-redis/build.gradle`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/build.gradle:1) | 허용된 project edge와 외부 라이브러리 | Redis leaf compile/runtime classpath | `sdk` package와 topology test task |
|
|
| [`app-bootstrap/build.gradle`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/build.gradle:55) | runtime composition membership | 실제 애플리케이션에 Redis leaf 포함 | Spring component scan과 auto-configuration |
|
|
| [`AutoConfiguration.imports`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:1) | auto-configuration class 이름 | `RedisSdkAutoConfiguration` 발견 | `app.redis.enabled` 조건 |
|
|
| [`RedisSdkModuleBoundaryTest`](/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/RedisSdkModuleBoundaryTest.java:22) | `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`의 초기화 코드](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/settings.gradle:15)에 있습니다.
|
|
|
|
각 module entry도 `id`, `gradle_path`, `source_path`, `allowed_dependencies`, `runtime_memberships` 다섯 field만 허용합니다. 중복 id, 중복 Gradle path, 저장소 밖으로 빠져나가는 source path, 존재하지 않는 directory, 알 수 없는 runtime membership은 설정 단계에서 실패합니다. 검증을 통과한 항목만 [`include`와 `projectDir` 지정](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/settings.gradle:180)으로 Gradle project가 됩니다.
|
|
|
|
```mermaid
|
|
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행](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/config/architecture/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` 의존 선언](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/build.gradle:55)은 `implementation project(':adapter:outbound:cache-redis')`를 포함합니다. 레지스트리 membership과 build dependency가 같은 방향을 가리키는 구조입니다.
|
|
|
|
## leaf의 허용 의존과 실제 의존
|
|
|
|
Redis leaf의 project dependency는 [`cache-redis/build.gradle` 13행](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/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행](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/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` 머리말](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/build.gradle:1)과 [`RedisSdkModuleBoundaryTest` 설명](/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/RedisSdkModuleBoundaryTest.java:22)이 함께 고정합니다.
|
|
|
|
테스트의 `DESIGNED_MODULES`는 [`70행](/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/RedisSdkModuleBoundaryTest.java:69)부터 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-bootstrap`의 `RedisCapabilityConfig`가 선택적으로 bean을 만듭니다.
|
|
|
|
## import 방향을 강제하는 규칙
|
|
|
|
가장 엄격한 경계는 `sdk.api`입니다. [`FORBIDDEN_IMPORTS`](/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/RedisSdkModuleBoundaryTest.java:44)는 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()`](/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/RedisSdkModuleBoundaryTest.java:121)는 source의 import 문을 읽어 위반을 모읍니다.
|
|
|
|
Reactive type도 `api/reactive`와 구현에만 머물러야 합니다. [`reactorIsConfinedToReactivePackages()`](/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/RedisSdkModuleBoundaryTest.java:145)는 다른 공개 API에 Reactor import가 들어오면 실패합니다.
|
|
|
|
두 개의 source scan은 API 모양 자체를 제한합니다.
|
|
|
|
- [`noArbitraryStringCommandApi()`](/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/RedisSdkModuleBoundaryTest.java:161)는 `execute(String ...)`, `call(String ...)` 같은 임의 명령 표면을 거부합니다.
|
|
- [`noJavaNativeSerialization()`](/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/RedisSdkModuleBoundaryTest.java:177)는 `ObjectOutputStream`, `ObjectInputStream`, `java.io.Serializable` 사용을 거부합니다.
|
|
|
|
이 테스트들은 Java compiler나 ArchUnit의 complete type graph가 아니라 정규식 기반 source scan입니다. fully qualified type 사용이나 새로운 문법 형태가 규칙 의도를 우회하지 않는지 review가 여전히 필요합니다.
|
|
|
|
## Spring runtime 진입점
|
|
|
|
Redis leaf가 `app-bootstrap` classpath에 들어온 뒤에는 두 경로가 작동합니다.
|
|
|
|
첫째, SDK 기반 bean은 [`AutoConfiguration.imports`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:1)가 `RedisSdkAutoConfiguration`을 Spring Boot에 등록합니다. 이 class는 `app.redis.enabled=true`일 때만 설정 binding, credential resolution, client, runtime owner, health contributor를 만듭니다.
|
|
|
|
둘째, semantic capability는 `app-bootstrap` package의 [`RedisCapabilityConfig`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/redis/RedisCapabilityConfig.java:54)가 맡습니다. 이 configuration도 global switch를 요구하고, cache/rate-limit/lease/idempotency selector마다 port bean을 따로 만듭니다.
|
|
|
|
따라서 호출 순서는 다음과 같습니다.
|
|
|
|
```mermaid
|
|
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()`](/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/RedisSdkModuleBoundaryTest.java:193)에서 잡습니다.
|
|
|
|
[`RedisCapabilityCompositionTest`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/redis/RedisCapabilityCompositionTest.java:53)는 runtime owner만 있는 경우와 selector별 port가 있는 경우를 구분합니다. 이 테스트는 연결을 열지 않으므로 bean graph 계약입니다.
|
|
|
|
실제 topology 연결은 `redisTopologyTest`라는 별도 opt-in task입니다. [`cache-redis/build.gradle` 68행](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/build.gradle:68)은 standalone, Sentinel, Cluster, TLS lane을 구분하고, 기본 `test`는 `redis-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`가 담당합니다.
|
|
- `RedisStartupProbe`와 `RedisCapabilityProbe`도 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](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/config/architecture/modules.json:115)
|
|
2. [`cache-redis/build.gradle`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/build.gradle:1)
|
|
3. [`RedisSdkModuleBoundaryTest`](/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/RedisSdkModuleBoundaryTest.java:32)
|
|
4. [`AutoConfiguration.imports`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:1)
|
|
5. [`RedisCapabilityConfig`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/redis/RedisCapabilityConfig.java:35)
|
|
|
|
시리즈에서 이어지는 주제는 Spring 조립, 설정·credential, topology factory, connection lifecycle, health·observability입니다.
|
|
|
|
## 시리즈에서 이어 읽기
|
|
|
|
- 이전 글: [Redis를 범용 클라이언트가 아니라 정책 경계로 다루기](/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)
|
|
- 전체 흐름: [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)
|
|
|