Files
clean-architecture-backend-…/docs/notification/adr/NOTIF-ADR-005-canonical-namespace.md
T

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 규칙이 지킨다 (CleanArchitectureTestNOTIFICATION_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은 제거 계획이 생길 때 붙인다.