Uber DOMA: 마이크로서비스 관리 방법론
원문: Introducing Domain-Oriented Microservice Architecture - Uber Engineering (2020)
들어가며

2018에서 2020년, Uber는 2,200개 이상의 마이크로서비스를 운영하고 있었다. 그리고 위 포스팅에서 그 혼란과 해결책을 공유했다.
마이크로서비스는 모놀리스의 문제를 해결하기 위해 도입됐지만, 규모가 커지면서 새로운 종류의 복잡성을 만들어냈다.
Uber의 DOMA(Domain-Oriented Microservice Architecture)는 이 복잡성을 관리하기 위한 아키텍처 원칙이다.
DOMA는 마이크로서비스를 부정하는 게 아닌 마이크로서비스를 조직화하는 방법론이다.
Uber의 여정: 모놀리스에서 카오스까지
2012년: 모놀리스 시절
Uber는 단일 Python 애플리케이션으로 시작했다.
초기에는 이 구조가 잘 작동했다. 팀이 작고, 코드베이스가 작고, 비즈니스 로직이 단순했기 때문이다.
2014-2018년: 마이크로서비스 폭발
회사가 성장하면서 모놀리스는 한계에 부딪혔다:
- 배포에 몇 시간이 걸림
- 한 팀의 변경이 다른 팀에 영향
- 새 기능 추가가 점점 어려워짐
해결책으로 마이크로서비스를 도입했고, 서비스 수가 폭발적으로 증가했다.
2018년: 새로운 문제의 시작
┌─────────────────────────────────────────────────────────────┐
│ 마이크로서비스 복잡성의 기하급수적 증가 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 서비스 수와 잠재적 연결 수: │
│ │
│ N개 서비스 → N × (N-1) / 2 개의 잠재적 연결 │
│ │
│ 10개 → 45개 연결 (관리 가능) │
│ 100개 → 4,950개 연결 (어려움) │
│ 500개 → 124,750개 연결 (카오스) │
│ 2,200개 → 2,418,900개 연결 (??????) │
│ │
│ "서비스가 늘어날수록 복잡성은 선형이 아니라 │
│ 제곱에 비례하여 증가한다" │
│ │
└─────────────────────────────────────────────────────────────┘
마이크로서비스의 진짜 문제들
1. 시스템을 이해할 수 없다
2,200개 서비스 중에서 내가 수정해야 할 서비스는 어디에 있을까?
이 서비스는 어떤 다른 서비스에 의존하고 있을까? 내가 이 코드를 바꾸면 어디에 영향을 미칠까?
이런 질문에 답하기가 점점 어려워졌다. 새로 입사한 엔지니어가 시스템 전체를 파악하는 것은 불가능에 가까워졌다.
2. 정보가 사일로에 갇힌다
각 팀이 자신의 서비스만 알고, 전체 그림을 아는 사람이 없다:
- A팀: "우리는 결제 서비스만 담당해요"
- B팀: "우리는 알림 서비스만 알아요"
- C팀: "우리는 CNI만 알아요"
- 누구도: "결제 후 알림이 어떻게 연결되는지"를 책임지지 않음
3. 변경의 파급 효과를 예측할 수 없다
하나의 서비스를 수정하면, 그것에 의존하는 수십 개의 서비스에 영향을 미칠 수 있다.
하지만 그 의존성을 모두 파악하기가 어렵다.
4. 중복 기능이 생긴다
팀들이 독립적으로 움직이다 보니, 비슷한 기능을 여러 팀이 각자 만드는 일이 발생한다:
- A팀: 자체 인증 로직 구현
- B팀: 또 다른 인증 로직 구현
- 결과: 유지보수 비용 2배, 보안 취약점 2배
5. 온보딩이 끝없이 어려워진다
새 팀원에게 "우리 시스템은..."이라고 설명하려면 어디서부터 시작해야 할까? 2,200개 서비스를 다 설명할 수는 없다.
DOMA의 핵심 원칙
Uber는 이 문제들을 해결하기 위해 4가지 핵심 원칙을 세웠다.
원칙 1: Domain (도메인)
핵심 아이디어: 관련 있는 마이크로서비스들을 논리적 그룹으로 묶는다.
┌─────────────────────────────────────────────────────────────┐
│ 도메인의 개념 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Before: 2,200개의 개별 서비스 │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ S │ S │ S │ S │ S │ S │ S │ S │ S │ S │ S │ S │ ... │
│ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ │
│ (서비스 하나하나가 개별 단위) │
│ │
│ After: 도메인으로 그룹화 │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Rides 도메인 │ │ Eats 도메인 │ │
│ │ ┌───┬───┬───┐ │ │ ┌───┬───┬───┐ │ │
│ │ │ S │ S │ S │ │ │ │ S │ S │ S │ │ │
│ │ └───┴───┴───┘ │ │ └───┴───┴───┘ │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ 도메인 = 하나의 비즈니스 capability를 나타내는 서비스 집합 │
│ │
└─────────────────────────────────────────────────────────────┘
도메인의 특징:
- 하나의 비즈니스 개념을 대표 (예: "라이드", "배달", "결제")
- 하나의 팀이 소유하고 책임짐
- 내부 서비스들은 외부에서 직접 접근 불가 (캡슐화)
실제 예시 (Uber):
- Rides 도메인: 매칭, 가격 책정, 배차 서비스
- Eats 도메인: 주문, 배달, 레스토랑 관리 서비스
- Maps 도메인: 라우팅, ETA 계산, 지오코딩 서비스
- Payments 도메인: 결제 처리, 정산, 프로모션 서비스
원칙 2: Layer (레이어)
핵심 아이디어: 도메인들을 수직적 계층으로 조직화하고, 의존 방향을 규칙화한다.
┌─────────────────────────────────────────────────────────────┐
│ 레이어 계층 구조 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Layer 2: Edge / Product │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Rider App Driver App Merchant App │ │
│ │ (최종 사용자 facing 제품) │ │
│ └──────────────────────┬──────────────────────────────┘ │
│ │ 의존 │
│ ▼ │
│ Layer 1: Business Logic │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Rides Domain Eats Domain Maps Domain │ │
│ │ (핵심 비즈니스 로직) │ │
│ └──────────────────────┬──────────────────────────────┘ │
│ │ 의존 │
│ ▼ │
│ Layer 0: Infrastructure │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Auth Storage Messaging Observability │ │
│ │ (공통 인프라 서비스) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
핵심 규칙: 아래 레이어는 위 레이어에 의존하면 안 된다.
- ✅ Rider App → Rides Domain → Auth (위에서 아래로)
- ❌ Auth → Rides Domain (아래에서 위로) 금지!
이 규칙이 왜 중요할까?
- 순환 의존 방지: A→B→C→A 같은 순환이 생기면 시스템이 엉킴
- 계층별 안정성: 아래 레이어일수록 안정적이고 변경이 적음
- 테스트 용이성: 아래 레이어를 모킹하면 위 레이어 테스트 가능
원칙 3: Gateway (게이트웨이)
핵심 아이디어: 각 도메인은 단일 진입점(Gateway)을 통해서만 접근 가능하다.
┌─────────────────────────────────────────────────────────────┐
│ Gateway 패턴 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 외부 (다른 도메인, 제품) │
│ │ │
│ │ 오직 Gateway를 통해서만 접근 │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Rides Domain Gateway │ │
│ │ │ │
│ │ 역할: │ │
│ │ • 외부 요청 수신 │ │
│ │ • 내부 서비스로 라우팅 │ │
│ │ • API 버전 관리 │ │
│ │ • 인터페이스 안정성 보장 │ │
│ └──────────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Matching │ │ Pricing │ │ Dispatch │ │
│ │ Service │ │ Service │ │ Service │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ↑ ↑ ↑ │
│ └──────────────┴──────────────┘ │
│ 도메인 내부 서비스 직접 접근 금지! │
│ │
└─────────────────────────────────────────────────────────────┘
Gateway가 제공하는 이점:
- 캡슐화: 도메인 내부 구현을 숨김
- 리팩토링 자유: 내부 서비스를 마음대로 재구성해도 외부 영향 없음
- 계약 안정성: Gateway API만 안정적이면 됨
- 중앙화된 정책: 인증, 로깅, 모니터링을 한 곳에서
원칙 4: Extension (확장)
핵심 아이디어: 도메인의 핵심 로직을 수정하지 않고 기능을 확장할 수 있는 플러그인 포인트를 제공한다.
┌─────────────────────────────────────────────────────────────┐
│ Extension 패턴 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Rides 도메인 (Core) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 라이드 요청 처리 플로우: │ │
│ │ │ │
│ │ 요청 수신 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ [Extension Point: pre_ride_hooks] ◀── 여기 확장! │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 드라이버 매칭 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 가격 계산 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ [Extension Point: price_modifiers] ◀── 여기도 확장! │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 라이드 생성 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 등록된 Extensions: │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Safety │ │ Promo │ │ Rewards │ │
│ │ Extension │ │ Extension │ │ Extension │ │
│ │ │ │ │ │ │ │
│ │ pre_ride: │ │ price_mod: │ │ post_ride: │ │
│ │ 안전 체크 │ │ 할인 적용 │ │ 포인트 적립 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Extension이 필요한 이유:
"라이드 시작 전에 안전 체크를 추가하고 싶어요" → Rides 코드를 직접 수정?
- ❌ Rides 코드 직접 수정: Rides 팀 승인 필요, 배포 충돌, 코드 복잡해짐
- ✅ Extension 등록: Safety 팀이 독립적으로 개발/배포, Rides 코드 변경 없음
도메인 간 통신 원칙
비동기를 기본으로
DOMA에서 도메인 간 통신은 가능하면 비동기(Async)를 권장한다.
┌─────────────────────────────────────────────────────────────┐
│ 도메인 간 통신 방식 비교 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 동기 통신 (Synchronous): │
│ ┌─────────────┐ 요청 ┌─────────────┐ │
│ │ Domain A │ ──────▶│ Domain B │ │
│ │ │◀────── │ │ │
│ └─────────────┘ 응답 └─────────────┘ │
│ │
│ 문제점: │
│ • A가 B의 응답을 기다리는 동안 블로킹 │
│ • B가 죽으면 A도 영향 받음 (장애 전파) │
│ • A와 B가 강하게 결합됨 │
│ │
│ 비동기 통신 (Asynchronous): │
│ ┌─────────────┐ ┌───────┐ ┌─────────────┐ │
│ │ Domain A │──────▶│ MQ │──────▶│ Domain B │ │
│ └─────────────┘ 이벤트 └───────┘ └─────────────┘ │
│ │ │
│ └── 응답 기다리지 않고 바로 다음 작업 │
│ │
│ 장점: │
│ • A는 메시지만 보내고 바로 다음 작업 │
│ • B가 죽어도 A는 영향 없음 (MQ가 버퍼링) │
│ • 느슨한 결합 │
│ │
└─────────────────────────────────────────────────────────────┘
언제 동기 통신을 쓰나?
모든 통신을 비동기로 할 수는 없다. 동기 통신이 필요한 경우:
| 즉각적인 응답이 비즈니스 요구사항 | 실시간 가격 조회, 잔액 확인 |
| 트랜잭션 일관성 필요 | 결제 승인 후 주문 확정 |
| 사용자가 결과를 기다림 | 로그인 인증 |
DOMA가 해결하는 문제들
Before vs After
| 시스템 이해 | 2,200개 서비스 각각 파악 필요 | 도메인 단위로 파악 (훨씬 적은 수) |
| 소유권 | 서비스마다 다른 팀 | 도메인 = 팀 (명확한 책임) |
| 변경 영향 | 예측 불가 | Gateway가 변경 격리 |
| 온보딩 | 어디서 시작? | 도메인부터 학습 |
| 기능 추가 | 여러 서비스 수정 | Extension 등록 |
구체적인 효과
- 인지 부하 감소: "우리 팀은 Rides 도메인을 담당해요" - 범위가 명확
- 독립적 개발: 도메인 내부는 자유롭게 변경, 외부 영향 없음
- 장애 격리: Gateway가 도메인의 방화벽 역할
- 확장 유연성: Extension으로 기능 추가가 쉬워짐
DOMA 도입 시 주의점
도메인 경계 정하기가 가장 어렵다
잘못된 도메인 경계는 오히려 문제를 악화시킨다:
- 너무 작은 도메인: 서비스 1-2개짜리 도메인 → 오버헤드만 증가
- 너무 큰 도메인: 서비스 100개짜리 도메인 → 그냥 작은 모놀리스
- 잘못된 경계: 강하게 결합된 서비스들을 다른 도메인에 배치 → 도메인 간 통신 폭증
좋은 도메인 경계의 특징:
- 하나의 비즈니스 capability를 대표
- 팀 구조와 일치
- 도메인 내부 통신 >> 도메인 간 통신
점진적 도입이 핵심
모두가 하루아침에 DOMA를 적용할 수는 없다.
(이코에코는 태생이 AI/Cloud Native, 도메인 분리로 출발해 베이스라인이 마련된 상태다.)
- 가장 문제가 심한 영역부터 도메인화
- Gateway 먼저 도입하여 경계 명확화
- 점진적으로 도메인 확대
- Extension 시스템은 나중에
핵심 개념 정리
| Domain | 관련 서비스들의 논리적 그룹 | 인지 부하 감소, 명확한 소유권 |
| Layer | 수직적 의존성 계층화 | 순환 의존 방지, 안정성 확보 |
| Gateway | 도메인의 단일 진입점 | 캡슐화, 리팩토링 자유 |
| Extension | 플러그인 방식 확장 | 코어 수정 없이 기능 추가 |
더 읽을 자료
- Uber's Fulfillment Platform - DOMA 실제 적용 사례
- Building Microservices, 2nd Edition - Sam Newman
- Team Topologies - 팀 구조와 아키텍처의 관계
# workloads/namespaces/base/namespaces.yaml 에서 발췌
apiVersion: v1
kind: Namespace
metadata:
name: scan
labels:
istio-injection: enabled
app.kubernetes.io/part-of: ecoeco-backend # 제품군
tier: business-logic # 레이어
role: api # 역할
domain: scan # 도메인
라벨 체계:
| 라벨 | 값 | 설명 |
|---|---|---|
app.kubernetes.io/part-of |
ecoeco-backend, ecoeco-platform |
제품군 구분 |
tier |
business-logic, data, observability, infrastructure, integration |
DOMA Layer |
role |
api, database, cache, messaging, metrics, dashboards |
세부 역할 |
domain |
auth, scan, character, my, chat, location, image |
비즈니스 도메인 |
2. 레이어 구조 (실제 코드 기반)
┌─────────────────────────────────────────────────────────────────────────┐
│ Eco² 레이어 아키텍처 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Layer 2: Product (Edge) │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Mobile Web App (React/PWA) ─────────▶ api.growbin.app │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ═══════════════════════════════════════════════════════════════════ │
│ │ Istio Ingress Gateway (eco2-gateway) │ │
│ │ └─▶ AuthorizationPolicy → ext-authz (인증) │ │
│ │ └─▶ VirtualService (라우팅) │ │
│ ═══════════════════════════════════════════════════════════════════ │
│ │ │
│ Layer 1: Business Logic (tier=business-logic) │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ AI Domain │ │ User Domain │ │ Info Domain │ │ │
│ │ │ (ns: scan, │ │ (ns: auth, │ │ (ns: location, │ │ │
│ │ │ chat) │ │ my, character)│ │ image) │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ • scan-api │ │ • auth-api │ │ • location-api │ │ │
│ │ │ • chat-api │ │ • my-api │ │ • image-api │ │ │
│ │ │ │ │ • my-grpc │ │ │ │ │
│ │ │ │ │ • character-api│ │ │ │ │
│ │ │ │ │ • character-grpc│ │ │ │ │
│ │ └────────┬────────┘ └────────┬────────┘ └─────────────────┘ │ │
│ │ │ │ │ │
│ │ │ gRPC (50051) │ gRPC (50052) │ │
│ │ └───────────────────┘ │ │
│ │ scan → character → my (도메인 간 gRPC 통신) │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ Layer 0: Infrastructure │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Platform (tier=infrastructure) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ ext-authz │ │ Istio │ │ ArgoCD │ │ │
│ │ │ (ns: auth) │ │ (ns: istio- │ │ (ns: argocd)│ │ │
│ │ │ JWT 검증 │ │ system) │ │ GitOps │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ Data (tier=data) Integration (tier=integration) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ PostgreSQL │ │ Redis │ │ RabbitMQ │ │ │
│ │ │ (ns:postgres)│ │ (ns: redis) │ │ (ns:rabbitmq)│ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ Observability (tier=observability) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Prometheus │ │ Grafana │ │ EFK Stack │ │ │
│ │ │ (ns:prometh)│ │ (ns:grafana)│ │ (ns:logging)│ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
3. Gateway 패턴 구현 (Istio 기반)
Eco²는 Istio Service Mesh로 Gateway 패턴을 구현하고 있다.
┌─────────────────────────────────────────────────────────────────────────┐
│ Eco² Gateway 아키텍처 (Istio) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 클라이언트 요청 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ALB (AWS Load Balancer) │ │
│ │ • HTTPS 종료 (ACM Certificate) │ │
│ │ • api.growbin.app → Istio Ingress Gateway │ │
│ └─────────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Istio Ingress Gateway (eco2-gateway) │ │
│ │ • namespace: istio-system │ │
│ │ • 모든 외부 트래픽의 단일 진입점 │ │
│ └─────────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌───────────────────┴───────────────────┐ │
│ ▼ ▼ │
│ ┌──────────────────────┐ ┌───────────────────────┐ │
│ │ AuthorizationPolicy │ │ VirtualService │ │
│ │ (ext-authz-policy) │ │ (도메인별 라우팅) │ │
│ │ │ │ │ │
│ │ • /api/v1/* 검사 │ │ • /api/v1/auth → auth │ │
│ │ • OAuth 콜백 우회 │ │ • /api/v1/scan → scan │ │
│ │ • 헬스체크 우회 │ │ • /api/v1/user → my │ │
│ └──────────┬───────────┘ │ • /api/v1/chat → chat │ │
│ │ └───────────────────────┘ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ ext-authz │ │
│ │ (Go, gRPC) │ │
│ │ │ │
│ │ 역할: │ │
│ │ ✅ JWT 검증 (RS256) │ │
│ │ ✅ 블랙리스트 조회 │ │
│ │ ❌ 라우팅 (담당 X) │ │
│ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
실제 AuthorizationPolicy 설정:
# workloads/routing/gateway/base/authorization-policy.yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: ext-authz-policy
namespace: istio-system
spec:
selector:
matchLabels:
istio: ingressgateway
action: CUSTOM
provider:
name: eco2-ext-authz
rules:
- to:
- operation:
hosts: [api.dev.growbin.app, api.growbin.app]
paths: [/api/v1/*]
notPaths:
# OAuth 콜백, JWKS, 헬스체크는 인증 우회
- /api/v1/auth/kakao/callback
- /api/v1/auth/.well-known/jwks.json
- /api/v1/*/health
4. 도메인 간 gRPC 통신 (실제 코드)
┌─────────────────────────────────────────────────────────────────────────┐
│ 도메인 간 gRPC 통신 흐름 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ [1] scan → character (보상 평가) │
│ │
│ ┌────────────┐ gRPC (50051) ┌────────────────┐ │
│ │ scan-api │ ─────────────────────▶ │ character-grpc │ │
│ │ │ │ │ │
│ │ CharacterGrpcClient: │ CharacterServicer: │
│ │ • CircuitBreaker │ • GetCharacterReward() │
│ │ • Retry (Exp Backoff) │ │ │
│ │ • Timeout │ │ │
│ └────────────┘ └────────────────┘ │
│ │
│ [2] character → my (캐릭터 지급 동기화) │
│ │
│ ┌────────────────┐ gRPC (50052) ┌────────────┐ │
│ │ character-grpc │ ────────────────▶ │ my-grpc │ │
│ │ │ │ │ │
│ │ MyUserCharacterClient: │ UserCharacterServicer: │
│ │ • CircuitBreaker │ • GrantCharacter() │
│ │ • Retry (Exp Backoff) │ │ │
│ │ • Timeout │ │ │
│ └────────────────┘ └────────────────┘ │
│ │
│ NetworkPolicy로 격리: │
│ • allow-scan-to-character-grpc (ns: character) │
│ • allow-character-to-my-grpc (ns: my) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
gRPC 클라이언트 구현 특징 (실제 코드):
# domains/scan/core/grpc_client.py
class CharacterGrpcClient:
def __init__(self, settings: "Settings") -> None:
# Circuit Breaker로 장애 전파 방지
self._circuit_breaker = CircuitBreaker(
name="character-grpc-client",
fail_max=settings.grpc_circuit_fail_max, # 연속 실패 5회
timeout_duration=settings.grpc_circuit_timeout_duration, # 30초
)
async def _call_with_retry(self, call_func, log_ctx: dict):
"""Exponential backoff + jitter 재시도"""
for attempt in range(self.max_retries + 1):
try:
return await call_func()
except grpc.aio.AioRpcError as e:
if e.code() in RETRYABLE_STATUS_CODES:
delay = min(self.retry_base_delay * (2**attempt), self.retry_max_delay)
delay *= (0.75 + random.random() * 0.5) # jitter
await asyncio.sleep(delay)
5. NetworkPolicy 격리 (실제 설정)
# workloads/network-policies/base/allow-scan-to-character-grpc.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-scan-to-character-grpc
namespace: character
spec:
podSelector:
matchLabels:
app: character-grpc
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: scan # scan 네임스페이스만 허용
ports:
- protocol: TCP
port: 50051
6. DOMA 원칙 적용 현황 평가
| 원칙 | 현재 상태 | 구현 방식 | 개선 여지 |
|---|---|---|---|
| Domain | ✅ 적용됨 | 네임스페이스 + domain 라벨로 명시적 경계 |
- |
| Layer | ✅ 적용됨 | tier 라벨로 계층 구분, nodeSelector로 배치 격리 |
- |
| Gateway | ✅ 부분 적용 | Istio Gateway + ext-authz로 인증 통합 | 도메인별 Gateway는 없음 |
| Extension | ❌ 미적용 | Character 보상 로직이 하드코딩 | Evaluator 인터페이스로 확장 가능성 있음 |
7. 개선 로드맵 (DOMA 관점)
| P1 | gRPC → MQ 전환 | 동기 gRPC 호출 | RabbitMQ 비동기 통신 |
| P1 | ext-authz 로컬 캐시 | 매번 Redis 조회 | MQ로 캐시 동기화 |
| P2 | 도메인 Gateway | 없음 | 도메인별 진입점 통일 |
| P3 | Extension 시스템 | 보상 로직 하드코딩 | 플러그인 아키텍처 |
gRPC → MQ 전환 대상:
| 현재 통신 | 특성 | 전환 방식 |
|---|---|---|
scan → character |
Fire-and-forget 가능 | RabbitMQ Point-to-Point |
character → my |
best-effort 동기화 | RabbitMQ + at-least-once |
my → character (캐릭터 조회) |
읽기 캐싱 필요 | 로컬 캐시 + MQ 동기화 |