163 lines
9.0 KiB
Markdown
163 lines
9.0 KiB
Markdown
---
|
|
title: Optional Adapter를 설정 계약으로 다루기
|
|
source_type: blog
|
|
status: draft
|
|
confidence: unknown
|
|
tags: [blog, ca-tmpl, integration, spring-boot, externalized-config, component-scan]
|
|
related_projects: [ca-tmpl]
|
|
last_reviewed:
|
|
canonical_sources: []
|
|
audience: backend-engineer
|
|
target_publish:
|
|
status_label: draft
|
|
---
|
|
|
|
# Optional Adapter를 설정 계약으로 다루기
|
|
|
|
> `@ConditionalOnProperty` 하나만 붙이면 될 것 같지만, "꺼져 있어도 안전한가"까지 물으면 이야기가 달라집니다.
|
|
|
|
## TL;DR
|
|
|
|
- Optional adapter(Redis, Kafka, Slack 등)는 "있으면 쓰고 없으면 말고"로 접근하기 쉽지만, 실제로는 env key drift, 조건 없이 등록되는 bean, disabled 상태인데 조용히 흘러가는 코드 경로 같은 실패 모드를 만듭니다.
|
|
- ca-tmpl은 이걸 **세 개의 층**으로 나눠서 다룹니다: env registry gate, bean gating(`@ConditionalOnProperty` + 정적 검사), startup fail-fast.
|
|
- `@ConditionalOnProperty`는 bean 등록 조건은 보여주지만, **런타임에 실제로 어떤 property가 적용됐는지까지 증명하지는 않습니다.** 이 한계를 인정하는 게 이 글의 핵심입니다.
|
|
- 모든 provider(Kafka, Redis, Slack 등)의 완성을 주장하지 않습니다. "켜지고 꺼지는 실패 모드를 계약으로 드러내기 시작했다"까지만 말할 수 있습니다.
|
|
|
|
---
|
|
|
|
## 1. Optional adapter는 왜 조용히 무너지는가
|
|
|
|
Optional adapter는 처음엔 꽤 편해 보입니다. Redis가 있으면 cache adapter를 켜고, Kafka가 있으면 message broker adapter를 켜고, Slack이나 이메일 provider는 필요할 때만 붙이면 되니까요.
|
|
|
|
문제는 **"꺼져 있어도 정말 안전한가?"**라는 질문입니다. 실제로는 이런 일들이 조용히 쌓입니다.
|
|
|
|
- env key가 `.env`에는 있는데 `application.yml`에서는 안 쓰이고 있거나
|
|
- optional adapter의 bean이 아무 조건 없이 그냥 등록되거나
|
|
- adapter가 disabled 상태인데 application layer가 그 adapter 패키지를 직접 import하고 있거나
|
|
|
|
이런 상태가 쌓이면 설정은 **계약이 아니라 "대충 이런 분위기"**가 되어버립니다. 누군가 `.env`에 값을 하나 빼먹어도 아무도 모르고, 배포 후에야 터지는 식이죠.
|
|
|
|
ca-tmpl은 이 문제를 `@ConditionalOnProperty` 하나로 끝내지 않았습니다. env registry, `.env` drift gate, `@ConfigurationProperties`, optional adapter package isolation, `@ConditionalOnProperty` 정적 규칙, startup failure exception을 각각 나눠뒀습니다.
|
|
|
|
---
|
|
|
|
## 2. 세 개의 층으로 보기
|
|
|
|
전체 구조를 표로 먼저 보겠습니다.
|
|
|
|
| 층 | 잡는 문제 | 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 |
|
|
|
|
하나씩 보겠습니다.
|
|
|
|
---
|
|
|
|
## 3. ① 설정 파일들이 서로 어긋나지 않게 — Env Registry Gate
|
|
|
|
설정값은 코드 밖에 있지만, 실제로는 코드의 실행 경로를 바꿉니다. `APP_CACHE_REDIS_ENABLED=true`가 들어오면 Redis cache backend가 생기고, `app.messaging.broker=kafka`가 들어오면 Kafka broker bean이 등록되는 식입니다.
|
|
|
|
이런 설정을 문서로만 관리하면 drift가 생깁니다. `.env`에만 남아있는 key, `application.yml`에만 있는 placeholder, 어디에도 등록 안 된 `APP_` key가 조금씩 쌓이는 거죠.
|
|
|
|
ca-tmpl은 이 drift를 **Gradle task로 막습니다.** `verifyEnvKeys`는 `src/.env`, `application.yml`, `docs/registries/env-keys.yaml` 세 파일을 함께 읽습니다.
|
|
|
|
```groovy
|
|
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")
|
|
}
|
|
```
|
|
|
|
이 task는 세 가지를 검사합니다.
|
|
- `application.yml`의 required placeholder가 `.env`에 없으면 → 실패
|
|
- `.env`의 key가 어떤 placeholder에도 안 쓰이면 → 실패
|
|
- `APP_` 접두사를 가진 key가 env registry에 등록 안 돼 있으면 → 실패
|
|
|
|
즉 설정 문서와 실제 boot 설정이 따로 움직이지 않도록 **빌드 단계에서 묶어버립니다.**
|
|
|
|
---
|
|
|
|
## 4. ② Adapter가 켜지는 조건을 코드로 선언하기 — Bean Gating
|
|
|
|
Adapter activation은 Spring bean 조건으로 표현합니다. Redis cache backend는 `app.cache.redis.enabled=true`일 때만 등록됩니다.
|
|
|
|
```java
|
|
@Bean
|
|
@ConditionalOnProperty(
|
|
name = "app.cache.redis.enabled",
|
|
havingValue = "true",
|
|
matchIfMissing = false)
|
|
public CacheBackend redisCacheBackend(RedisClient redisClient) {
|
|
return new RedisCacheStore(redisClient);
|
|
}
|
|
```
|
|
|
|
Kafka broker도 마찬가지입니다. 게다가 조건을 통과해도 설정값 자체가 비어있으면 즉시 실패하도록 되어 있습니다.
|
|
|
|
```java
|
|
@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);
|
|
}
|
|
```
|
|
|
|
이 방식의 장점은, adapter 구현체가 중앙 router나 use case 코드를 직접 건드리지 않고도 **"내가 활성화되는 조건"을 자기 config 안에 스스로 선언**할 수 있다는 점입니다.
|
|
|
|
### 하지만 `@ConditionalOnProperty`만으로는 부족하다
|
|
|
|
여기서 중요한 인정이 필요합니다. **ArchUnit은 런타임 property evaluation을 실행하지 않습니다.** 즉 "지금 이 profile에서 이 bean이 실제로 켜져 있는가"를 증명하는 도구가 아니에요.
|
|
|
|
대신 ca-tmpl은 정적 분석으로 두 가지만 확인합니다.
|
|
|
|
1. application layer가 optional adapter 패키지를 import하지 않는지
|
|
2. optional adapter 패키지 안의 `@Bean` 메서드가 `@ConditionalOnProperty`를 갖고 있는지
|
|
|
|
```java
|
|
static final ArchRule APPLICATION_DOES_NOT_DEPEND_ON_OPTIONAL_ADAPTERS =
|
|
noClasses()
|
|
.that()
|
|
.resideInAPackage("..application..")
|
|
.should()
|
|
.dependOnClassesThat()
|
|
.resideInAnyPackage(OPTIONAL_ADAPTER_PACKAGES);
|
|
```
|
|
|
|
이건 "현재 어떤 profile에서 bean이 켜졌는가"를 증명하는 게 아니라, **disabled-default를 우회할 수 있는 코드 구조 자체를 막는 쪽**입니다. 증명과 방지는 다른 문제이고, ca-tmpl이 하는 건 후자입니다.
|
|
|
|
---
|
|
|
|
## 5. ③ 꺼져 있는데 필요한 경로라면 빨리 실패하기 — Startup Fail-fast
|
|
|
|
세 번째 층은 startup 시점의 fail-fast입니다. required capability adapter가 꺼져 있거나 coordination bean이 없으면 `RequiredAdapterDisabledException` 계열의 startup failure로 드러납니다.
|
|
|
|
cache router나 messaging config 같은 중앙 binding 지점에서도, disabled backend로의 binding이 **조용한 no-op으로 흘러가지 않도록** 설계되어 있습니다.
|
|
|
|
여기서 skeleton이 지키려는 원칙은 이겁니다: **"꺼져 있으면 아무 일도 하지 않는다"가 목표가 아니라, "꺼져 있는데 필요한 경로라면 빨리 실패한다"가 목표입니다.** 조용히 무시되는 것과 시작하자마자 명확하게 실패하는 것은 운영 관점에서 완전히 다른 경험입니다.
|
|
|
|
---
|
|
|
|
## 6. 아직 말할 수 없는 것들
|
|
|
|
ca-tmpl에는 env registry/gate와 optional adapter guard가 구현되어 있고, `./gradlew check`로 로컬 검증됐습니다. 여기까지는 분명하게 말할 수 있습니다.
|
|
|
|
하지만 **모든 provider-specific adapter template가 완성됐다고 말하면 안 됩니다.** Kafka, Redis, Slack, Google Email 같은 표면이 일부 존재하더라도, "모든 외부 provider 전환을 검증했다"는 주장은 이 프로젝트가 실제로 확인한 범위를 넘어섭니다.
|
|
|
|
그래서 이 글의 결론은 **"optional adapter를 완성했다"가 아니라, "optional adapter가 켜지고 꺼지는 실패 모드를 설정 계약으로 드러내기 시작했다"**입니다.
|
|
|
|
---
|
|
|
|
## 마무리
|
|
|
|
Optional adapter를 다루는 방식은 결국 "런타임에 신뢰할 수 있는 상태만 켜지게 하려면 무엇을 정적으로, 무엇을 시작 시점에 확인해야 하는가"의 문제입니다.
|
|
|
|
ca-tmpl은 이 질문에 세 개의 층으로 답했습니다 — 설정 파일 간의 drift를 빌드에서 막고, bean이 켜지는 조건을 코드에 선언하고 우회 경로를 정적으로 막고, 그래도 필요한 게 꺼져 있으면 시작 시점에 실패시키는 것. 이 셋 중 하나만 있었다면 여전히 구멍이 남았을 겁니다.
|