3.6 KiB
NOTIF-ADR-005 — 어느 notification API가 canonical인가
상태
Accepted (2026-08-15). NTF-018 대응.
문제
같은 저장소에 notification 모델이 두 개 있다.
| 세대 | 위치 | 규모 |
|---|---|---|
| R0 legacy | adapter:outbound:notification의 router/provider seam |
삭제 예정 |
| R1 | dev.caskeleton.application.notification (직속) |
public type 100개 |
| Platform | dev.caskeleton.application.notification.platform.. |
신규 |
application-core/CLAUDE.md는 R1을 "R1 canonical"이라 부르고,
docs/notification/migration-guide.md는 R0 → platform 이행만 설명하며 R1의 처분을 전혀 다루지 않는다.
Channel, plan, dispatch, receipt/evidence 모델이 두 namespace에 중복 존재하고 둘 사이에 production
bridge도 import도 없다.
실패 모드는 "무엇이 깨지는가"가 아니라 "무엇을 써야 하는가"다. 새 consumer가 어느 API를 쓸지 알 수 없고, 두 모델이 각자 진화하며, R0를 지운 뒤에도 R1 graph가 고아로 남거나 platform이 R1 정책을 우회하는 이중 canonical이 된다.
결정
Platform이 canonical이다. R1은 유지되지만 새 production consumer를 받지 않는다.
이유는 능력이 아니라 증거다. platform은 durable acceptance, fenced claim, event ledger, projection,
reconciliation을 실제 PostgreSQL 레인으로 증명한다(NOTIF-ADR-001~003). R1은 fake로 증명된 R1 계약이며
스스로 그렇게 선언한다 — application-core/CLAUDE.md가 "R1 application contract proven with fakes.
It does not claim PostgreSQL schema/locking, provider protocol, cryptographic verifier, or runtime
wiring qualification"이라고 적어 둔 그대로다.
Disposition
R1 public type 100개의 처분은 네 가지 중 하나다.
| 처분 | 의미 | 대상 |
|---|---|---|
replace |
platform에 동등물이 있다. 새 consumer는 platform을 쓴다 | Channel, plan/dispatch/receipt/evidence 계열 |
bridge |
변환이 필요하다. 변환은 ACL 한 곳에만 둔다 | writer-cutover / receipt 적용 경로 |
retain |
platform이 다루지 않는 관심사다. 그대로 둔다 | consent/quiescence verifier port |
delete |
R0와 함께 사라진다 | R0 router가 쓰던 seam |
전수 분류표는 이 ADR이 아니라 docs/notification/module-mapping.md가 소유한다. ADR은 규칙을,
mapping 문서는 목록을 소유한다 — 목록을 두 곳에 복제하면 드리프트하는 쪽이 늘어난다.
강제
두 namespace 사이의 production dependency는 0건이며, 이것은 문서가 아니라 ArchUnit 규칙이 지킨다
(CleanArchitectureTest의 NOTIFICATION_R1_AND_PLATFORM_DO_NOT_DEPEND_ON_EACH_OTHER).
변환이 필요해지면 dev.caskeleton.application.notification.compatibility.r1 한 패키지에만 두고, 그
패키지만 규칙에서 예외로 인정한다. 예외를 한 곳으로 모으는 것이 목적이다 — 두 모델이 서로를 아는
지점이 여러 곳이면 "어느 쪽이 canonical인가"라는 질문에 코드가 답하지 못한다.
결과
- 새 production consumer는
..notification.platform..만 쓴다. - R1 type은 남지만, 새 코드가 그것을 import하면 ArchUnit이 막는다.
- R0 삭제는 이 ADR과 무관하게 진행된다. R1은 R0와 함께 사라지지 않는다.
하지 않은 것
R1 100개 type에 @Deprecated(forRemoval = true)를 붙이지 않았다. 제거 시점이 정해지지 않았고,
forRemoval은 "이 릴리스 이후 사라진다"는 약속이라 시점 없이 붙이면 그 자체가 거짓 신호다. 경계는
ArchUnit이 강제하고, deprecation은 제거 계획이 생길 때 붙인다.