콘텐츠로 이동

ADR-0018: Domain Code Sharing over RPC

Umbra 의 세 Go 프로세스(bot, api, worker)는 서로를 RPC 로 호출하지 않는다. 대신 같은 engine/* 도메인 패키지를 import 하여 각자의 진입점에서 유스케이스를 직접 실행한다. 프로세스 간 조정이 필요한 경우에만 Outbox 또는 Temporal 을 경유한다.

Status

Accepted

  • Decided at — 2026-04-13
  • Decided by — Pablo

Context

프로세스를 분리(ADR-0017)했을 때 자연스럽게 따라오는 질문: "프로세스들이 서로 어떻게 통신하는가?"

세 가지 옵션:

  1. Full microservices — 각 프로세스가 독립 서비스, REST/gRPC 로 통신
  2. 분산 모놀리스 — 같은 코드베이스 공유, 각자 도메인 직접 호출, 간접 통신만 DB/queue 경유
  3. 혼합 — 일부는 RPC, 일부는 공유

Umbra 의 특수 요구사항:

  • Discord 슬래시 커맨드는 3초 내 응답 필수. Bot 이 API 를 RPC 로 호출하면 네트워크 왕복 부담
  • MVP 규모에서 RPC 인프라(gRPC, protobuf, 버전 호환성) 관리 부담 과도
  • 팀이 소규모 (Pablo + 진욱) 이라 단일 코드베이스 관리가 훨씬 쉬움

Decision

프로세스 간 직접 RPC 를 사용하지 않는다. 세 프로세스는 같은 engine/* 패키지를 공유하며, 각자의 진입점에서 도메인 서비스를 직접 호출한다.

Shared packages

engine/           ← 도메인 코어 (세 프로세스 공유)
├─ recovery/
├─ licensing/
├─ billing/
├─ identity/
├─ guild/
├─ member/
├─ notification/
├─ audit/
└─ webhook/

platform/         ← 인프라 유틸 (세 프로세스 공유)
├─ event/         (Outbox)
├─ db/
├─ redis/
├─ temporal/
├─ uuid/
├─ crypto/
└─ ...

apps/
├─ bot/           ← 독립 진입점, engine/platform import
├─ api/           ← 독립 진입점, engine/platform import
└─ worker/        ← 독립 진입점, engine/platform import

Inter-process coordination

직접 RPC 가 필요한 경우는 실제로 없음. 대신 다음 간접 경로 사용:

  • 상태 변경 전파 — Outbox 이벤트 (DB 경유)
  • 진행 중 워크플로우와 조정 — Temporal Signal
  • (미래) 실시간 브로드캐스트 — Redis pub/sub (MVP 범위 외)

선택 근거:

  • latency 0 — 네트워크 왕복 없이 함수 호출
  • MVP 복잡도 최소화 — gRPC, protobuf, 버전 호환성 불필요
  • 팀 규모 적합 — 2인 팀이 하나의 코드베이스 유지가 가장 효율적
  • 추후 분리 가능성 보존 — 도메인이 Port/Adapter 로 캡슐화되어 있어 필요 시 Adapter 를 RPC 구현으로 교체 가능

이를 팀 내부에서는 "distributed monolith" (의도적 분산 모놀리스) 라고 부른다.

Consequences

Positive

  • 슬래시 커맨드의 3초 제한이 편안함 (네트워크 왕복 0)
  • MVP 인프라 복잡도 ↓
  • 도메인 로직 중복 없음 (세 프로세스가 같은 구현 사용)
  • 디버깅 단순 (스택 트레이스 그대로, RPC 경계 없음)
  • 리팩토링 안전 (IDE 가 전체 코드베이스 인식)

Negative

  • 세 프로세스의 Docker 이미지가 같은 코드를 포함 (이미지 크기 3배)
  • 전체 레포가 단일 module 로 묶임 → 한 부분의 빌드 오류가 모두 영향
  • "진짜 마이크로서비스 전환" 비용이 미래에 발생할 수 있음

Neutral

  • 도메인 코드는 Hexagonal 로 캡슐화되어 언제든 RPC adapter 추가 가능
  • Go workspace (go.work) 또는 단일 go.mod 로 관리 (현재는 단일 module)

Guidance: when to cross process boundaries

세 프로세스가 언제 어떤 경계를 넘는가?

Within-process (direct call)

같은 프로세스 안에서는 도메인 서비스를 직접 호출. 예:

  • apps/bot/internal/handler/snapshot.goengine/recovery/subdomain/snapshot/Service.Create() 호출
  • apps/api/internal/handler/billing.goengine/billing/Service.StartSubscription() 호출

Cross-process (indirect via DB or Temporal)

다른 프로세스에 작업을 넘기는 경우:

  • Bot → Worker: "이 Restore 를 실행해줘" → Temporal workflow 시작
  • Worker → Worker: "결제 재시도 24h 뒤" → asynq delayed task
  • Billing(worker) → Licensing(worker): "결제 성공" → Outbox event
  • API → Worker: "알림 발송" → asynq task enqueue

이 경계들은 네트워크 RPC 가 아니라 DB/queue 경유 이다.

Alternatives considered

Alternative 1: Full microservices (RPC)

Pros

  • 도메인 독립 배포
  • 각 서비스가 완전 독립

Cons

  • 슬래시 커맨드의 3초 제한 부담 ↑
  • RPC 스펙(proto) 관리 부담
  • 버전 호환성 관리
  • 팀 규모 대비 과한 복잡도

Why rejected — MVP 범위 초과. 팀 2인에게 불필요한 오버헤드.

Alternative 2: Full monolith (단일 프로세스)

Pros

  • 가장 단순

Cons

  • 장애 격리 불가 (ADR-0017 에서 거부)

Why rejected — ADR-0017 에서 프로세스 분리가 결정되었으므로 이 옵션은 불가.

Alternative 3: Monorepo + 서비스별 독립 module (RPC)

Pros

  • 코드 공유와 RPC 동시

Cons

  • 공유 module + RPC spec 관리 부담 이중 증가
  • 이점 불명 (결국 RPC 를 쓰는 마이크로서비스와 유사)

Why rejected — 복잡도 대비 이점 부족.

Alternative 4: 공유 라이브러리를 별도 repo

Pros

  • 진짜 마이크로서비스 분리

Cons

  • 버전 관리 (공유 라이브러리 bump 마다 세 서비스 재빌드)
  • PR 이 여러 repo 에 걸침
  • MVP 에 과함

Why rejected — 2인 팀에게 관리 부담 과도.

Compliance

  • engine/*platform/* 는 여러 프로세스가 import 가능
  • 세 프로세스(apps/bot, apps/api, apps/worker)는 서로 import 금지 (cyclic 방지)
  • 프로세스 간 상태 변경 전파는 Outbox event 로만
  • 진행 중 워크플로우 조정은 Temporal Signal 로만
  • "Bot 에서 API 를 HTTP 로 호출" 같은 패턴 금지 (코드 리뷰 차단)

Revisit triggers

  • 팀 규모 10인 이상 — 도메인별 소유 분리가 필요해지면 마이크로서비스 전환 검토
  • 도메인별 배포 주기 분리 필요 — 예: Recovery 만 빠르게 배포
  • 한 언어로 모두 재작성 — Rust 재작성(ADR-0031) 시 재평가
  • 성능 프로파일링 결과 — 이미지 크기나 빌드 시간이 심각해지면

진짜 마이크로서비스 전환이 필요해지면 Hexagonal 의 Port/Adapter 덕에 다음 방식이 가능:

  • Billing 의 engine/billing/app/Serviceengine/billing/adapter/grpc/Client 로 교체
  • 다른 프로세스는 Client 경유로 호출 (함수 시그니처 동일)
  • 점진적 분리 가능

References

  • ADR-0009 — Monorepo 구조
  • ADR-0017 — 프로세스 분리
  • ADR-0016 — 도메인 간 이벤트 전달
  • ADR-0019 — Port/Adapter (미래 RPC 전환 가능성)
  • ADR-0031 — Rust 재작성 시 재평가