Files
llm-wiki/raw/interviews/optional-adapter-3-layer-disabled-detection-2026-06-09.md

49 lines
4.0 KiB
Markdown

---
title: interview / optional-adapter-3-layer-disabled-detection-2026-06-09
source_type: interview-prep
status: raw
related_branches: [feature-integration-adapter-templates]
related_projects: [ca-skeleton]
tags: [interview, ca-skeleton, spring, clean-architecture, adapter, observability]
created: 2026-06-09
status_label: raw
---
# interview: optional-adapter-3-layer-disabled-detection-2026-06-09
> Layer: `raw/interviews/` — 이 작업에서 정직하게 뽑을 수 있는 면접 질문/답변.
## Parent / 부모
- [[raw/branch-notes/feature-integration-adapter-templates]]
## Q1. 선택형 어댑터(Kafka/Redis/Slack/Email)를 "꺼져 있음"으로 안전하게 보장하려면?
3계층으로 검출한다.
- **Layer 1 (startup, runtime)** — Spring `@ConditionalOnProperty(name="app.<domain>.<adapter>.enabled", havingValue="true", matchIfMissing=false)`. flag 미설정/false 면 real adapter bean 미등록(disabled bean count = 0). `matchIfMissing=false` 를 명시해 **누락=disabled** 가 사고가 아니라 의도가 되게 한다.
- **Layer 2 (build, static)** — ArchUnit. (a) application layer 가 optional adapter 패키지를 import 하지 못하게 격리, (b) optional adapter 패키지의 모든 `@Bean``@ConditionalOnProperty` 로 gating 됐는지 검사. 정적 검사는 "후보 클래스가 annotation 을 가짐" 까지만 보장한다.
- **Layer 3 (runtime, fail-fast)** — disabled 일 때 port 를 만족시키는 sentinel(`DisabledMessagePublisher` 등)을 등록해, 우회 호출이 들어오면 `AdapterDisabledException` 으로 즉시 throw(silent no-op/timeout 대기 금지).
## Q2. 각 계층의 한계는?
- Layer 1 의 "bean count = 0 검증" 은 Spring 공식 검증 패턴이 아니라 프로젝트 자체 선택(통합 테스트로 assert).
- Layer 2 는 runtime config 평가를 못 하므로 "실제 active 여부" 는 보장 못 함 → Layer 3 로 위임.
- Layer 1 이 정상 경로에선 bean 자체를 안 만들어 호출 불가이므로, Layer 3 는 "Layer 1·2 를 우회한 호출의 최후 방어선" 일 뿐 정상 경로 코드가 아니다.
## Q3. 어댑터별 실패를 fail-open 으로 둔 이유와 예외는?
- skeleton 기본은 fail-open: 알림/캐시/메시지는 핵심 use case 의 **부수효과**라 전송/캐시 실패가 HTTP 5xx 로 승격되면 안 됨.
- Kafka: publish 실패 → correlationId 부착 로그 + outbox/retry 위임, core 는 성공.
- Redis: unavailable → cache-miss 로 graceful degrade(절대 INTERNAL 로 뭉개지 않음).
- Slack/Email: 전송 실패 → 관측(metric/log)만, 단 provider body/PII 는 로그에 절대 미등장(로거 시그니처에 payload 인자 자체를 없애 구조적으로 차단).
- 예외: notification 이 use case 의 **primary outcome**(예: 비밀번호 재설정 메일 자체가 목적)이면 도메인 branch 가 동기 + fail-closed 로 호출 — skeleton scope 밖.
## Q4. disabled adapter runtime 호출에 startup 의 `REQUIRED_ADAPTER_DISABLED` 코드를 재사용하지 않은 이유?
- 그 코드는 `feature-migration-startup-contract` 소유 + startup-exit(72) 시맨틱(= disabled required adapter 로 app 이 뜨면 실패). runtime invoke 는 **lifecycle 이 다르다**. 하나의 코드로 startup·runtime 두 의미를 표현하면 운영/런북이 혼동된다 → runtime 전용 `ADAPTER_DISABLED`(INTERNAL/500/retryable=false) 를 본 branch owner 로 신설. retryable=false 인 이유: 재배포 전까지 계속 disabled → 재시도로 안 풀리는 결정적 설정 버그(= `INTERNAL_AUTH_MISCONFIGURATION` 과 동류).
## Q5. 왜 spring-kafka/lettuce 같은 실 SDK 를 안 넣었나?
- skeleton 이 모든 선택형 adapter SDK 를 기본 탑재하면 무거워진다. 대신 `KafkaSender`/`RedisClient`/`SlackClient`/`GoogleEmailClient` 같은 **integration seam(interface)** 만 제공하고, 실제 client 구현 + SDK 의존은 해당 adapter 를 켜는 fork 프로젝트가 추가한다. 템플릿은 "실패/관측 계약 + on/off 메커니즘" 을 소유하고, 운영 연동은 소비자가 채운다.