adapter:outbound:notification — R0 legacy truth
현재 구현 전체는 교체 전 호환성 기준선인
R0 legacy다.GoogleEmailClient와SlackClient는 project-supplied seam일 뿐 실제 Google Mail 또는 Slack 연동이 아니며, provider/card qualification evidence도 없다.
알림(email/Slack 등) 아웃바운드 어댑터 모듈. 패키지 루트:
dev.caskeleton.adapter.outbound.notification. :adapter:outbound:support 에 의존해 공유
correlation / fail-open 의존성 로깅을 재사용한다.
허용/금지 의존 정책은 src/config/architecture/modules.json의
adapter-outbound-notification row가 SSOT다. 이 문서는 코드 주석에서 덜어낸 설계 결정의
근거와 canonical 구현 전 삭제 대상을 모아둔 참조용 기록이다.
Task 1 R0 truth table
| 축 | 현재 사실 | 증거와 한계 |
|---|---|---|
| application contract | raw NotificationPort.notify(Channel, route, Notification) |
mode, transaction, receipt, attempt certainty가 없는 R0 port |
| routing | (channel, providerId) registry + route별 provider ID list fan-out |
RoutingNotifierTest; route list의 모든 provider를 순서대로 호출 |
| provider failure | 모든 provider를 중앙 FailOpenNotificationProvider로 감싸고 예외를 삼킴 |
global fail-open이며 application kind별 정책이 아님 |
| unbound route | AdapterDisabledException fail-fast |
NotificationAdapterTest; disabled sentinel은 없음 |
| email seam | google-email + GoogleEmailClient interface |
module 안 production client/SDK/credential/protocol 구현 0 |
| Slack seam | slack-webhook + SlackClient interface |
module 안 production client/SDK/credential/protocol 구현 0 |
| configuration | code는 app.notification.routes.*와 provider별 *.enabled를 읽음 |
checked-in application.yml/env registry의 app.notification.{slack,email}.provider selector와 drift |
| production consumer | feature/application production consumer 0 | main source에는 application contract 선언, adapter 구현과 bootstrap composition만 존재 |
| evidence grade | local fake/contract baseline | real provider, durability, callback, security, load evidence 0; 모든 seam R0 legacy |
selector drift는 이 기준선의 일부다. Task 1에서는 고치지 않는다. canonical graph가 준비되고 cutover evidence가 생기기 전까지 기존 key를 새 의미로 재사용하거나 legacy class에 production 동작을 추가하지 않는다.
모듈 개요
application-core 포트 뒤에 두는 선택형 알림 어댑터다. @ConditionalOnProperty 로 게이팅되고
기본 비활성이다. 이 모듈이 기본 제공하는 프로바이더는 email/google(GoogleEmailProvider /
GoogleEmailClient)과 slack/webhook(SlackWebhookProvider / SlackClient)이며, 실제 연동
client 는 포킹 프로젝트가 채우는 seam 이다.
(channel, providerId) 복합 키 + fan-out
RoutingNotifier 는 (channel, providerId) 복합 키로 프로바이더를 등록한다 — 채널 내 중복
providerId 는 생성 시점에 실패한다. 라우트당 providerId 목록을 주면 fan-out(모든 프로바이더
호출)이 된다. 각 프로바이더는 이미 FailOpenNotificationProvider 로 감싸져 있어 한 곳의 실패가
다른 곳을 막지 않고, 그래서 fan-out 루프에 try/catch 가 필요 없다. FailOpenNotificationProvider .send 가 throws 를 선언하지 않는 건 이 루프를 try/catch 없이 예외-free 로 증명하기 위함이다.
중앙 fail-open 합성 + 라우팅 바인딩
NotificationConfig 가 모든 프로바이더를 FailOpenNotificationProvider 로 중앙에서 감싼다(→
:adapter:outbound:support 의 FailOpenDependencyLogger 로 WARN 로깅). 라우팅은
app.notification.routes.<channel>.<route>=<providerId>[,<providerId>]. 프로바이더는
channel()+providerId() 로 키잉된 NotificationProvider 빈으로 기여한다(예:
GoogleEmailProvider, SlackWebhookProvider). GoogleEmailClient/SlackClient 는 포크가
구현하는 seam 이며 실패는 데코레이터가 fail-open 처리한다.
Wave G deletion inventory
다음 surface는 canonical-only cutover와 retained evidence 검증이 끝난 뒤 한 묶음으로 제거한다. 그 전에는 동작을 확장하지 않고 R0 회귀 기준선으로만 유지한다.
- application R0 contract:
Channel,Notification, rawNotificationPort - router/decorator SPI:
NotificationConfig,NotificationRoutesSettings,RoutingNotifier,NotificationProvider,FailOpenNotificationProvider - fake-only Google email seam:
GoogleEmailClient,GoogleEmailProvider,GoogleEmailNotificationAdapterConfig - fake-only Slack webhook seam:
SlackClient,SlackWebhookProvider,SlackNotificationAdapterConfig - legacy configuration/tests:
app.notification.routes.*,app.notification.google-email.enabled,app.notification.slack-webhook.enabled, driftedAPP_NOTIFICATION_EMAIL_PROVIDER/APP_NOTIFICATION_SLACK_PROVIDER,NotificationAdapterTest,RoutingNotifierTest와 bootstrap legacy gating cases
accepted 또는 indeterminate work를 inventory하지 않은 상태에서 이 목록을 삭제하거나 canonical provider로 자동 재전송하지 않는다.