Q. 마이크로 서비스 아키텍쳐에 대해 예시와 함께 설명해 주세요.
마이크로서비스 아키텍처는 애플리케이션을 작고 독립 배포 가능한 서비스들로 나누어,
각 서비스가 하나의 비즈니스 기능에 집중하도록 설계하는 방식이다.
쉽게 말해, “계약, 결제, 재고, 회원처럼 작업 단위로 잘게 쪼개 각각을 따로 개발·배포·확장하는 구조예요.
독립 배포: 작은 변경을 특정 서비스만 배포하면 됨 → 출시 속도↑
확장성: 트래픽 많은 서비스만 스케일 아웃 가능(비용 최적화)
장애 격리: 한 서비스 장애가 전체를 끌어내리지 않도록 방화벽 역할
기술 자율성: 서비스별 언어/DB/프레임워크 선택 가능 (필요 시)
단, 도입비용(운영 복잡성, 분산 트랜잭션, 관측성)도 큽니다.
팀 규모·도메인 복잡도가 충분히 있을 때 이득이 커져요.
[Client]
↓ HTTP
[API Gateway] ── 인증/라우팅/레이트리밋/집계
├─→ [User Service] ─ DB_user
├─→ [Order Service] ─ DB_order
├─→ [Inventory Service] ─ DB_inventory
└─→ [Payment Service] ─ DB_payment
↑
[Message Broker] (Kafka/RabbitMQ) ← 비동기 이벤트
장점: 단순, 디버깅 용이, 요청-응답 흐름이 명확
단점: 연쇄 호출이 늘수록 지연/장애 전파 위험, 타임아웃/재시도 설계 필요
권장: 조회, 사용자 요청 즉시 응답 필요한 경우
장점: 결합도↓, 스파이크 흡수, 확장 유연
단점: 최종 일관성(Eventual Consistency), 디버깅 난이도↑
권장: 상태 변경/도메인 이벤트 전파(주문 생성, 결제 완료 등)
보통 “명령은 동기, 이벤트는 비동기”로 섞어 씁니다.
DB per Service: 스키마 공유·크로스 DB 조인은 금지(강한 권장)
분산 트랜잭션(2PC) 회피: 대신 Saga 패턴(보상 트랜잭션) 사용
Outbox 패턴: 로컬 트랜잭션과 이벤트 발행을 원자적으로 묶음
Idempotency: 중복 처리 안전(재시도 시 부작용 방지)
CDC(Change Data Capture): 데이터 변경 이벤트 스트리밍
클라이언트 → Order Service: POST /orders
Order는 재고 예약을 요청(동기)하거나 이벤트 발행(비동기)
Inventory Service: 재고 차감/예약 성공 시 이벤트 발행
Payment Service: 결제 시도 후 성공/실패 이벤트 발행
Order는 이벤트들을 모아 상태를 확정(CONFIRMED/CANCELED)
결제 실패 → 재고 예약 롤백
재고 부족 → 주문 취소
어느 단계에서 실패하든 보상 액션으로 이전 상태로 되돌림
// POST /orders
async function createOrder(cmd: CreateOrderCmd, reqId: string) {
// 1) 로컬 트랜잭션 시작
const order = await db.tx(async (tx) => {
const o = await tx.order.insert({
customerId: cmd.customerId,
status: 'PENDING',
amount: cmd.amount,
idempotencyKey: req.headers['Idempotency-Key'] ?? null
});
// 2) Outbox 에 이벤트 기록 (아직 브로커로는 안 나감)
await tx.outbox.insert({
type: 'OrderCreated',
payload: JSON.stringify({ orderId: o.id, amount: o.amount }),
headers: JSON.stringify({ 'x-correlation-id': reqId }),
status: 'READY'
});
return o;
});
// 3) 별도 워커가 Outbox에서 읽어 브로커(Kafka 등)로 발행
return { id: order.id, status: order.status };
}
broker.on('OrderCreated', async (evt) => {
const ok = await reserveStock(evt.payload.orderId);
await broker.publish(ok ? 'StockReserved' : 'StockFailed', {
orderId: evt.payload.orderId
}, { 'x-correlation-id': evt.headers['x-correlation-id'] });
});
broker.on('StockReserved', async (evt) => {
await requestPayment(evt.orderId); // 동기/gRPC or 이벤트로 위임
});
broker.on('PaymentSucceeded', async (evt) => {
await db.order.update({ id: evt.orderId }, { status: 'CONFIRMED' });
});
broker.on('PaymentFailed', async (evt) => {
// 보상: 재고 롤백 이벤트 발행
await broker.publish('ReleaseStock', { orderId: evt.orderId });
await db.order.update({ id: evt.orderId }, { status: 'CANCELED' });
});
컨테이너·Kubernetes: 서비스 수십 개도 표준화된 배포/스케일
CI/CD: 서비스별 파이프라인, 카나리/블루-그린, 피처 플래그
회복탄력성: 타임아웃, 재시도(지수 백오프+Jitter), Circuit Breaker, Bulkhead
관측성:
로그: 구조적 JSON + 샘플링/집중 로깅
메트릭: RED(요청율/오류율/지연), USE(자원) 대시보드
트레이싱: OpenTelemetry, 상관관계 ID 필수
보안: 게이트웨이 인증(OIDC/JWT), 서비스 간 mTLS, 시크릿 관리
구성/발견: 서비스 디스커버리, central config(스프링 클라우드/Consul 등)
계약 테스트(Consumer-Driven Contract, Pact 등): 인터페이스 신뢰성 확보
통합 테스트: 리포지토리/메시지 흐름 검증(테스트 컨테이너)
엔드투엔드(E2E): 핵심 유스케이스 최소 시나리오
서비스 가상화: 의존 서비스 모킹으로 독립 개발/테스트
Schema 진화: JSON/Proto 스키마 하위 호환 우선, 점진적 롤아웃
장점
단점
운영 복잡성↑(네트워크/배포/관측/보안 전반),
분산 트랜잭션/최종 일관성 설계 필요,
잘못 쪼개면 “분산 모놀리식”로 더 나빠짐
Bounded Context 기준: 한 팀이 소유 가능한 크기, 데이터/용어가 내부 일관
변경 동시성: 함께 자주 바뀌는 것끼리 묶기
데이터 소유권: 한 데이터의 “진실의 원천(SoT)”은 한 서비스만 가진다
API는 의도 노출: CRUD 나열 대신 유스케이스 중심
예: Dear Carmate 같은 차량/계약 관리라면
계약, 차량, 고객, 문서/알림을 경계 후보로 보고, 이벤트(ContractCreated, CarDeleted)로 약결합을 만든다.
경계를 문장으로 정의: “X 서비스는 Y를 책임지고 Z 데이터의 SoT다.”
통신 정책 결정: “명령=동기, 이벤트=비동기” 기본으로 예외만 명시
이벤트 초안 작성: OrderCreated/StockReserved/PaymentSucceeded 처럼 과거형, 비즈니스 의미
관측성 기본값: x-correlation-id 도입, 요청/이벤트에 전파
회복 탄력 기본값: 타임아웃/재시도/서킷브레이커 라이브러리 적용
데이터 원칙: DB 공유 금지, Outbox로 이벤트 발행 원자화
계약 테스트 시작: 가장 많이 의존받는 서비스부터 계약 고정