[NB] 위클리 페이퍼: Node.js 백엔드 개발 중급[20]

권나현·2025년 8월 25일
0
post-thumbnail

Q. 마이크로 서비스 아키텍쳐에 대해 예시와 함께 설명해 주세요.

마이크로서비스 아키텍처(MSA)란?

  • 마이크로서비스 아키텍처는 애플리케이션을 작고 독립 배포 가능한 서비스들로 나누어,
    각 서비스가 하나의 비즈니스 기능에 집중하도록 설계하는 방식이다.

  • 쉽게 말해, “계약, 결제, 재고, 회원처럼 작업 단위로 잘게 쪼개 각각을 따로 개발·배포·확장하는 구조예요.

활용

  • 독립 배포: 작은 변경을 특정 서비스만 배포하면 됨 → 출시 속도↑

  • 확장성: 트래픽 많은 서비스만 스케일 아웃 가능(비용 최적화)

  • 장애 격리: 한 서비스 장애가 전체를 끌어내리지 않도록 방화벽 역할

  • 기술 자율성: 서비스별 언어/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)  ← 비동기 이벤트
  • API Gateway: 엔드포인트 단일화, 인증/인가, 라우팅, 응답 집계
  • Service per Bounded Context: 도메인별로 팀/코드/DB를 쪼갬
  • Database per Service: 각 서비스는 자신의 데이터만 소유(강한 원칙)
  • Message Broker: 서비스 간 비동기 통신, 이벤트 발행/구독
  • Service Discovery/Config: 동적 주소(오토스케일)와 설정 관리
  • Observability: 로그/메트릭/트레이싱(필수)

통신 방식: 동기 vs 비동기

동기(REST/gRPC)

  • 장점: 단순, 디버깅 용이, 요청-응답 흐름이 명확

  • 단점: 연쇄 호출이 늘수록 지연/장애 전파 위험, 타임아웃/재시도 설계 필요

  • 권장: 조회, 사용자 요청 즉시 응답 필요한 경우

비동기(이벤트/메시지)

  • 장점: 결합도↓, 스파이크 흡수, 확장 유연

  • 단점: 최종 일관성(Eventual Consistency), 디버깅 난이도↑

  • 권장: 상태 변경/도메인 이벤트 전파(주문 생성, 결제 완료 등)

  • 보통 “명령은 동기, 이벤트는 비동기”로 섞어 씁니다.

데이터와 트랜잭션: 핵심 패턴

  • DB per Service: 스키마 공유·크로스 DB 조인은 금지(강한 권장)

  • 분산 트랜잭션(2PC) 회피: 대신 Saga 패턴(보상 트랜잭션) 사용

  • Outbox 패턴: 로컬 트랜잭션과 이벤트 발행을 원자적으로 묶음

  • Idempotency: 중복 처리 안전(재시도 시 부작용 방지)

  • CDC(Change Data Capture): 데이터 변경 이벤트 스트리밍

예시 시나리오: 전자상거래 주문(주문/재고/결제)

1) 흐름(간단 시퀀스)

  • 클라이언트 → Order Service: POST /orders

  • Order는 재고 예약을 요청(동기)하거나 이벤트 발행(비동기)

  • Inventory Service: 재고 차감/예약 성공 시 이벤트 발행

  • Payment Service: 결제 시도 후 성공/실패 이벤트 발행

  • Order는 이벤트들을 모아 상태를 확정(CONFIRMED/CANCELED)

2) Saga(보상 트랜잭션) 요약

  • 결제 실패 → 재고 예약 롤백

  • 재고 부족 → 주문 취소

  • 어느 단계에서 실패하든 보상 액션으로 이전 상태로 되돌림

간단 코드/이벤트 예시 (TypeScript 의사코드)

Order 서비스: Outbox + 이벤트 발행

// 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 };
}

Inventory 서비스: 이벤트 소비 → 재고 예약 → 결과 이벤트 발행

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'] });
});

Order 서비스: 결과 이벤트 수신 → 상태 전이

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' });
});

공통: 상관관계 ID(Trace 연동)

  • 모든 요청/이벤트에 x-correlation-id를 전파 → 분산 추적 가능

운영 관점(DevOps/플랫폼)

  • 컨테이너·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 스키마 하위 호환 우선, 점진적 롤아웃

장단점

  • 장점

    • 독립 배포/확장, 장애 격리, 팀 자율성, 도메인 정렬(Conway’s Law)
  • 단점

    • 운영 복잡성↑(네트워크/배포/관측/보안 전반),

    • 분산 트랜잭션/최종 일관성 설계 필요,

    • 잘못 쪼개면 “분산 모놀리식”로 더 나빠짐

도메인 기준으로 자르기(경계 설정 힌트)

  • Bounded Context 기준: 한 팀이 소유 가능한 크기, 데이터/용어가 내부 일관

  • 변경 동시성: 함께 자주 바뀌는 것끼리 묶기

  • 데이터 소유권: 한 데이터의 “진실의 원천(SoT)”은 한 서비스만 가진다

  • API는 의도 노출: CRUD 나열 대신 유스케이스 중심

  • 예: Dear Carmate 같은 차량/계약 관리라면
    계약, 차량, 고객, 문서/알림을 경계 후보로 보고, 이벤트(ContractCreated, CarDeleted)로 약결합을 만든다.

도입 체크리스트(오늘 TIL 액션)

  1. 경계를 문장으로 정의: “X 서비스는 Y를 책임지고 Z 데이터의 SoT다.”

  2. 통신 정책 결정: “명령=동기, 이벤트=비동기” 기본으로 예외만 명시

  3. 이벤트 초안 작성: OrderCreated/StockReserved/PaymentSucceeded 처럼 과거형, 비즈니스 의미

  4. 관측성 기본값: x-correlation-id 도입, 요청/이벤트에 전파

  5. 회복 탄력 기본값: 타임아웃/재시도/서킷브레이커 라이브러리 적용

  6. 데이터 원칙: DB 공유 금지, Outbox로 이벤트 발행 원자화

  7. 계약 테스트 시작: 가장 많이 의존받는 서비스부터 계약 고정

결론

  • 마이크로서비스는 “작게 나눠 빠르게 변화”하기 위한 전략이고, 대신 분산 시스템의 복잡성을
    설계·운영 원칙으로 다뤄야 합니다.
profile
node.js 백앤드 개발자가 되기 위한 Study Vlog

0개의 댓글