11 KiB
title, source_type, status, confidence, tags, related_projects, last_reviewed, canonical_sources, audience, target_publish, status_label
| title | source_type | status | confidence | tags | related_projects | last_reviewed | canonical_sources | audience | target_publish | status_label | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Optional Adapter를 설정 계약으로 다루기 | blog | verified | high |
|
|
2026-07-03 |
|
backend-engineer | ready |
Optional Adapter를 설정 계약으로 다루기
Parent / 부모 (필수)
- 핵심 canonical: wiki/projects/ca-tmpl/config-and-adapter-templates
- 관련 개념 문서: wiki/concepts/config-and-adapter-templates - 일반 config/adapter template 비교. 현재 이 글의 구현 사실 근거는 verified project canonical에 둔다.
타깃 독자 / Target reader
- 독자 profile: Spring Boot optional adapter와 env-driven 설정을 skeleton에 넣으려는 백엔드 엔지니어.
- 이미 안다고 가정하는 것:
@ConfigurationProperties,@ConditionalOnProperty, env var. - 처음 듣는다고 가정하는 것: adapter 추가를 설정값 하나가 아니라 registry, bean gating, static rule, startup fail-fast가 맞물린 계약으로 보는 방식.
도입 / Hook
Optional adapter는 처음에는 편해 보입니다. Redis가 있으면 cache adapter를 켜고, Kafka가 있으면 message broker adapter를 켜고, Slack이나 email provider는 필요할 때만 붙이면 됩니다. 문제는 “꺼져 있어도 안전한가”입니다. env key가 .env에는 있는데 application.yml에서 안 쓰이거나, optional adapter bean이 조건 없이 등록되거나, disabled 상태인데 application layer가 adapter package를 직접 import하면 설정은 계약이 아니라 분위기가 됩니다.
ca-tmpl은 이 문제를 @ConditionalOnProperty 하나로 끝내지 않았습니다. APP_ env registry와 .env drift gate, @ConfigurationProperties settings, optional adapter package isolation, @ConditionalOnProperty annotation rule, startup failure exception을 나눠 두었습니다. 이 글은 optional adapter를 “있으면 쓰고 없으면 말고”가 아니라 “켜지는 조건과 실패 방식이 검증되는 계약”으로 만든 이유를 정리합니다.
본문 outline / Body outline
- optional adapter의 흔한 실패 - env drift, ungated bean, hidden direct import.
- 설정은 runtime contract다 -
.env,application.yml, env registry를 같이 검증한다. @ConditionalOnProperty의 역할과 한계 - bean 등록 조건은 보지만 runtime activation 전체를 증명하지는 않는다.- static isolation과 startup fail-fast - disabled adapter가 조용히 섞이지 않게 한다.
- 구현된 것과 provider-specific template의 남은 범위를 분리한다.
본문 / Body
설정값은 코드 밖에 있지만, 실제로는 코드의 실행 경로를 바꿉니다. APP_CACHE_REDIS_ENABLED=true가 들어오면 Redis cache backend가 생기고, app.messaging.broker=kafka가 들어오면 Kafka broker bean이 등록됩니다. 이런 설정을 문서로만 관리하면 drift가 생깁니다. .env에만 남은 key, application.yml에만 있는 placeholder, registry에 등록되지 않은 APP_ key가 조금씩 쌓입니다.
ca-tmpl은 이 drift를 Gradle task로 막습니다. verifyEnvKeys는 src/.env, application.yml, docs/registries/env-keys.yaml을 함께 읽습니다. application.yml의 required placeholder가 .env에 없으면 실패하고, .env key가 어떤 placeholder에도 쓰이지 않으면 실패합니다. 또 APP_ key는 env registry에 등록되어 있어야 합니다. 즉 설정 문서와 실제 boot 설정이 따로 움직이지 않게 빌드 단계에서 묶습니다.
adapter activation은 Layer 1에서 Spring bean 조건으로 표현합니다. Redis cache backend는 app.cache.redis.enabled=true일 때만 CacheBackend bean을 제공합니다. Kafka broker도 app.messaging.broker=kafka일 때만 MessageBroker bean을 등록합니다. 이 방식의 장점은 adapter 구현이 중앙 router나 use case를 직접 수정하지 않아도 “내가 활성화되는 조건”을 자기 config에 선언할 수 있다는 점입니다.
하지만 @ConditionalOnProperty만으로는 충분하지 않습니다. ArchUnit은 런타임 property evaluation을 실행하지 않습니다. 대신 ca-tmpl은 정적 분석으로 두 가지를 봅니다. application layer가 optional adapter package를 import하지 않는지, optional adapter package 안의 @Bean method가 @ConditionalOnProperty를 갖고 있는지입니다. 이건 “현재 어떤 profile에서 bean이 켜졌는가”를 증명하는 것이 아니라, disabled-default를 우회할 수 있는 코드 구조를 막는 쪽입니다.
Layer 3는 startup fail-fast입니다. required capability adapter가 꺼져 있거나 coordination bean이 없으면 RequiredAdapterDisabledException 계열 startup failure로 드러납니다. cache router나 messaging config처럼 중앙 binding 지점에서도 disabled backend binding이 조용한 no-op으로 흘러가지 않도록 설계합니다. skeleton에서 중요한 것은 “꺼져 있으면 아무 일도 하지 않는다”가 아니라, “꺼져 있는데 필요한 경로라면 빨리 실패한다”입니다.
이 결정을 그림으로 보면 세 층입니다.
| 층 | 잡는 문제 | ca-tmpl 구현 범위 |
|---|---|---|
| Env registry gate | .env / application.yml / registry drift |
verifyEnvKeys |
| Bean gating | optional adapter bean이 조건 없이 등록되는 문제 | @ConditionalOnProperty, DisabledAdapterArchitectureTest |
| Startup/runtime fail-fast | required adapter가 disabled인데 조용히 진행되는 문제 | startup failure exception, router/config guard |
이 글에서 조심해야 할 경계도 있습니다. ca-tmpl에는 env registry/gate와 optional adapter guard가 구현되어 있고 ./gradlew check로 로컬 검증됐습니다. 반면 모든 provider-specific adapter template가 완성됐다고 말하면 안 됩니다. Kafka, Redis, Slack, Google Email 같은 표면이 일부 존재하더라도, “모든 외부 provider 전환을 검증했다”는 주장은 project canonical 범위를 넘습니다. 이 글의 결론은 “optional adapter를 완성했다”가 아니라 “optional adapter가 켜지고 꺼지는 실패 모드를 설정 계약으로 드러내기 시작했다”입니다.
코드 예제 / Code samples (있다면)
// 출처: [[wiki/projects/ca-tmpl/config-and-adapter-templates]]
// 실제 파일: src/build.gradle, ca-tmpl @f6fbd4e196b4
tasks.register('verifyEnvKeys') {
description = 'Verifies src/.env covers application.yml placeholders and every APP_ key is registered.'
File envFile = file("${rootProject.projectDir}/.env")
File appYml = file("${rootProject.projectDir}/app-bootstrap/src/main/resources/application.yml")
File registryFile = file("${rootProject.projectDir}/../docs/registries/env-keys.yaml")
}
// 출처: [[wiki/projects/ca-tmpl/config-and-adapter-templates]]
// 실제 파일: adapter-outbound/.../RedisCacheAdapterConfig.java, ca-tmpl @f6fbd4e196b4
@Bean
@ConditionalOnProperty(
name = "app.cache.redis.enabled",
havingValue = "true",
matchIfMissing = false)
public CacheBackend redisCacheBackend(RedisClient redisClient) {
return new RedisCacheStore(redisClient);
}
// 출처: [[wiki/projects/ca-tmpl/config-and-adapter-templates]]
// 실제 파일: adapter-outbound/.../KafkaAdapterConfig.java, ca-tmpl @f6fbd4e196b4
@Bean
@ConditionalOnProperty(name = "app.messaging.broker", havingValue = "kafka")
public MessageBroker kafkaMessageBroker(KafkaSender sender, KafkaAdapterSettings settings) {
if (settings.brokers().isEmpty()) {
throw new IllegalStateException("app.messaging.broker=kafka requires a non-empty broker list");
}
return new KafkaMessageBroker(sender);
}
// 출처: [[wiki/projects/ca-tmpl/config-and-adapter-templates]]
// 실제 파일: app-bootstrap/.../DisabledAdapterArchitectureTest.java, ca-tmpl @f6fbd4e196b4
static final ArchRule APPLICATION_DOES_NOT_DEPEND_ON_OPTIONAL_ADAPTERS =
noClasses()
.that()
.resideInAPackage("..application..")
.should()
.dependOnClassesThat()
.resideInAnyPackage(OPTIONAL_ADAPTER_PACKAGES);
Sources / 근거 (canonical 인용 필수, derived layer 의무)
- wiki/projects/ca-tmpl/config-and-adapter-templates - 이 글의 1차 canonical. env registry/gate,
@ConfigurationProperties, optional adapter@ConditionalOnProperty, ArchUnit static guard, startup fail-fast, local verification, provider별 미완성 범위를 따른다. - wiki/concepts/config-and-adapter-templates - 관련 개념 문서. Spring Cloud Config, ConfigMap reload, Consul, Parameter Store, feature flag SaaS 같은 대안 비교 배경으로만 둔다.
사실 vs 의견 / Fact vs opinion 구분
- 사실: ca-tmpl에는
verifyEnvKeys,docs/registries/env-keys.yaml, 여러@ConfigurationPropertiessettings, optional adapter@ConditionalOnPropertyconfig,DisabledAdapterArchitectureTest, startup failure exception이 존재한다. 근거: wiki/projects/ca-tmpl/config-and-adapter-templates - 사실:
./gradlew check가 2026-07-02 기준 통과했고,verifyEnvKeys와 optional adapter 관련 검증이 local/dev 범위에 포함된다. 근거: wiki/projects/ca-tmpl/config-and-adapter-templates - 사실: 모든 provider-specific adapter template와 모든 disabled runtime path가 완성됐다고 말하지 않는다. 근거: wiki/projects/ca-tmpl/config-and-adapter-templates
- 의견: optional adapter는 silent noop보다 fail-fast 계약으로 두는 편이 skeleton 학습과 장애 분석에 더 유리하다.
- 알지 못하는 것: 실제 환경에서 adapter on/off를 전환한 운영 경험, provider SDK별 production tuning.
답할 수 있는 범위 / Answer boundary
- 자신 있게 답할 수 있는 후속 질문:
- env key drift를 왜 build gate로 잡는가?
@ConditionalOnProperty는 어떤 문제를 해결하고 어떤 문제를 해결하지 못하는가?- optional adapter static isolation과 startup fail-fast가 왜 둘 다 필요한가?
- 다음 글로 넘길 부분:
- 특정 provider SDK별 timeout/retry/auth 설정.
- runtime reload나 feature flag SaaS가 필요한 제품 단계.
- 실제 환경에서 adapter toggle을 운영한 경험.
게시 체크리스트 / Publish checklist
- 모든 사실 주장에 canonical 링크 있음
- 사실 vs 의견 분리 명시됨
- 금지 마케팅 표현 없음
- 코드 예제 출처 명시
- 타깃 독자 가정과 톤 일치
/lint통과- 게시 URL 기록 (게시 후):