MCP 아키텍처 설계기 — 글로벌 인프라의 뼈대를 세우다
2025년 5월, REINDEERS 플랫폼의 인프라 재설계가 본격적으로 시작되었다. 4월 한 달 동안 우리는 기존 시스템을 전면 점검했고, AWS 싱가폴 리전에 산재된 비효율적인 구조를 확인했다. CI/CD는 단순 스크립트 수준에 머물렀고, 운영 스테이지도 명확히 구분되지 않았다. 소스 관리조차 통합되지 않아 빌드가 환경마다 달랐다. 결국 우리는 아키텍처를 완전히 다시 세워야 했다.
MCP가 무엇인가 — 아키텍처적 정의
MCP는 Multi-Commerce Platform의 약자다. 단일 서비스가 아니라 5개의 독립 플랫폼이 하나의 메시지 브로커를 중심으로 연결된 구조를 가리킨다. 각 플랫폼은 고유한 도메인을 담당하면서도, 무역 거래의 전체 흐름 안에서 하나의 시스템처럼 동작해야 한다.
5개 플랫폼이 독립적으로 동작하면서도 거래 데이터를 일관되게 공유해야 하는 요구사항은 기존의 모놀리식 구조로는 해결할 수 없었다. 견적 플랫폼에서 생성된 PQ(Price Quotation)가 공급사 플랫폼의 QA(Quote Answer)와 연결되고, 이것이 다시 QB(Quote Back) - PO(Purchase Order) - DO(Delivery Order) - FWD(Forwarding) - Pay - Settlement으로 이어지는 무역 거래의 흐름은 각 플랫폼 간의 정교한 이벤트 전달 없이는 성립하지 않는다.
왜 이벤트 드리븐인가 — Request-Response를 버린 이유
초기 설계에서 플랫폼 간 통신을 REST API 기반 동기 호출로 구현하는 안을 검토했다. 거래 플랫폼이 공급사 플랫폼의 API를 직접 호출하고, 응답을 받아 다음 단계로 진행하는 방식이다. 이 구조의 문제는 무역 거래의 특성에서 드러난다.
PO가 확정되면 동시에 여러 일이 발생해야 한다. 공급사에 생산 지시가 전달되고, 포워딩 플랫폼에서 물류 견적 준비가 시작되며, 결제 플랫폼에서 선수금 요청이 생성되고, 거래 로그가 기록되어야 한다. 동기 호출 구조에서는 이 중 하나라도 실패하면 전체 흐름이 멈춘다. 타임아웃, 재시도, 부분 실패 처리까지 고려하면 서비스 간 결합도가 급격히 높아진다.
이벤트 드리븐 아키텍처에서는 PO 확정이라는 하나의 이벤트가 MQ에 발행되면, 각 플랫폼이 독립적으로 이를 소비한다. 공급사 플랫폼이 일시적으로 다운되더라도 메시지는 큐에 남아있고, 복구 후 처리된다. 이 구조가 4개국에 걸친 멀티 리전 환경에서 안정성을 보장하는 유일한 방법이었다.
MQ 토폴로지 설계 — LavinMQ 기반 이벤트 라우팅
메시지 브로커로 LavinMQ를 선택했다. AMQP 0-9-1 프로토콜을 완전 지원하면서도 RabbitMQ 대비 메모리 사용량이 현저히 낮다. 4개 리전에 걸쳐 브로커 클러스터를 운영해야 하는 상황에서 리소스 효율성은 중요한 선택 기준이었다.
MQ 토폴로지는 세 가지 Exchange 타입을 조합하여 구성했다.
| Exchange | Type | 용도 |
|---|---|---|
| trade.events | topic | 거래 이벤트 라우팅 (PO, DO, FWD 등) |
| platform.direct | direct | 플랫폼 간 1:1 명령 전달 |
| notification.fanout | fanout | 전체 구독 알림 (시스템 공지, 긴급 알림) |
거래 이벤트의 라우팅 키는 trade.{country}.{event_type}.{entity} 패턴을 따른다. 예를 들어 태국에서 PO가 확정되면 trade.th.confirmed.po가 발행된다. 공급사 플랫폼은 trade.*.confirmed.po를 바인딩하여 전체 국가의 PO 확정을 수신하고, 포워딩 플랫폼은 trade.th.confirmed.*를 바인딩하여 태국 거래의 모든 확정 이벤트를 수신하는 식이다.
이벤트 스키마는 세 가지 카테고리로 분류된다.
{
"event_id": "evt_20250505_a1b2c3",
"event_type": "trade.th.confirmed.po",
"category": "event",
"timestamp": "2025-05-05T09:30:00Z",
"source": "platform-trade",
"payload": {
"po_id": "PO-2025-TH-00142",
"buyer_id": "BUY-TH-0031",
"vendor_id": "VEN-CN-0088",
"total_amount": 45000.00,
"currency": "USD"
},
"metadata": {
"region": "ap-bangkok",
"correlation_id": "txn_20250505_x9y8z7",
"schema_version": "2.1"
}
}
category 필드는 세 가지 값을 가진다. Event는 이미 발생한 사실을 기록하고(PO 확정, DO 생성), State는 현재 엔티티 상태의 스냅샷이며(PO 상태 변경), Log는 감사 추적용 불변 기록이다(누가 언제 무엇을 변경했는지). 이 세 카테고리의 조합으로 거래의 전체 히스토리를 재구성할 수 있다.
멀티 리전 설계 — 4개국을 하나로 연결하는 방법
REINDEERS는 태국, 한국, 중국, 말레이시아 4개국에서 운영된다. 각 국가의 사용자는 물리적으로 가까운 서버에 접속해야 하지만, 거래 데이터는 국경을 넘어 일관성 있게 공유되어야 한다. 태국 바이어의 PO가 중국 공급사에게 실시간으로 전달되어야 하고, 한국 포워더가 이를 기반으로 물류 견적을 생성해야 한다.
새로운 MCP 구조는 홍콩을 메인 리전, 서울을 DR 리전으로 설정하고, 각 지역의 접속은 DNSPod의 Geo Routing을 이용해 가장 가까운 리전으로 유도하도록 설계했다.
| 리전 | 역할 | 대상 국가 | 주요 구성 |
|---|---|---|---|
| ap-hongkong | Primary | 중국, 말레이시아 | API, DB Master, MQ Primary |
| ap-seoul | DR + Read Replica | 한국 | API, DB Slave, MQ Secondary |
| ap-bangkok | Edge + Cache | 태국 | CDN Edge, Redis Cache, MQ Consumer |
| ap-shanghai | Mainland Optimized | 중국 본토 | CDN Node, API Proxy |
MQ 클러스터는 홍콩에 Primary, 서울에 Secondary를 두고 Shovel 플러그인으로 메시지를 동기화한다. 방콕과 상하이의 Consumer는 홍콩 Primary에 직접 연결하되, 장애 시 서울 Secondary로 자동 전환(failover)된다. 거래 이벤트의 순서 보장이 중요하므로, 각 거래(correlation_id 기준)는 단일 큐에서만 소비되도록 Consistent Hashing을 적용했다.
데이터 복제 전략
데이터베이스는 PostgreSQL을 사용한다. 서울-홍콩 간에는 논리적 복제(Logical Replication)를 구성했다. Master는 홍콩(ap-hongkong)에 위치하며, 서울(ap-seoul)에 Standby를 두어 장애 발생 시 즉시 페일오버가 가능하다. 복제 지연은 평균 150ms 이내로 유지된다.
모든 쓰기 연산은 홍콩 Master로 라우팅되고, 읽기 연산은 각 리전의 가장 가까운 Replica에서 처리된다. 방콕과 상하이에는 Redis 캐시 레이어를 두어 자주 조회되는 데이터(환율, 상품 카탈로그, 운송 스케줄)의 DB 부하를 줄인다. 캐시 무효화는 MQ 이벤트를 통해 전파된다. 예를 들어 환율이 갱신되면 system.rate.updated.exchange 이벤트가 발행되고, 각 리전의 Redis가 해당 키를 삭제한다.
데이터베이스 설계 원칙 — 멀티 테넌트, 멀티 통화, 멀티 언어
4,300개 이상의 파트너사가 사용하는 플랫폼에서 데이터 격리는 필수다. Row-Level Security(RLS)를 적용하여 각 파트너사는 자신의 데이터만 조회할 수 있다. 모든 거래 테이블에는 tenant_id 컬럼이 존재하며, API 레이어에서 JWT 토큰의 tenant claim을 기반으로 자동 필터링된다.
금액 관련 컬럼은 항상 원래 통화의 금액과 통화 코드를 함께 저장한다.
-- 거래 금액 저장 패턴
CREATE TABLE trade_line_items (
id BIGSERIAL PRIMARY KEY,
po_id BIGINT NOT NULL REFERENCES purchase_orders(id),
tenant_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INTEGER NOT NULL,
unit_price NUMERIC(18,4) NOT NULL,
currency VARCHAR(3) NOT NULL, -- THB, KRW, CNY, USD, MYR
unit_price_usd NUMERIC(18,4), -- 정산용 USD 환산
exchange_rate NUMERIC(12,6), -- 환산 시점 환율
rate_date DATE, -- 환율 기준일
created_at TIMESTAMPTZ DEFAULT NOW()
);
다국어 처리는 별도의 번역 테이블 패턴을 사용한다. 상품명, 카테고리명, 인증서 종류 등 번역이 필요한 모든 텍스트 필드는 원본 테이블에 기본 언어(영어)를 저장하고, translations 테이블에서 locale별 번역을 관리한다. 태국어, 한국어, 중국어(간체), 말레이어 4개 언어를 지원하며, API 응답 시 Accept-Language 헤더에 따라 적절한 번역이 반환된다.
API 설계 — 버전 관리와 인증
API는 RESTful 원칙을 따르되, URL 경로에 버전을 포함한다.
# API 엔드포인트 패턴
GET /api/v2/trade/po/{po_id}
POST /api/v2/trade/po
PATCH /api/v2/trade/po/{po_id}/state
GET /api/v2/trade/po/{po_id}/events
# 목록 조회 — 페이지네이션 + 필터
GET /api/v2/trade/po?page=1&limit=20&status=confirmed&country=th
# 파트너사용 API — 별도 인증 체계
GET /api/partner/v1/orders?since=2025-05-01
인증은 이중 구조다. 플랫폼 내부 사용자(운영팀, 매니저)는 JWT 토큰을 사용하고, 외부 파트너사 연동은 API Key + HMAC 서명 방식을 사용한다. JWT에는 tenant_id, role, allowed_countries 클레임이 포함되어, 각 요청의 데이터 접근 범위가 토큰 레벨에서 결정된다. 파트너사 API Key는 요청 본문과 타임스탬프를 조합하여 HMAC-SHA256으로 서명하며, replay attack 방지를 위해 5분 이내의 타임스탬프만 허용한다.
CI/CD — Drone 기반 파이프라인
CI/CD는 Drone을 도입했다. 단순히 스크립트를 실행하는 수준이 아닌, YAML 기반의 self-service CI로 바꿔서 모든 빌드/배포 파이프라인을 코드로 관리한다.
kind: pipeline
type: docker
name: frontend-build
steps:
- name: install
image: node:20
commands:
- npm ci
- name: build
image: node:20
commands:
- npm run build
- name: deploy
image: tencentcloudtools/cli
environment:
COS_BUCKET: reindeers-front-hk
commands:
- tccli cos cp ./dist cos://${COS_BUCKET}/ --recursive
이 Drone 설정은 GitHub Action보다 훨씬 단순하면서도 빠르다. 서버 자원은 모두 CVM Auto-Scaling 그룹으로 구성되어, 트래픽 변화에 따라 자동으로 인스턴스를 확장한다. API 서버는 TKE(Kubernetes Engine) 위에서 운영되며, Ingress Controller가 CLB(Cloud Load Balancer)와 연결되어 헬스체크를 통과한 Pod만 외부에 노출된다.
CDN 전략 — COS + CDN으로 정적 자산 서빙
프런트엔드는 Vue3 + Nuxt로 새로 작성했다. React + Java로 되어 있던 기존 구조는 유지보수가 어렵고, CSR과 SSR이 혼재되어 SEO와 초기 로딩 모두 불안정했다. Nuxt로 전환하면서 코드 스플리팅, SSR 캐싱, 라우팅 구조가 개선되었고, 정적 빌드 파일은 COS에 저장되어 CDN 엣지에서 서빙된다.
CDN 구조는 다음과 같다.
- ap-seoul: origin
- ap-hongkong: backup + edge
- ap-bangkok: local cache
- ap-shanghai: mainland optimized node
COS 업로드 이벤트는 SCF(Serverless Cloud Function)로 트리거되어 모든 리전의 COS 버킷을 자동 동기화한다. Python SDK로 작성된 이 함수는 4개의 리전에 동시에 CopyObject 명령을 수행하며, Cloud Log Service(CLS)에 로그가 남는다.
from tencentcloud.cos import CosS3Client
def sync_handler(event, context):
client = CosS3Client(...)
for region in ["ap-hongkong", "ap-seoul", "ap-shanghai", "ap-bangkok"]:
client.copy_object(
Bucket=f"reindeers-front-{region}",
CopySource="ap-hongkong/reindeers-front-hk/index.html",
Key="index.html"
)
왜 처음부터 만들어야 했는가
기존의 무역 플랫폼 프레임워크를 검토하지 않은 것은 아니다. Trademo, Alibaba의 OneTouch, 그 외 여러 B2B 거래 SaaS를 분석했다. 문제는 이들 모두가 단일 국가, 단일 통화, 단일 언어를 전제로 설계되어 있었다는 점이다. REINDEERS가 다루는 현실은 다르다.
태국 바이어가 중국 공급사에게 THB 기준 견적을 요청하고, 공급사는 CNY로 응답하며, 포워더는 USD로 물류비를 청구하고, 최종 정산은 KRW로 이루어지는 하나의 거래에서 4개 통화가 교차한다. 여기에 각국의 인증 요건(TISI, FDA, KC 마크 등), 관세율, 수출입 규제까지 거래 조건에 반영되어야 한다. 이러한 복합 요건을 지원하는 기성 프레임워크는 존재하지 않았다.
결국 무역 도메인의 복잡성을 정확히 표현할 수 있는 자체 데이터 모델과, 그 위에서 동작하는 이벤트 기반 플랫폼 아키텍처를 직접 설계하는 것이 가장 현실적인 선택이었다.
보안과 형상 관리
보안은 CAM(Cloud Access Management)과 KMS(Key Management Service)를 통해 강화했다. API 키는 각 서비스 계정별로 최소 권한 원칙에 따라 분리되며, GitHub Secrets에 저장되는 값은 Drone 빌드 시 임시 환경 변수로만 사용된다. IAM 사용자 정책은 JSON 기반으로 정의되어 코드로 버전 관리된다.
실제 시스템을 설계하면서 가장 신경 쓴 부분은 "운영 자동화와 지역 확장성"이었다. 이 MCP 구조는 2026년 인도차이나 리전으로 확장될 것을 전제로 설계되었으며, 모든 구성 요소가 코드 기반으로 재현 가능하도록 Terraform과 YAML로 문서화되었다. 시스템 엔지니어링의 기준은 단순했다.
"한 명이 만들어도, 열 명이 유지보수할 수 있어야 한다."
이 원칙 아래, 우리는 모든 서버 설정, 배포 파이프라인, DB 복제 구성까지 형상 관리에 포함시켰다. 이제 한 줄의 코드 변경이 전 세계의 CDN 엣지까지 반영되는 데 걸리는 시간은 90초를 넘지 않는다.
이것이 REINDEERS의 새로운 글로벌 인프라, 'MCP'의 첫 번째 완성본이다.