콘텐츠로 이동

Context Map

Umbra 는 9개의 Bounded Context 로 구성됩니다. 각 컨텍스트는 명확한 책임과 경계를 가지며, 서로 간 통신은 직접 호출 또는 Outbox 이벤트로 이루어집니다. 이 문서는 컨텍스트의 분류, 관계, 통신 방식을 정리합니다.

Why this matters

Bounded Context 는 DDD 의 핵심 개념이며 Umbra 에서는 Go 패키지 경계PostgreSQL schema 경계 로 동시에 구현됩니다. 컨텍스트 경계가 명확하지 않으면 도메인 로직이 뒤섞여 결제 변경이 복구 기능에 영향을 주거나, 권한 체크 로직이 여러 곳에 중복되는 문제가 발생합니다.

Umbra 의 컨텍스트 구분은 세 단계로 분류됩니다.

  • Core — 비즈니스 차별화가 일어나는 핵심 도메인. 풀세트 Hexagonal 로 구현.
  • Supporting — Core 를 떠받치는 도메인. 단순 구조로 실용적 적용.
  • Generic — 일반 솔루션으로 충분한 도메인. 단순 구조.

분류 기준은 "이 도메인이 Umbra 의 경쟁 우위에 직접 기여하는가" 입니다.

Context inventory

Umbra 의 전체 컨텍스트 목록입니다.

# Context Type Location Description
1 Recovery Core engine/recovery/ 스냅샷, 복구, Live Sync, AntiNuke
2 Licensing Core engine/licensing/ 길드 권한, Plan, Feature 게이팅
3 Billing Core engine/billing/ Toss 결제, 빌링키, 구독 사이클
4 Identity Supporting engine/identity/ Discord User 매핑, OAuth2, 세션
5 Guild Supporting engine/guild/ 길드 등록, 설정, owner 관리
6 Member Supporting engine/member/ 멤버 정보, Web Join 흐름
7 Notification Supporting engine/notification/ Discord DM/채널 알림
8 Audit Generic engine/audit/ 도메인 이벤트 감사 로그
9 Webhook Generic engine/webhook/ 외부 웹훅 수신(Toss)

Recovery 는 내부에 4개의 sub-context 를 가집니다.

  • engine/recovery/subdomain/snapshot/ — 스냅샷 생성 및 저장
  • engine/recovery/subdomain/restore/ — 복구 실행 (Temporal Workflow)
  • engine/recovery/subdomain/sync/ — Live Sync 배치 처리
  • engine/recovery/subdomain/antinuke/ — 이상 감지 및 자동 대응 (Temporal Workflow)

Context map

컨텍스트 간 관계를 DDD 표기법으로 표현합니다. 화살표 방향은 의존 방향입니다.

graph TB
    subgraph Core
        Recovery[Recovery]
        Licensing[Licensing]
        Billing[Billing]
    end

    subgraph Supporting
        Identity[Identity]
        Guild[Guild]
        Member[Member]
        Notification[Notification]
    end

    subgraph Generic
        Audit[Audit]
        Webhook[Webhook]
    end

    Billing -->|Outbox event| Licensing
    Guild -->|Outbox event| Licensing
    Licensing -->|Outbox event| Notification
    Billing -->|Outbox event| Notification
    Recovery -->|Outbox event| Notification

    Recovery -->|reads| Guild
    Recovery -->|reads| Member
    Recovery -->|permission check| Licensing

    Member -->|reads| Guild
    Member -->|reads| Identity

    Billing -->|reads| Identity
    Billing -->|reads| Guild

    Webhook -->|forwards| Billing

    Audit -.->|subscribes all events| Billing
    Audit -.->|subscribes all events| Licensing
    Audit -.->|subscribes all events| Recovery

실선은 동기 호출 또는 이벤트 발행, 점선은 이벤트 구독입니다.

Communication patterns

컨텍스트 간 통신은 세 가지 방식으로 이루어집니다.

1. Direct call (synchronous)

같은 프로세스 안에서 도메인 서비스가 다른 도메인 서비스를 호출합니다. 주로 읽기권한 체크 에 사용됩니다.

Examples

  • Recovery 가 복구 실행 전 Licensing 에 권한 조회
  • Member 가 새 멤버 등록 시 Guild 에서 길드 정보 조회
  • Billing 이 구독 생성 시 Identity 에서 사용자 확인

Why direct — 읽기는 데이터 일관성이 즉시 필요하고 트랜잭션 경계 안에서 처리되어야 합니다. Outbox 는 비동기라 읽기에 부적합합니다.

2. Outbox event (asynchronous)

도메인 상태 변경이 다른 도메인의 상태 변경을 촉발 할 때 사용됩니다. Publisher 는 트랜잭션 안에 이벤트를 outbox 테이블에 기록하고, Poller 가 Subscriber 에 전달합니다.

Examples

  • Billing 에서 결제 성공 → Licensing 이 License 기간 연장
  • Guild 에서 봇 강퇴 감지 → Licensing 이 License suspend, Billing 이 Subscription suspend
  • Licensing 에서 권한 downgrade → Notification 이 사용자 알림

Why outbox — 쓰기 트랜잭션의 일관성을 보장하면서 도메인 간 결합도를 낮춥니다. Publisher 는 Subscriber 를 모릅니다.

3. External input (webhook or gateway)

외부 시스템(Toss, Discord)에서 들어오는 이벤트는 adapter 를 통해 도메인으로 라우팅됩니다.

Examples

  • Toss 웹훅 → Webhook 컨텍스트 → Billing 호출
  • Discord Gateway 이벤트 → Bot 프로세스의 handler → Recovery / Member 등 도메인 호출

Why adapter — 외부 시스템 프로토콜을 도메인 코어에서 분리합니다. Toss 가 Stripe 로 바뀌어도 Webhook adapter 만 변경됩니다.

Context relationships

DDD 의 Context Mapping 패턴으로 각 관계를 분류합니다.

Upstream-Downstream (U ← D)

Downstream 이 Upstream 의 변경에 영향을 받습니다.

Upstream Downstream Pattern
Billing Licensing Published Events (Outbox)
Guild Licensing Published Events (Outbox)
Licensing Notification Published Events (Outbox)
Billing Notification Published Events (Outbox)
Recovery Notification Published Events (Outbox)
Identity Member Open Host Service
Identity Billing Open Host Service
Guild Member Open Host Service
Guild Recovery Open Host Service
Member Recovery Open Host Service

Partnership

서로가 서로에게 영향을 주는 관계는 없습니다. Umbra 의 컨텍스트는 의도적으로 단방향 의존만 허용합니다. 양방향 의존이 필요해 보이는 순간은 도메인 경계 재검토 신호입니다.

Conformist

Umbra 는 외부 시스템의 모델을 그대로 받아들이는 Conformist 관계를 피합니다. Discord API 의 Guild 구조는 engine/guild/Guild entity 로 변환되며, Toss 의 payment 응답은 engine/billing/PaymentAttempt 로 매핑됩니다.

Anti-Corruption Layer

외부 시스템과의 경계는 adapter 레이어가 Anti-Corruption Layer 역할을 합니다.

  • engine/billing/adapter/toss/ — Toss API 모델 → Billing domain model
  • engine/recovery/subdomain/*/adapter/discord/ — Discord API 모델 → Recovery domain model
  • apps/bot/internal/ — Discord Gateway 이벤트 → 도메인 호출
  • apps/api/internal/handler/ — HTTP 요청/응답 → 도메인 호출

이 레이어에서 외부 모델과 내부 모델의 매핑이 일어나며, 도메인 코어는 외부 변경에 보호됩니다.

Dependency rules

컨텍스트 간 의존성에는 엄격한 규칙이 적용됩니다. 이 규칙은 guides/hexagonal-pattern.md 에 상세히 정리되어 있으며, 아키텍처 테스트로 CI 에서 강제됩니다.

Rule 1. Core cannot depend on Supporting or Generic

Core 컨텍스트(Recovery, Licensing, Billing)는 Supporting 또는 Generic 컨텍스트에 직접 의존하지 않습니다. 필요한 데이터는 Port 인터페이스로 정의되며, Adapter 가 실제 Supporting/Generic 을 호출합니다.

Example — Recovery 가 Member 데이터를 필요로 하면 recovery/port/MemberReader 인터페이스를 정의하고, recovery/adapter/member/engine/member/ 를 호출하는 구현을 제공합니다.

Rule 2. Supporting can depend on other Supporting

Supporting 컨텍스트는 서로 자유롭게 의존할 수 있습니다. Identity, Guild, Member 는 단순 구조이므로 Hexagonal 전환의 비용 대비 이점이 적습니다.

Rule 3. Generic can be depended on by all

Audit 과 Webhook 은 다른 컨텍스트에서 자유롭게 참조 가능합니다. Audit 은 모든 이벤트를 구독하고, Webhook 은 외부 진입점 역할을 합니다.

Rule 4. No circular dependency

컨텍스트 간 순환 의존은 금지됩니다. 순환이 필요해 보이면 도메인 경계 재검토 신호입니다.

Rule 5. Events flow only one direction

Outbox 이벤트는 단방향입니다. Billing → Licensing 은 허용되지만 Licensing → Billing 은 다른 이벤트 타입으로 명시되어야 합니다.

Sub-contexts in Recovery

Recovery 는 내부에 4개의 sub-context 를 가지며 이들 간에도 경계가 있습니다.

graph LR
    Sync[sync
Live Sync 배치] Snapshot[snapshot
스냅샷 생성/조회] Restore[restore
복구 실행] AntiNuke[antinuke
이상 감지] Sync -->|writes| DB[(길드 상태 DB)] Snapshot -->|reads| DB Snapshot -->|writes| SnapshotDB[(스냅샷 DB)] Restore -->|reads| SnapshotDB Restore -->|sends signal| Sync AntiNuke -->|triggers| Snapshot AntiNuke -->|sends signal| Restore

각 sub-context 의 책임:

  • sync — Discord Gateway 이벤트를 배치로 DB 에 반영
  • snapshot — 현재 DB 상태를 JSONB 로 스냅샷
  • restore — 스냅샷을 기준으로 Discord 에 복구 실행 (Temporal Workflow)
  • antinuke — Discord 이벤트 패턴 분석, 이상 감지 시 snapshot + restore 트리거 (Temporal Workflow)

sub-context 간 통신도 컨텍스트 간 규칙을 따릅니다. 직접 호출 또는 Temporal Signal 로 조정되며, Outbox 는 사용하지 않습니다(같은 도메인 내부 통신).

Permission model flow

Licensing 은 모든 기능 접근의 단일 진입점입니다. 다른 컨텍스트가 "이 사용자가 이 기능을 쓸 수 있는가" 를 질문할 때 Licensing 이 답합니다.

sequenceDiagram
    participant User
    participant Handler
    participant Domain as Core Domain
    participant Licensing
    participant Repo as License Repo

    User->>Handler: Request action
    Handler->>Licensing: Can(guild_id, feature_id)
    Licensing->>Repo: Get active license
    Repo-->>Licensing: License + Plan
    Licensing-->>Handler: Permitted / Denied
    alt Permitted
        Handler->>Domain: Execute use case
        Domain-->>Handler: Result
    else Denied
        Handler-->>User: 403 Forbidden
    end

이 패턴은 bot, api, worker 모든 진입점에서 동일하게 적용됩니다.

Integration strategy

새 컨텍스트를 추가하거나 기존 컨텍스트를 변경할 때 따르는 전략입니다.

Adding a new context

  1. 분류 결정 — Core / Supporting / Generic 중 어디에 속하는가
  2. 경계 정의 — 다른 컨텍스트와 어떤 관계를 맺는가
  3. 폴더 생성 — engine/{name}/ (Core 는 Hexagonal 풀세트, Supporting/Generic 은 단순 구조)
  4. Schema 분리 — db/schema/{name}.sql, db/queries/{name}/
  5. 이벤트 정의 — Publish/Subscribe 할 이벤트를 platform/event/ 카탈로그에 등록
  6. ADR 작성 — 결정 근거 기록

Modifying context boundaries

경계 변경은 아키텍처 변경입니다. ADR 로 기록하고 팀 리뷰를 거쳐야 합니다.

Splitting a context

컨텍스트가 커져서 분리가 필요한 신호:

  • 한 컨텍스트에서 여러 팀이 작업하며 merge conflict 빈번
  • 특정 기능 변경이 다른 기능에 영향을 줌 (응집도 저하)
  • 컨텍스트 내부에 명확히 다른 비즈니스 의미가 공존

분리 시 Recovery 처럼 sub-context 로 시작하고, 더 큰 분리가 필요하면 top-level 컨텍스트로 승격합니다.

Constraints

  • 도메인 간 직접 DB 접근 금지 — Licensing 이 Billing 테이블을 직접 SELECT 하는 것 금지
  • 순환 이벤트 금지 — A → B → A 이벤트 체인 금지
  • 외부 시스템은 항상 adapter 경유 — 도메인 코어에서 Discord API 직접 호출 금지
  • sqlc 코드는 해당 도메인에서만 사용 — db/queries/billing/ 에서 생성된 코드는 engine/billing/adapter/persistence/ 안에서만 import

See also

  • architecture/overview.md — 시스템 아키텍처 전체
  • architecture/event-flow.md — Outbox 이벤트 카탈로그와 흐름
  • architecture/process-communication.md — 프로세스 간 통신 패턴
  • guides/hexagonal-pattern.md — Hexagonal 적용 가이드
  • domain/ — 각 컨텍스트 상세 문서
  • adr/0016-outbox-pattern.md — Outbox 패턴 선택
  • adr/0019-hexagonal-pragmatic.md — 실용적 Hexagonal 적용