feat: redis, fileserver, httpclient 런타임 시점 구현 추가
This commit is contained in:
@@ -4,9 +4,9 @@
|
||||
|
||||
- Module ID: `app-bootstrap`
|
||||
- Gradle path: `:app-bootstrap`
|
||||
- Focused test: `./gradlew :app-bootstrap:test --console=plain`
|
||||
- Focused test (derived from Gradle path): `./gradlew :app-bootstrap:test --console=plain`
|
||||
- Runtime baseline: Java 21; repository framework baseline: Spring Boot 4.0.0.
|
||||
- Registry SSOT: `.harness/project/modules.yaml`.
|
||||
- Registry SSOT: `src/config/architecture/modules.json`.
|
||||
|
||||
Package root: `dev.caskeleton.bootstrap`.
|
||||
|
||||
@@ -19,8 +19,8 @@ Package root: `dev.caskeleton.bootstrap`.
|
||||
|
||||
## Allowed
|
||||
|
||||
- Runtime leaves explicitly allowed by `.harness/project/modules.yaml`; do not duplicate the
|
||||
19-leaf dependency list here.
|
||||
- Runtime leaves explicitly allowed by the `app-bootstrap` entry in
|
||||
`src/config/architecture/modules.json`; do not duplicate the 19-leaf dependency list here.
|
||||
- Spring Boot startup/runtime dependencies.
|
||||
- ArchUnit in tests.
|
||||
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
|
||||
애플리케이션 진입점이자 합성 루트(composition root) 모듈. 패키지 루트: `dev.caskeleton.bootstrap`.
|
||||
|
||||
이 모듈은 비즈니스 로직을 담지 않는다. Spring Boot 기동, 런타임 설정 바인딩, 모듈 간 최종
|
||||
와이어링, 그리고 모든 모듈을 검사하는 아키텍처 테스트만 둔다. 허용/금지 의존, 책임 범위, 테스트
|
||||
명령 같은 **모듈 규칙**의 SSOT 는 [CLAUDE.md](CLAUDE.md) 다.
|
||||
이 모듈은 비즈니스 로직을 담지 않는다. Spring Boot 기동, 런타임 설정 바인딩, 기본 runtime
|
||||
모듈 간 최종 와이어링, 그리고 composition classpath를 대상으로 한 중앙 아키텍처 테스트만 둔다.
|
||||
19개 leaf 전체의 프로젝트 edge는 JSON registry를 읽는 Gradle gate가 별도로 검사한다. 허용/금지
|
||||
의존, 책임 범위, 테스트 명령 같은 **모듈 규칙**의 SSOT 는 [CLAUDE.md](CLAUDE.md) 다.
|
||||
|
||||
기본 composition은 `build.gradle`에 선언된 runtime leaf만 포함한다. GraphQL, gRPC, WebSocket,
|
||||
MongoDB, file server, object storage 같은 optional leaf는 독립적으로 빌드·테스트되지만 자동으로
|
||||
기본 애플리케이션에 합성되지 않는다. optional leaf를 활성화하려면
|
||||
`src/config/architecture/modules.json`의 `app-bootstrap.allowed_dependencies`에 허용 edge를
|
||||
명시하고, 같은 변경에서 `app-bootstrap/build.gradle` 의존성과 필요한 typed settings/검증을
|
||||
추가해야 한다.
|
||||
|
||||
이 문서는 코드 주석에서 덜어낸 **설계 결정의 근거**를 모아둔 참조용 기록이다 — 코드를 읽다
|
||||
"왜 이렇게 했나"가 궁금할 때 본다. 본문은 한국어로 쓰고, 클래스·Spring API·메트릭 이름처럼
|
||||
@@ -426,6 +434,9 @@
|
||||
하는데, `application-core`는 설정(`OutboxSettings`)을 직접 읽으면 안 된다. 그래서 설정을 볼 수 있는
|
||||
합성 루트(`OutboxConfig`)가 값을 꺼내 use case 를 손으로 만들어 넘긴다. use case 클래스의
|
||||
`@UseCaseCapability` 애너테이션은 와이어링 방식과 무관하게 유지된다(ArchUnit 이 강제).
|
||||
- **bootstrap은 failure reporter를 구현하지 않고 주입만 한다.** `MessagingConfig`가 broker 설정에서
|
||||
정확히 하나의 `OutboxRelayFailureReportPort` 구현을 만들고, `OutboxConfig`는 이를 relay 생성자에
|
||||
전달한다. broker가 비활성이어도 reporter bean은 존재한다.
|
||||
- **릴레이 use case 를 독립 컨텍스트 빈으로 등록하지 않는다.** 만약 빈으로 올리면 `adapter-web`의
|
||||
`MethodSecurityConfig` 메서드 보안 pointcut(`@RequiresPermission`)이 이 타입을 CGLIB 프록시로
|
||||
감싼다. 그런데 use case 가 `final` 클래스라 프록시 생성 자체가 실패하고, 설령 된다 해도 스케줄러
|
||||
@@ -466,8 +477,9 @@
|
||||
스케줄러 등록까지 같이 소유한다.
|
||||
- **릴레이 사이클에서 발생하는 예상치 못한 예외를 잡아 ERROR 로 로깅만 하고 삼킨다.** 스케줄러
|
||||
스레드가 죽으면 릴레이가 조용히 멈추므로, 다음 틱을 위해 스레드를 살려둔다. 단, 개별 발행 실패
|
||||
(FAILED/DEAD 전이)는 릴레이 use case 내부에서 이미 상태 전이와 ERROR 로그로 처리되어 결과에
|
||||
반영되므로 이 catch 블록까지 오지 않는다 — 여기서 삼키는 것은 어디까지나 "예상치 못한" 예외다.
|
||||
(FAILED/DEAD 전이)는 relay가 persisted transition 성공 뒤 typed reporter로 canonical ERROR를
|
||||
요청하고 결과에 반영하므로 이 catch 블록까지 오지 않는다 — 여기서 삼키는 것은 어디까지나
|
||||
"예상치 못한" 예외다.
|
||||
- **`@EnableScheduling`을 직접 켜지 않고 fixed-delay 를 쓴다.** 스케줄링은 이미 `IdempotencyConfig`를
|
||||
통해 활성화돼 있어 중복으로 켤 필요가 없고, fixed-delay 는 릴레이 실행 시간과 무관하게 사이클이
|
||||
겹치지 않도록(non-overlapping) 보장한다.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Application entry point. Wires every module together and runs Spring Boot.
|
||||
// Application entry point. Wires the default runtime module set and runs Spring Boot.
|
||||
// Optional leaves require an explicit registry allowance plus a composition-root dependency.
|
||||
apply plugin: 'org.springframework.boot'
|
||||
|
||||
// The sample fixture (sampleFixture -> sample-portfolio -> objectstorage) pulls software.amazon.awssdk:s3
|
||||
@@ -52,6 +53,7 @@ dependencies {
|
||||
implementation project(':shared-contract')
|
||||
implementation 'org.springframework.boot:spring-boot-starter'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
implementation 'me.paulschwarz:spring-dotenv:4.0.0'
|
||||
// Boot 4 Flyway API/autoconfiguration: the composition root drives startup migration
|
||||
// (MigrationStartupConfig). See README.
|
||||
|
||||
@@ -30,7 +30,7 @@ com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,sampleOffTestAn
|
||||
com.github.docker-java:docker-java-api:3.7.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.github.docker-java:docker-java-transport-zerodep:3.7.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.github.docker-java:docker-java-transport:3.7.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.github.f4b6a3:uuid-creator:6.1.1=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
com.github.f4b6a3:uuid-creator:6.1.1=sampleFixture,testRuntimeClasspath
|
||||
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
|
||||
com.github.spotbugs:spotbugs-annotations:4.10.2=spotbugs
|
||||
com.github.spotbugs:spotbugs-annotations:4.8.6=compileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
|
||||
@@ -93,6 +93,7 @@ io.github.resilience4j:resilience4j-micrometer:2.2.0=productionRuntimeClasspath,
|
||||
io.github.resilience4j:resilience4j-ratelimiter:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-retry:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-timelimiter:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.lettuce:lettuce-core:6.8.1.RELEASE=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.micrometer:context-propagation:1.1.4=sampleFixture
|
||||
io.micrometer:context-propagation:1.2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-commons:1.15.12=sampleFixture
|
||||
@@ -109,20 +110,22 @@ io.micrometer:micrometer-tracing-bridge-otel:1.5.12=sampleFixture
|
||||
io.micrometer:micrometer-tracing-bridge-otel:1.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-tracing:1.5.12=sampleFixture
|
||||
io.micrometer:micrometer-tracing:1.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.netty:netty-buffer:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-codec-base:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-buffer:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.netty:netty-codec-base:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.netty:netty-codec-compression:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-codec-dns:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.netty:netty-codec-http2:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-codec-http:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-codec-marshalling:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-codec-protobuf:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-codec:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-common:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-handler:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-resolver:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-common:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.netty:netty-handler:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.netty:netty-resolver-dns:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.netty:netty-resolver:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.netty:netty-transport-classes-epoll:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-transport-native-unix-common:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-transport:4.2.7.Final=testRuntimeClasspath
|
||||
io.netty:netty-transport-native-unix-common:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.netty:netty-transport:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.opentelemetry.semconv:opentelemetry-semconv:1.32.0=sampleFixture
|
||||
io.opentelemetry.semconv:opentelemetry-semconv:1.37.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.opentelemetry:opentelemetry-api:1.49.0=sampleFixture
|
||||
@@ -167,9 +170,12 @@ io.prometheus:prometheus-metrics-model:1.4.3=compileClasspath,productionRuntimeC
|
||||
io.prometheus:prometheus-metrics-tracer-common:1.3.10=sampleFixture
|
||||
io.prometheus:prometheus-metrics-tracer-common:1.4.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.smallrye:jandex:3.2.0=sampleFixture
|
||||
io.swagger.core.v3:swagger-annotations-jakarta:2.2.29=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-core-jakarta:2.2.29=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-models-jakarta:2.2.29=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-annotations-jakarta:2.2.29=sampleFixture
|
||||
io.swagger.core.v3:swagger-annotations-jakarta:2.2.38=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-core-jakarta:2.2.29=sampleFixture
|
||||
io.swagger.core.v3:swagger-core-jakarta:2.2.38=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-models-jakarta:2.2.29=sampleFixture
|
||||
io.swagger.core.v3:swagger-models-jakarta:2.2.38=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
jakarta.activation:jakarta.activation-api:2.1.4=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.annotation:jakarta.annotation-api:2.1.1=sampleFixture
|
||||
jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
@@ -297,14 +303,17 @@ org.slf4j:jul-to-slf4j:2.0.18=sampleFixture
|
||||
org.slf4j:slf4j-api:2.0.17=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:slf4j-api:2.0.18=sampleFixture
|
||||
org.slf4j:slf4j-simple:2.0.17=checkstyle,spotbugsSlf4j
|
||||
org.springdoc:springdoc-openapi-starter-common:2.8.6=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.6=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springdoc:springdoc-openapi-starter-common:2.8.6=sampleFixture
|
||||
org.springdoc:springdoc-openapi-starter-common:3.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.6=sampleFixture
|
||||
org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-actuator-autoconfigure:3.5.16=sampleFixture
|
||||
org.springframework.boot:spring-boot-actuator-autoconfigure:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-actuator:3.5.16=sampleFixture
|
||||
org.springframework.boot:spring-boot-actuator:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-autoconfigure:3.5.16=sampleFixture
|
||||
org.springframework.boot:spring-boot-autoconfigure:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-configuration-processor:4.0.0=annotationProcessor
|
||||
org.springframework.boot:spring-boot-data-commons:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-data-jpa-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-data-jpa:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
@@ -428,6 +437,7 @@ org.xmlresolver:xmlresolver:5.3.3=checkstyle,spotbugs
|
||||
org.xmlunit:xmlunit-core:2.10.4=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.yaml:snakeyaml:2.4=sampleFixture
|
||||
org.yaml:snakeyaml:2.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
redis.clients.authentication:redis-authx-core:0.1.1-beta2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
software.amazon.awssdk:annotations:2.30.0=testRuntimeClasspath
|
||||
software.amazon.awssdk:apache-client:2.30.0=testRuntimeClasspath
|
||||
software.amazon.awssdk:arns:2.30.0=testRuntimeClasspath
|
||||
|
||||
@@ -2,6 +2,7 @@ package dev.caskeleton.bootstrap.outbox;
|
||||
|
||||
import dev.caskeleton.application.outbox.OutboxBackoffPolicy;
|
||||
import dev.caskeleton.application.outbox.OutboxMessagePublishPort;
|
||||
import dev.caskeleton.application.outbox.OutboxRelayFailureReportPort;
|
||||
import dev.caskeleton.application.outbox.OutboxStorePort;
|
||||
import dev.caskeleton.application.outbox.PublishPendingOutboxEventsUseCase;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
@@ -36,6 +37,7 @@ public class OutboxConfig {
|
||||
public OutboxRelayScheduler outboxRelayScheduler(
|
||||
OutboxStorePort store,
|
||||
OutboxMessagePublishPort publishPort,
|
||||
OutboxRelayFailureReportPort failureReporter,
|
||||
TransactionPort tx,
|
||||
Clock clock,
|
||||
RandomGenerator outboxRandomGenerator,
|
||||
@@ -45,6 +47,7 @@ public class OutboxConfig {
|
||||
new PublishPendingOutboxEventsUseCase(
|
||||
store,
|
||||
publishPort,
|
||||
failureReporter,
|
||||
tx,
|
||||
new OutboxBackoffPolicy(outboxRandomGenerator),
|
||||
clock,
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ public class SecretSourceValidator implements SmartInitializingSingleton {
|
||||
"APP_SECURITY_OAUTH_CLIENT_SECRET",
|
||||
"APP_EXTERNAL_API_KEY",
|
||||
"APP_CACHE_REDIS_PASSWORD",
|
||||
"APP_CACHE_REDIS_KEY_HMAC_SECRET",
|
||||
"APP_PRIVACY_PSEUDONYMIZATION_SALT");
|
||||
|
||||
private final ConfigurableEnvironment environment;
|
||||
|
||||
@@ -470,6 +470,22 @@ app:
|
||||
redis:
|
||||
# true | false (boolean_strict). Redis cache adapter on/off.
|
||||
enabled: ${APP_CACHE_REDIS_ENABLED}
|
||||
# managed = module-owned Lettuce runtime; external = project-supplied RedisClient bean.
|
||||
client-mode: ${APP_CACHE_REDIS_CLIENT_MODE:managed}
|
||||
host: ${APP_CACHE_REDIS_HOST:}
|
||||
port: ${APP_CACHE_REDIS_PORT:6379}
|
||||
password: ${APP_CACHE_REDIS_PASSWORD:}
|
||||
# Base64-encoded, at least 32 decoded bytes. Required when the managed runtime is enabled.
|
||||
key-hmac-secret: ${APP_CACHE_REDIS_KEY_HMAC_SECRET:}
|
||||
command-timeout: ${APP_CACHE_REDIS_COMMAND_TIMEOUT:2s}
|
||||
maximum-queued-commands: ${APP_CACHE_REDIS_MAXIMUM_QUEUED_COMMANDS:8}
|
||||
maximum-in-flight-bytes: ${APP_CACHE_REDIS_MAXIMUM_IN_FLIGHT_BYTES:16777216}
|
||||
positive-ttl: ${APP_CACHE_DEFAULT_TTL:300s}
|
||||
negative-ttl: ${APP_CACHE_NEGATIVE_TTL:60s}
|
||||
namespace-application: ${APP_NAME:ca-skeleton}
|
||||
namespace-environment: ${APP_CACHE_REDIS_NAMESPACE_ENVIRONMENT:local}
|
||||
semantic-region: ${APP_CACHE_REDIS_SEMANTIC_REGION:default}
|
||||
maximum-value-bytes: ${APP_CACHE_REDIS_MAXIMUM_VALUE_BYTES:1048576}
|
||||
# Logical-cache-name → backendId routing (CacheStoreRouter). No keys by default —
|
||||
# forks add e.g. `bindings: { worklog: redis }` or env APP_CACHE_BINDINGS_WORKLOG=redis.
|
||||
# A binding to a backend that is not enabled fails startup (Layer 3 moved to router).
|
||||
@@ -501,6 +517,8 @@ app:
|
||||
read-timeout: ${APP_OUTBOUND_HTTP_READ_TIMEOUT}
|
||||
# duration (e.g. 10s). REQUIRED — non-zero (deadline budget for the whole call incl. retries).
|
||||
global-call-timeout: ${APP_OUTBOUND_HTTP_GLOBAL_CALL_TIMEOUT}
|
||||
# Per-client live worker bound; timed-out non-cooperative workers retain a slot until exit.
|
||||
maximum-in-flight-calls: ${APP_OUTBOUND_HTTP_MAXIMUM_IN_FLIGHT_CALLS:128}
|
||||
# true | false (boolean_strict). Resilience4j retry — default disabled (D3).
|
||||
retry-enabled: ${APP_OUTBOUND_HTTP_RETRY_ENABLED:false}
|
||||
# retry 튜닝 (retry-enabled=true 일 때 적용). 기본값 = 기존 하드코딩 동작 보존.
|
||||
|
||||
+16
-1
@@ -18,6 +18,8 @@ import dev.caskeleton.adapter.outbound.messaging.core.OutboundMessagePublisher;
|
||||
import dev.caskeleton.adapter.outbound.messaging.kafka.KafkaAdapterConfig;
|
||||
import dev.caskeleton.adapter.outbound.messaging.kafka.KafkaSender;
|
||||
import dev.caskeleton.adapter.outbound.messaging.outbox.DisabledOutboxMessagePublisher;
|
||||
import dev.caskeleton.adapter.outbound.messaging.outbox.OutboxMessagePublishAdapter;
|
||||
import dev.caskeleton.adapter.outbound.messaging.outbox.Slf4jOutboxRelayFailureReportAdapter;
|
||||
import dev.caskeleton.adapter.outbound.notification.NotificationConfig;
|
||||
import dev.caskeleton.adapter.outbound.notification.core.NotificationProvider;
|
||||
import dev.caskeleton.adapter.outbound.notification.core.RoutingNotifier;
|
||||
@@ -30,6 +32,7 @@ import dev.caskeleton.application.notification.Channel;
|
||||
import dev.caskeleton.application.notification.Notification;
|
||||
import dev.caskeleton.application.notification.NotificationPort;
|
||||
import dev.caskeleton.application.outbox.OutboxMessagePublishPort;
|
||||
import dev.caskeleton.application.outbox.OutboxRelayFailureReportPort;
|
||||
import dev.caskeleton.shared.error.AdapterDisabledException;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -88,6 +91,9 @@ class OptionalAdapterBeanGatingTest {
|
||||
.isInstanceOf(DisabledMessagePublisher.class);
|
||||
assertThat(context.getBean(OutboxMessagePublishPort.class))
|
||||
.isInstanceOf(DisabledOutboxMessagePublisher.class);
|
||||
assertThat(context.getBeansOfType(OutboxRelayFailureReportPort.class)).hasSize(1);
|
||||
assertThat(context.getBean(OutboxRelayFailureReportPort.class))
|
||||
.isInstanceOf(Slf4jOutboxRelayFailureReportAdapter.class);
|
||||
|
||||
// cache D4: zero backends boot fine, unwired access fails fast in the router
|
||||
CacheStoreRouter cacheRouter = context.getBean(CacheStoreRouter.class);
|
||||
@@ -117,13 +123,21 @@ class OptionalAdapterBeanGatingTest {
|
||||
.isInstanceOf(OutboundMessagePublisher.class);
|
||||
assertThat(context.getBeansOfType(DisabledMessagePublisher.class)).isEmpty();
|
||||
assertThat(context.getBeansOfType(DisabledOutboxMessagePublisher.class)).isEmpty();
|
||||
assertThat(context.getBean(OutboxMessagePublishPort.class))
|
||||
.isInstanceOf(OutboxMessagePublishAdapter.class);
|
||||
assertThat(context.getBeansOfType(OutboxRelayFailureReportPort.class)).hasSize(1);
|
||||
assertThat(context.getBean(OutboxRelayFailureReportPort.class))
|
||||
.isInstanceOf(Slf4jOutboxRelayFailureReportAdapter.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void redisEnabledContributesTheBackendAndRoutesBoundLogicalCaches() {
|
||||
runner
|
||||
.withPropertyValues("app.cache.redis.enabled=true", "app.cache.bindings.worklog=redis")
|
||||
.withPropertyValues(
|
||||
"app.cache.redis.enabled=true",
|
||||
"app.cache.redis.client-mode=external",
|
||||
"app.cache.bindings.worklog=redis")
|
||||
.run(
|
||||
context -> {
|
||||
assertThat(context).hasNotFailed();
|
||||
@@ -176,6 +190,7 @@ class OptionalAdapterBeanGatingTest {
|
||||
.withUserConfiguration(SecondBackendConfig.class)
|
||||
.withPropertyValues(
|
||||
"app.cache.redis.enabled=true",
|
||||
"app.cache.redis.client-mode=external",
|
||||
"app.cache.test-second.enabled=true",
|
||||
"app.cache.bindings.worklog=redis",
|
||||
"app.cache.bindings.session=test-second")
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package dev.caskeleton.application.architecture.violations;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** Intentional SLF4J dependency used to prove the application diagnostic-framework ban fires. */
|
||||
public final class ApplicationDiagnosticFrameworkViolation {
|
||||
|
||||
private static final Logger LOG =
|
||||
LoggerFactory.getLogger(ApplicationDiagnosticFrameworkViolation.class);
|
||||
|
||||
private ApplicationDiagnosticFrameworkViolation() {}
|
||||
|
||||
public static void emit() {
|
||||
LOG.info("intentional architecture violation fixture");
|
||||
}
|
||||
}
|
||||
+16
@@ -5,6 +5,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import com.tngtech.archunit.lang.EvaluationResult;
|
||||
import dev.caskeleton.application.architecture.violations.ApplicationDiagnosticFrameworkViolation;
|
||||
import dev.caskeleton.bootstrap.architecture.allowed.application.CleanProjectionQueryPort;
|
||||
import dev.caskeleton.bootstrap.architecture.violations.application.BulkWriteWithoutWriteAccessUseCase;
|
||||
import dev.caskeleton.bootstrap.architecture.violations.application.FixtureRepository;
|
||||
@@ -56,6 +57,8 @@ class ArchitectureViolationFixtureTest {
|
||||
new ClassFileImporter().importClasses(JakartaValidationDomainFixture.class);
|
||||
private static final JavaClasses VALIDATION_IN_APPLICATION_FIXTURE_ONLY =
|
||||
new ClassFileImporter().importClasses(JakartaValidationApplicationFixture.class);
|
||||
private static final JavaClasses APPLICATION_DIAGNOSTIC_FRAMEWORK_FIXTURE_ONLY =
|
||||
new ClassFileImporter().importClasses(ApplicationDiagnosticFrameworkViolation.class);
|
||||
|
||||
// Each WebSocket fixture is imported in ISOLATION so the two package globs in
|
||||
// NO_WEBSOCKET_HANDLER ("org.springframework.web.socket.." vs "jakarta.websocket..")
|
||||
@@ -167,6 +170,19 @@ class ArchitectureViolationFixtureTest {
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void applicationHasNoDiagnosticFrameworkCatchesSlf4jDependency() {
|
||||
EvaluationResult result =
|
||||
CleanArchitectureTest.APPLICATION_HAS_NO_DIAGNOSTIC_FRAMEWORK.evaluate(
|
||||
APPLICATION_DIAGNOSTIC_FRAMEWORK_FIXTURE_ONLY);
|
||||
|
||||
assertThat(result.hasViolation())
|
||||
.as(
|
||||
"APPLICATION_HAS_NO_DIAGNOSTIC_FRAMEWORK must catch "
|
||||
+ "ApplicationDiagnosticFrameworkViolation")
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void applicationDoesNotDependOnApplicationContextCatchesViolation() {
|
||||
EvaluationResult result =
|
||||
|
||||
+33
@@ -214,6 +214,39 @@ class CleanArchitectureTest {
|
||||
"org.hibernate..")
|
||||
.allowEmptyShould(true);
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule APPLICATION_HAS_NO_DIAGNOSTIC_FRAMEWORK =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAPackage("dev.caskeleton.application..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAnyPackage(
|
||||
"org.slf4j..",
|
||||
"java.util.logging..",
|
||||
"ch.qos.logback..",
|
||||
"org.apache.logging.log4j..",
|
||||
"io.micrometer..")
|
||||
.as(
|
||||
"APPLICATION_HAS_NO_DIAGNOSTIC_FRAMEWORK: application-core owns typed outbound "
|
||||
+ "diagnostic ports, while logging and metrics implementations belong to "
|
||||
+ "outbound adapters")
|
||||
.allowEmptyShould(true);
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule SAMPLE_APPLICATION_HAS_NO_SLF4J =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAPackage("dev.caskeleton.sample.portfolio.application..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAnyPackage("org.slf4j..")
|
||||
.as(
|
||||
"SAMPLE_APPLICATION_HAS_NO_SLF4J: sample application collaborators read "
|
||||
+ "correlation context through application-core CorrelationIdPort; inbound "
|
||||
+ "adapters own MDC")
|
||||
.allowEmptyShould(true);
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule APPLICATION_DOES_NOT_USE_SPRING_TRANSACTIONAL_ANNOTATION =
|
||||
noClasses()
|
||||
|
||||
+2
-1
@@ -60,7 +60,8 @@ class DeveloperExperienceContractTest {
|
||||
assertThat(readme)
|
||||
.contains("./gradlew bootstrap")
|
||||
.contains("GET /api/healthcheck")
|
||||
.contains("feature-developer-experience-contract");
|
||||
.contains("## 퀵스타트")
|
||||
.contains("첫 실행 진입점은 하나입니다.");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
@@ -188,6 +188,7 @@ final class OutboxContainerTestSupport {
|
||||
return new PublishPendingOutboxEventsUseCase(
|
||||
store,
|
||||
publisher,
|
||||
ignored -> {},
|
||||
tx,
|
||||
new OutboxBackoffPolicy(RandomGenerator.getDefault()),
|
||||
clock,
|
||||
|
||||
+2
@@ -579,6 +579,7 @@ class OutboxRowLifecycleContractTest {
|
||||
new PublishPendingOutboxEventsUseCase(
|
||||
store,
|
||||
event -> {},
|
||||
ignored -> {},
|
||||
ctx.getBean(TransactionPort.class),
|
||||
new OutboxBackoffPolicy(RandomGenerator.getDefault()),
|
||||
reclaimClock,
|
||||
@@ -668,6 +669,7 @@ class OutboxRowLifecycleContractTest {
|
||||
return new PublishPendingOutboxEventsUseCase(
|
||||
ctx.getBean(OutboxStoreAdapter.class),
|
||||
ctx.getBean(OutboxMessagePublishPort.class),
|
||||
ignored -> {},
|
||||
ctx.getBean(TransactionPort.class),
|
||||
new OutboxBackoffPolicy(RandomGenerator.getDefault()),
|
||||
Clock.fixed(clockInstant, ZoneOffset.UTC),
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ class SecretSourceValidatorTest {
|
||||
"APP_SECURITY_OAUTH_CLIENT_SECRET=real-oauth-secret",
|
||||
"APP_EXTERNAL_API_KEY=real-api-key",
|
||||
"APP_CACHE_REDIS_PASSWORD=real-redis-password",
|
||||
"APP_CACHE_REDIS_KEY_HMAC_SECRET=real-redis-key-hmac-secret",
|
||||
"APP_PRIVACY_PSEUDONYMIZATION_SALT=real-salt"
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user