Skip to content
Published on

마이크로서비스 아키텍처 2025 완전 가이드: 모놀리스에서 MSA로, 그리고 다시 모듈러 모놀리스로

Authors

들어가며

2024년 Amazon Prime Video 팀이 마이크로서비스를 모놀리스로 되돌려 비용을 90% 절감했다는 발표는 업계에 큰 충격을 줬습니다. DHH(Ruby on Rails 창시자)는 "마이크로서비스는 대부분의 팀에게 과잉 엔지니어링"이라고 일갈했고, Shopify는 모듈러 모놀리스로 대규모 트래픽을 성공적으로 처리하고 있습니다.

그렇다고 마이크로서비스가 죽은 것은 아닙니다. Netflix, Uber, Spotify는 여전히 수천 개의 마이크로서비스를 운영하며, 조직 규모와 도메인 복잡성에 따라 MSA가 올바른 선택인 경우는 분명히 존재합니다.

이 글에서는 아키텍처 진화의 역사부터 2025년 현재의 트렌드까지, 모놀리스/MSA/모듈러 모놀리스를 객관적으로 비교하고, 실전에서 필요한 패턴과 기술을 체계적으로 다룹니다.


1. 아키텍처 진화의 역사

1.1 모놀리스 시대 (2000년대)

모든 기능이 하나의 배포 단위에 포함된 전통적인 아키텍처입니다.

┌─────────────────────────────────────┐
Monolith Application│  ┌─────────┐ ┌─────────┐ ┌───────┐ │
│  │  User    │ │  Order  │ │Payment│ │
│  │  Module  │ │  Module │ │Module │ │
│  └────┬─────┘ └────┬────┘ └──┬────┘ │
│       └──────┬─────┴─────────┘      │
│         ┌────▼────┐                 │
│         │  Shared │                 │
│         │   DB    │                 │
│         └─────────┘                 │
└─────────────────────────────────────┘

장점: 단순한 개발/배포, 트랜잭션 관리 용이, 디버깅 편리

단점: 코드베이스 비대화, 배포 병목, 기술 스택 고정, 확장성 한계

1.2 SOA 시대 (2005~2015)

Service-Oriented Architecture는 엔터프라이즈 서비스 버스(ESB)를 중심으로 서비스를 연결했습니다.

┌────────┐    ┌────────┐    ┌────────┐
│Service A│    │Service B│    │Service C└───┬─────┘    └───┬─────┘    └───┬─────┘
    └──────────────┼──────────────┘
              ┌────▼────┐
ESB              (MessageBus)              └─────────┘

SOA는 서비스 재사용성과 표준화를 추구했지만, ESB가 단일 장애점이 되고, SOAP/WSDL의 복잡성이 문제였습니다.

1.3 마이크로서비스 시대 (2014~현재)

Martin Fowler와 James Lewis가 정의한 마이크로서비스는 SOA의 진화형으로, 각 서비스가 독립적으로 배포/확장됩니다.

┌─────────┐   ┌─────────┐   ┌─────────┐
User    │   │ Order   │   │ PaymentService │   │ Service │   │ Service  :8080  │   │  :8081  │   │  :8082└──┬──────┘   └──┬──────┘   └──┬──────┘
REST/gRPC  │   Event   ▼             ▼            ▼
┌──────┐    ┌──────┐    ┌──────┐
│UserDB│    │OrderDB│   │PayDB │
└──────┘    └──────┘    └──────┘

1.4 2025년: 모듈러 모놀리스의 재부상

Timeline:
2000 ──── 2005 ──── 2014 ──── 2020 ──── 2025
 │         │         │         │         │
 ▼         ▼         ▼         ▼         ▼
Monolith  SOA    Microservices  서비스    Modular
                               메시 확산  Monolith
                                          재부상

2. MSA가 과잉인 경우: 현실적인 교훈

2.1 Amazon Prime Video 사례

2023년 Amazon Prime Video 팀은 오디오/비디오 모니터링 서비스를 마이크로서비스에서 모놀리스로 전환하여 인프라 비용을 90% 절감했습니다.

문제점:

  • AWS Step Functions의 상태 전환 비용이 폭발적으로 증가
  • 서비스 간 데이터 전달을 위한 S3 중간 저장 비용
  • 마이크로서비스 오케스트레이션 오버헤드

해결:

  • 단일 프로세스 내에서 모든 처리 단계를 실행
  • 네트워크 호출 대신 메모리 내 통신
  • S3 중간 저장 제거

2.2 DHH의 비판

Ruby on Rails 창시자 DHH는 "대부분의 팀은 모놀리스로 충분하다"라고 주장합니다.

주장근거
분산 시스템 복잡성 과소평가네트워크 장애, 데이터 정합성, 디버깅 난이도
팀 규모와 불일치5~10명 팀이 20개 서비스 운영은 비효율
운영 비용 폭발인프라, 모니터링, 배포 파이프라인 비용
조기 분리의 위험도메인 이해 부족 시 잘못된 경계 설정

2.3 Shopify의 모듈러 모놀리스 성공

Shopify는 연간 수십조 원의 거래를 처리하면서도 모듈러 모놀리스 아키텍처를 사용합니다.

# Shopify의 Packwerk를 이용한 모듈 경계 정의
# packages/checkout/package.yml
enforce_dependencies: true
enforce_privacy: true
dependencies:
  - packages/inventory
  - packages/payment

3. 아키텍처 비교: 모놀리스 vs MSA vs 모듈러 모놀리스

3.1 비교 매트릭스

항목모놀리스모듈러 모놀리스마이크로서비스
배포 단위1개1개 (모듈별 빌드)N개 (서비스별)
팀 규모1~20명5~50명20~수백명
통신메서드 호출모듈 인터페이스네트워크(REST/gRPC)
데이터 저장소공유 DB모듈별 스키마서비스별 DB
트랜잭션ACIDACID (모듈 간 제한)Saga/보상
확장성수직수직 + 부분 수평수평 (서비스별)
운영 복잡성낮음중간높음
기술 다양성단일 스택단일 스택폴리글랏
장애 격리없음부분적완전 격리
초기 비용낮음중간높음

3.2 모듈러 모놀리스 아키텍처 상세

┌─────────────────────────────────────────────┐
Modular Monolith│                                             │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐    │
│  │  User   │  │  Order  │  │ Payment │    │
│  │ Module  │  │ Module  │  │ Module  │    │
│  │         │  │         │  │         │    │
│  │ Public  │  │ Public  │  │ Public  │    │
│  │   API   │◄─►   API   │◄─►   API   │    │
│  │         │  │         │  │         │    │
│  │ Private │  │ Private │  │ Private │    │
│  │  Impl   │  │  Impl   │  │  Impl   │    │
│  └──┬──────┘  └──┬──────┘  └──┬──────┘    │
│     │            │            │            │
│  ┌──▼──┐     ┌──▼──┐     ┌──▼──┐         │
│  │user │     │order│     │pay  │         │
│  │schema│    │schema│    │schema│         │
│  └─────┘     └─────┘     └─────┘         │
│     └────────────┼────────────┘            │
│           ┌──────▼──────┐                  │
│           │  Shared DB  │                  │
 (분리 스키마)│                  │
│           └─────────────┘                  │
└─────────────────────────────────────────────┘
// Java 모듈러 모놀리스 예시 (Spring Modulith)
@ApplicationModule(
    allowedDependencies = {"order", "shared"}
)
package com.example.payment;

// 모듈 간 통신은 이벤트로
@Service
public class PaymentService {

    private final ApplicationEventPublisher events;

    @Transactional
    public PaymentResult processPayment(PaymentRequest request) {
        Payment payment = Payment.create(request);
        paymentRepository.save(payment);

        // 모듈 간 이벤트 발행 (직접 의존 없음)
        events.publishEvent(new PaymentCompletedEvent(
            payment.getId(),
            payment.getOrderId(),
            payment.getAmount()
        ));

        return PaymentResult.success(payment);
    }
}

3.3 선택 기준 플로차트

시작: 새 프로젝트
팀 규모가 50명 이상인가? ──Yes──▶ 도메인이 명확히 분리되어 있는가?
  │                                    │
  No                                  Yes ──▶ MSA 고려
  │                                    │
No ──▶ 모듈러 모놀리스
독립적 확장이 필요한 서비스가 있는가?
  Yes ──▶ 해당 서비스만 분리 (하이브리드)
  No ──▶ 모놀리스 또는 모듈러 모놀리스

4. DDD와 서비스 분해

4.1 Bounded Context 식별

Domain-Driven Design에서 Bounded Context는 마이크로서비스의 자연스러운 경계입니다.

┌─────────────── E-Commerce Domain ───────────────┐
│                                                  │
│  ┌──────────────┐    ┌──────────────┐           │
│  │  Catalog     │    │  Order       │           │
│  │  Context     │    │  Context     │           │
│  │              │    │              │           │
│  │ - Product    │    │ - Order      │           │
│  │ - Category   │    │ - OrderItem  │           │
│  │ - Price      │    │ - Shipment   │           │
│  └──────────────┘    └──────────────┘           │
│                                                  │
│  ┌──────────────┐    ┌──────────────┐           │
│  │  Identity    │    │  Payment     │           │
│  │  Context     │    │  Context     │           │
│  │              │    │              │           │
│  │ - User       │    │ - Payment    │           │
│  │ - Role       │    │ - Refund     │           │
│  │ - Permission │    │ - Invoice    │           │
│  └──────────────┘    └──────────────┘           │
└──────────────────────────────────────────────────┘

4.2 Context Mapping 패턴

┌────────────┐                    ┌────────────┐
Upstream  │                    │ DownstreamContext   │ ──Conformist────▶  │  Context│            │                    │            │
  (Order)   │ ──ACL──────────▶   (Payment)│            │                    │            │
│            │ ──OHS/PL───────▶   (Shipping)└────────────┘                    └────────────┘

ACL: Anti-Corruption Layer (번역 계층)
OHS: Open Host Service (공개 API)
PL:  Published Language (공유 스키마)

4.3 서비스 분해 전략

// 1. Aggregate 기준 분해
// Order Aggregate Root
public class Order {
    private OrderId id;
    private CustomerId customerId;
    private List<OrderLine> lines;
    private OrderStatus status;
    private Money totalAmount;

    // Aggregate 내부의 비즈니스 로직
    public void addItem(ProductId productId, int quantity, Money price) {
        OrderLine line = new OrderLine(productId, quantity, price);
        this.lines.add(line);
        this.totalAmount = calculateTotal();
    }

    public void confirm() {
        if (this.lines.isEmpty()) {
            throw new OrderException("빈 주문은 확정할 수 없습니다");
        }
        this.status = OrderStatus.CONFIRMED;
    }
}

4.4 분해 시 피해야 할 실수

  1. 데이터 기준 분해: 테이블 단위로 서비스를 나누면 과도한 통신 발생
  2. 기술 기준 분해: 프론트엔드/백엔드/DB 레이어로 분리하면 MSA의 이점 없음
  3. 조기 분해: 도메인 이해가 부족할 때 분리하면 잘못된 경계 설정
  4. 너무 세분화: 나노서비스(Nanoservice)는 운영 부담만 증가

5. 통신 패턴: REST vs gRPC vs 이벤트 기반

5.1 동기 통신 비교

항목REST (HTTP/JSON)gRPC (HTTP/2 + Protobuf)
직렬화JSON (텍스트)Protocol Buffers (바이너리)
성능상대적 느림2~10배 빠름
스트리밍제한적 (SSE/WebSocket)네이티브 양방향 스트리밍
코드 생성OpenAPI (선택적)필수 (proto 파일)
브라우저 지원네이티브gRPC-Web 필요
가독성높음 (JSON)낮음 (바이너리)
사용 케이스외부 API, 단순 CRUD서비스 간 내부 통신

5.2 gRPC 서비스 정의

// order_service.proto
syntax = "proto3";

package order.v1;

service OrderService {
  // 단항 RPC
  rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);

  // 서버 스트리밍 - 주문 상태 업데이트
  rpc WatchOrderStatus (WatchRequest) returns (stream OrderStatusUpdate);

  // 클라이언트 스트리밍 - 대량 주문 등록
  rpc BulkCreateOrders (stream CreateOrderRequest) returns (BulkCreateResponse);

  // 양방향 스트리밍 - 실시간 주문 처리
  rpc ProcessOrders (stream OrderAction) returns (stream OrderResult);
}

message CreateOrderRequest {
  string customer_id = 1;
  repeated OrderItem items = 2;
  ShippingAddress shipping = 3;
}

message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
  int64 price_cents = 3;
}

message CreateOrderResponse {
  string order_id = 1;
  OrderStatus status = 2;
  int64 total_cents = 3;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING = 1;
  ORDER_STATUS_CONFIRMED = 2;
  ORDER_STATUS_SHIPPED = 3;
  ORDER_STATUS_DELIVERED = 4;
}

5.3 이벤트 기반 비동기 통신

Producer                         Consumer
┌─────────┐    ┌─────────┐    ┌─────────┐
Order   │───▶│  Kafka  │───▶│ PaymentService │    │ Topic:  │    │ Service│         │    │ orders  │    │         │
└─────────┘    └─────────┘    └─────────┘
                    ├────────▶ ┌─────────┐
                    │          │Inventory│
                    │          │ Service                    │          └─────────┘
                    └────────▶ ┌─────────┐
                               │Notific-                               │ation    │
Service                               └─────────┘
// Kafka Producer - 주문 이벤트 발행
@Service
public class OrderEventPublisher {

    private final KafkaTemplate<String, OrderEvent> kafkaTemplate;

    public void publishOrderCreated(Order order) {
        OrderCreatedEvent event = OrderCreatedEvent.builder()
            .orderId(order.getId())
            .customerId(order.getCustomerId())
            .items(order.getItems())
            .totalAmount(order.getTotalAmount())
            .timestamp(Instant.now())
            .build();

        kafkaTemplate.send("orders.created", order.getId(), event)
            .whenComplete((result, ex) -> {
                if (ex != null) {
                    log.error("이벤트 발행 실패: orderId={}",
                        order.getId(), ex);
                    // 보상 로직 또는 재시도
                }
            });
    }
}

// Kafka Consumer - 결제 처리
@Service
public class PaymentEventConsumer {

    @KafkaListener(
        topics = "orders.created",
        groupId = "payment-service",
        containerFactory = "kafkaListenerContainerFactory"
    )
    public void handleOrderCreated(OrderCreatedEvent event) {
        log.info("주문 이벤트 수신: orderId={}", event.getOrderId());
        paymentService.processPayment(event);
    }
}

5.4 통신 패턴 선택 가이드

동기 호출이 필요한가?
  Yes ──▶ 외부 클라이언트 대상인가?
  │          │
Yes ──▶ REST (OpenAPI)
  │          │
No ──▶ 고성능 필요? ──Yes──▶ gRPC
  │                    │
No ──▶ REST
  No ──▶ 순서 보장이 필요한가?
          Yes ──▶ Kafka (파티션 키로 순서 보장)
          No ──▶ 팬아웃이 필요한가?
                  Yes ──▶ SNS + SQS / Kafka 토픽
                  No ──▶ SQS / RabbitMQ

6. Service Mesh: Istio vs Linkerd

6.1 Service Mesh란?

서비스 메시는 마이크로서비스 간 통신을 인프라 레벨에서 관리하는 전용 계층입니다.

Without Service Mesh:          With Service Mesh:

┌─────────┐  ┌─────────┐     ┌─────────┐  ┌─────────┐
│Service A│──│Service B│     │Service A│  │Service B│         │  │         │     │ ┌─────┐ │  │ ┌─────┐ │
(retry,(retry,  │     │ │Proxy│─┼──┼─│Proxy│ │
│ auth,   │  │ auth,   │     │ (side│ │  │ (side│ │
│ metrics)│  │ metrics)│     │ │car) │ │  │ │car) │ │
└─────────┘  └─────────┘     │ └─────┘ │  │ └─────┘ │
                              └─────────┘  └─────────┘
   모든 서비스에                   통신 관심사를
   통신 로직 구현                  인프라로 위임

6.2 주요 기능

기능설명
트래픽 관리로드 밸런싱, 라우팅, 카나리 배포
보안mTLS 자동 적용, 인증/인가
관측성분산 추적, 메트릭 수집, 로그
복원력재시도, 서킷 브레이커, 타임아웃

6.3 Istio vs Linkerd 비교

항목IstioLinkerd
프록시Envoy (C++)linkerd2-proxy (Rust)
리소스 사용량높음 (100~200MB/pod)낮음 (20~30MB/pod)
기능 범위포괄적 (VM 지원 등)핵심 기능 집중
학습 곡선가파름완만함
mTLS수동 설정 필요기본 활성화
CNCF 등급졸업 프로젝트졸업 프로젝트
권장 규모대규모 (100+ 서비스)중소규모 (10~100 서비스)

6.4 Istio 트래픽 관리 예시

# VirtualService - 카나리 배포
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: order-service
spec:
  hosts:
    - order-service
  http:
    - match:
        - headers:
            x-canary:
              exact: "true"
      route:
        - destination:
            host: order-service
            subset: v2
    - route:
        - destination:
            host: order-service
            subset: v1
          weight: 90
        - destination:
            host: order-service
            subset: v2
          weight: 10
---
# DestinationRule - 서킷 브레이커
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: order-service
spec:
  host: order-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        h2UpgradePolicy: DEFAULT
        http1MaxPendingRequests: 100
        http2MaxRequests: 1000
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 30s
      baseEjectionTime: 30s
      maxEjectionPercent: 50
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2

6.5 Service Mesh 없이도 되는 경우

  • 서비스 수가 10개 미만
  • Kubernetes의 기본 기능으로 충분할 때
  • 팀에 서비스 메시 운영 경험이 없을 때
  • 지연 시간에 매우 민감한 시스템 (사이드카 오버헤드)

7. 분산 트랜잭션과 Saga 패턴

7.1 분산 트랜잭션의 문제

마이크로서비스에서는 각 서비스가 자체 DB를 가지므로, 여러 서비스에 걸친 ACID 트랜잭션이 불가능합니다.

주문 생성 시나리오:
1. Order Service: 주문 생성
2. Inventory Service: 재고 차감
3. Payment Service: 결제 처리
4. Notification Service: 알림 발송

만약 3단계에서 결제 실패하면?
-> 1, 2단계 롤백 필요!

7.2 Saga 패턴 - 코레오그래피 방식

┌─────────┐  OrderCreated  ┌─────────┐  StockReserved  ┌─────────┐
Order  │──────────────▶│Inventory│───────────────▶│ PaymentService │               │ Service │                │ Service└─────────┘               └─────────┘                └─────────┘
     ▲                         ▲                          │
StockReleasePaymentFailed         (보상)               │◀──────────────────────────┘
     │◀────────────────────────┘
OrderCancelled (보상)

각 서비스가 이벤트를 구독하고 자체적으로 다음 단계를 트리거합니다.

7.3 Saga 패턴 - 오케스트레이션 방식

                    ┌────────────────┐
Order SagaOrchestrator                    └───┬──┬──┬──┬──┘
           1.주문생성  │  │  │  │  4.알림
                ┌──────┘  │  │  └──────┐
                ▼         │  │         ▼
          ┌─────────┐     │  │   ┌──────────┐
Order  │     │  │   │Notificat-Service │     │  │   │ion Svc          └─────────┘     │  │   └──────────┘
              2.재고차감   │  │  3.결제
                    ┌─────┘  └─────┐
                    ▼              ▼
              ┌─────────┐   ┌─────────┐
              │Inventory│   │ PaymentService │   │ Service              └─────────┘   └─────────┘
// Saga Orchestrator 구현
@Service
public class OrderSagaOrchestrator {

    public Mono<OrderResult> createOrder(CreateOrderCommand command) {
        return Mono.just(new SagaState(command))
            // Step 1: 주문 생성
            .flatMap(state -> orderService.createOrder(state.getCommand())
                .map(order -> state.withOrder(order)))

            // Step 2: 재고 예약
            .flatMap(state -> inventoryService
                .reserveStock(state.getOrder())
                .map(reservation -> state.withReservation(reservation))
                .onErrorResume(e -> compensateOrder(state, e)))

            // Step 3: 결제 처리
            .flatMap(state -> paymentService
                .processPayment(state.getOrder())
                .map(payment -> state.withPayment(payment))
                .onErrorResume(e ->
                    compensateReservation(state, e)))

            // Step 4: 완료
            .flatMap(state -> {
                orderService.confirmOrder(
                    state.getOrder().getId());
                return Mono.just(OrderResult.success(state));
            });
    }

    private Mono<SagaState> compensateReservation(
            SagaState state, Throwable e) {
        log.warn("결제 실패, 재고 보상 시작: orderId={}",
            state.getOrder().getId());
        return inventoryService
            .releaseStock(state.getReservation())
            .then(compensateOrder(state, e));
    }

    private Mono<SagaState> compensateOrder(
            SagaState state, Throwable e) {
        log.warn("주문 보상 시작: orderId={}",
            state.getOrder().getId());
        return orderService
            .cancelOrder(state.getOrder().getId())
            .then(Mono.error(
                new SagaFailedException("Saga 실패", e)));
    }
}

7.4 코레오그래피 vs 오케스트레이션

항목코레오그래피오케스트레이션
결합도느슨함중앙 조정자 의존
복잡도서비스 수 증가 시 높음일정하게 유지
디버깅어려움 (이벤트 추적)비교적 쉬움
단일 장애점없음오케스트레이터
적합한 경우3~4단계 이하 단순 플로우5+ 단계 복잡 플로우

8. 관측성: OpenTelemetry

8.1 관측성의 3가지 축

         ┌─────────────┐
Observability│
         └──────┬──────┘
     ┌──────────┼──────────┐
     ▼          ▼          ▼
┌─────────┐ ┌──────┐ ┌─────────┐
Logs   │ │Metrics│ │ Traces(이벤트)(수치)(요청흐름)└─────────┘ └──────┘ └─────────┘
 무엇이       얼마나     어디서
 발생했는가   발생하는가  발생했는가

8.2 OpenTelemetry 통합 아키텍처

┌─────────────────────────────────────────────────────┐
Application│  ┌─────────────────────────────────────────────┐   │
│  │        OpenTelemetry SDK                    │   │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐   │   │
│  │  │  Tracer  │ │  Meter   │ │  Logger  │   │   │
│  │  └──────────┘ └──────────┘ └──────────┘   │   │
│  └────────────────────┬────────────────────────┘   │
└───────────────────────┼─────────────────────────────┘
OTLP
              ┌─────────────────┐
OTel Collector              │  ┌───────────┐  │
              │  │ Receivers │  │
              │  │Processors│  │
              │  │ Exporters │  │
              │  └───────────┘  │
              └────┬──┬──┬──────┘
                   │  │  │
          ┌────────┘  │  └────────┐
          ▼           ▼           ▼
     ┌─────────┐ ┌────────┐ ┌─────────┐
Jaeger │ │Promethe│ │  Loki      (Traces)│ │us/Mimir│  (Logs)     │         │ (Metrics│ │         │
     └─────────┘ └────────┘ └─────────┘
          │           │           │
          └─────┬─────┘───────────┘
          ┌───────────┐
Grafana          (Dashboard)          └───────────┘

8.3 분산 추적 구현

// Spring Boot + OpenTelemetry 자동 설정
// build.gradle
// implementation 'io.opentelemetry.instrumentation:
//   opentelemetry-spring-boot-starter'

// 커스텀 스팬 생성
@Service
public class OrderService {

    private final Tracer tracer;

    public Order createOrder(CreateOrderRequest request) {
        Span span = tracer.spanBuilder("order.create")
            .setAttribute("order.customer_id",
                request.getCustomerId())
            .setAttribute("order.item_count",
                request.getItems().size())
            .startSpan();

        try (Scope scope = span.makeCurrent()) {
            // 주문 생성 로직
            Order order = processOrder(request);

            span.setAttribute("order.id", order.getId());
            span.setAttribute("order.total",
                order.getTotal().doubleValue());
            span.setStatus(StatusCode.OK);

            return order;
        } catch (Exception e) {
            span.setStatus(StatusCode.ERROR, e.getMessage());
            span.recordException(e);
            throw e;
        } finally {
            span.end();
        }
    }
}

8.4 핵심 메트릭 (RED/USE)

RED Method (서비스 관점):
┌──────────────────────────────────────┐
R - Rate:     초당 요청 수           │
E - Errors:   에러율 (%)D - Duration: 응답 시간 (p50/p95/p99)└──────────────────────────────────────┘

USE Method (리소스 관점):
┌──────────────────────────────────────┐
U - Utilization: 리소스 사용률       │
S - Saturation:  대기열 길이         │
E - Errors:      리소스 에러 수      │
└──────────────────────────────────────┘

9. API Gateway 패턴

9.1 API Gateway의 역할

                    ┌─────────────────────┐
API Gateway   Client ────────▶│                     │
- 인증/인가          │
- Rate Limiting- 요청 라우팅        │
- 로드 밸런싱        │
- 응답 캐싱          │
- 요청/응답 변환     │
- Circuit Breaker- 로깅/모니터링      │
                    └──┬──────┬──────┬────┘
                       │      │      │
                       ▼      ▼      ▼
                    ┌─────┐┌─────┐┌─────┐
                    │Svc A││Svc B││Svc C                    └─────┘└─────┘└─────┘

9.2 BFF (Backend for Frontend) 패턴

┌────────┐     ┌──────────────┐
Web   │────▶│  Web BFF     │──┐
Client(GraphQL)     │  │
└────────┘     └──────────────┘  │
                                 │    ┌──────────┐
┌────────┐     ┌──────────────┐  ├───▶│ OrderMobile │────▶│ Mobile BFF   │──┤    │ ServiceApp(REST, 경량)  │  │    └──────────┘
└────────┘     └──────────────┘  │
                                 │    ┌──────────┐
┌────────┐     ┌──────────────┐  ├───▶│ UserIoT   │────▶│  IoT BFF     │──┤    │ ServiceDevice(MQTT 변환)   │  │    └──────────┘
└────────┘     └──────────────┘  │
                                 │    ┌──────────┐
                                 └───▶│ ProductService                                      └──────────┘

9.3 주요 API Gateway 비교

항목KongAWS API GatewayEnvoy GatewayAPISIX
기반Nginx + LuaAWS 관리형Envoy ProxyNginx + Lua
배포셀프호스팅/클라우드AWS 전용Kubernetes셀프호스팅
프로토콜REST, gRPC, WSREST, WS, HTTPREST, gRPCREST, gRPC, MQTT
플러그인풍부함Lambda 통합확장 가능풍부함
가격오픈소스/엔터프라이즈요청당 과금오픈소스오픈소스

10. 배포 전략

10.1 블루-그린 배포

Phase 1: Blue(현재) 운영 중
┌─────────────┐     ┌──────────┐
Load        │────▶│ Blue v1    (100% 트래픽)
Balancer (Active)└─────────────┘     └──────────┘
                    ┌──────────┐
Green v2   (대기 중)
                     (Idle)                    └──────────┘

Phase 2: Green으로 전환
┌─────────────┐     ┌──────────┐
Load        │     │ Blue v1    (대기)
Balancer (Idle)└─────────────┘     └──────────┘
       │            ┌──────────┐
       └───────────▶│ Green v2   (100% 트래픽)
                     (Active)                    └──────────┘

10.2 카나리 배포

# Kubernetes + Argo Rollouts 카나리 배포
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: order-service
spec:
  replicas: 10
  strategy:
    canary:
      steps:
        - setWeight: 5
        - pause:
            duration: 5m
        - analysis:
            templates:
              - templateName: success-rate
            args:
              - name: service-name
                value: order-service
        - setWeight: 25
        - pause:
            duration: 10m
        - analysis:
            templates:
              - templateName: success-rate
        - setWeight: 50
        - pause:
            duration: 15m
        - setWeight: 100
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: order-service:v2
          ports:
            - containerPort: 8080

10.3 배포 전략 비교

전략다운타임롤백 속도리소스 비용위험도
롤링 업데이트없음보통낮음중간
블루-그린없음즉시높음 (2배)낮음
카나리없음빠름중간낮음
A/B 테스트없음빠름중간낮음
재생성있음느림낮음높음

11. 마이그레이션: Strangler Fig 패턴

11.1 패턴 개요

교살자 무화과 패턴은 레거시 모놀리스를 점진적으로 마이크로서비스로 전환하는 전략입니다. 무화과나무가 숙주 나무를 감싸며 자라듯이, 새로운 서비스가 레거시를 점진적으로 대체합니다.

Phase 1: 프록시 배치
┌────────┐     ┌──────────┐     ┌────────────┐
Client │────▶│  Proxy   │────▶│  Monolith└────────┘     (Facade)  (모든기능)               └──────────┘     └────────────┘

Phase 2: 일부 기능 분리
┌────────┐     ┌──────────┐     ┌────────────┐
Client │────▶│  Proxy   │─┬──▶│  Monolith└────────┘     │          │ │    (기능 축소)               └──────────┘ │   └────────────┘
                            └──▶┌────────────┐
New Service│
                                 (분리된기능)                                └────────────┘

Phase 3: 대부분 기능 이관
┌────────┐     ┌──────────┐     ┌────────────┐
Client │────▶│  Proxy   │─┬──▶│  Monolith└────────┘     │          │ │    (최소 기능)               └──────────┘ │   └────────────┘
                            ├──▶┌────────────┐
                            │   │ Service A                            │   └────────────┘
                            ├──▶┌────────────┐
                            │   │ Service B                            │   └────────────┘
                            └──▶┌────────────┐
Service C                                └────────────┘

Phase 4: 모놀리스 제거
┌────────┐     ┌──────────┐
Client │────▶│  API GW  │─┬──▶ Service A
└────────┘     └──────────┘ ├──▶ Service B
                            └──▶ Service C

11.2 마이그레이션 단계

1. 분석 (2~4)
   ├── 도메인 모델링 (Event Storming)
   ├── 의존성 분석 (코드/데이터)
   └── 우선순위 결정

2. 기반 구축 (4~8)
   ├── CI/CD 파이프라인
   ├── 컨테이너/오케스트레이션 환경
   ├── 관측성 스택
   └── API Gateway 배치

3. 첫 서비스 분리 (4~6)
   ├── 가장 독립적인 도메인 선택
   ├── Anti-Corruption Layer 구현
   ├── 데이터 마이그레이션
   └── 듀얼 라이트 / 이벤트 브리지

4. 반복 분리 (서비스당 2~4)
   ├── 다음 서비스 분리
   ├── 모놀리스 코드 제거
   └── 통합 테스트

5. 모놀리스 제거
   ├── 잔여 기능 이관
   ├── 데이터 정리
   └── 인프라 해제

11.3 데이터 마이그레이션 전략

// Dual Write 패턴 - 전환 기간 동안 양쪽에 쓰기
@Service
public class UserServiceMigration {

    private final MonolithUserRepository monolithRepo;
    private final NewUserServiceClient newServiceClient;
    private final FeatureFlag featureFlag;

    public User createUser(CreateUserRequest request) {
        // 항상 모놀리스에 쓰기 (기존)
        User user = monolithRepo.save(toEntity(request));

        // 새 서비스에도 쓰기 (마이그레이션)
        try {
            newServiceClient.createUser(
                toNewServiceRequest(user));
        } catch (Exception e) {
            log.warn("새 서비스 동기화 실패, 나중에 재시도", e);
            migrationQueue.enqueue(new SyncEvent(user));
        }

        return user;
    }

    public User getUser(String userId) {
        // Feature Flag로 읽기 전환
        if (featureFlag.isEnabled("read-from-new-service")) {
            try {
                return newServiceClient.getUser(userId);
            } catch (Exception e) {
                log.warn("새 서비스 읽기 실패, 폴백", e);
                return monolithRepo.findById(userId);
            }
        }
        return monolithRepo.findById(userId);
    }
}

12. 안티패턴

12.1 대표적인 MSA 안티패턴

안티패턴설명해결책
분산 모놀리스서비스를 나눴지만 강결합 유지Bounded Context 재설정
나노서비스너무 작은 서비스 (함수 단위)서비스 병합, 관련 기능 그룹화
공유 DB여러 서비스가 같은 DB 직접 접근DB per Service 원칙
동기 체인A에서 B, C, D 순차 동기 호출이벤트 기반 비동기화
골든 해머모든 문제에 MSA 적용적합성 평가 후 아키텍처 선택
버전 지옥API 버전이 난립하위 호환성 유지, 계약 테스트

12.2 분산 모놀리스 징후

분산 모놀리스 체크리스트:
[ ] 하나의 서비스를 배포하려면 다른 서비스도 함께 배포해야 한다
[ ] 서비스 간 동기 호출이 3단계 이상 연쇄된다
[ ] 여러 서비스가 같은 데이터베이스를 직접 읽고 쓴다
[ ] 서비스 간 공유 라이브러리의 버전을 맞춰야 한다
[ ] 하나의 서비스 장애가 전체 시스템을 다운시킨다
[ ] 서비스 경계가 기술 레이어(프론트//DB)로 나뉘어 있다

3개 이상 해당되면 분산 모놀리스일 가능성이 높습니다.

12.3 동기 호출 체인 문제

문제: 동기 체인
Client -> Order -> Inventory -> Payment -> Shipping
                                           (3)
         전체 응답시간: 3+ 각 서비스 처리시간
         하나라도 실패하면 전체 실패

해결: 비동기 이벤트 + 응답은 즉시
Client -> Order (200 OK, 주문 접수됨)
              |
              +-- Event: OrderCreated
              |        |
              |        +-- Inventory (비동기 처리)
              |        +-- Payment (비동기 처리)
              |        +-- Shipping (비동기 처리)
              |
              +-- 상태 조회 API 제공

13. 의사결정 프레임워크

13.1 아키텍처 선택 매트릭스

다음 질문에 점수를 매기세요 (1~5점).

질문1점 (낮음)5점 (높음)
팀 규모5명 이하50명 이상
도메인 복잡성단순 CRUD복잡한 비즈니스 로직
독립적 확장 필요성전체 동일 부하서비스별 극도로 다른 부하
배포 빈도월 1회일 수십 회
기술 다양성 필요단일 스택서비스별 최적 기술 필요
팀 자율성중앙 집중팀별 독립 운영
운영 역량DevOps 초보성숙한 플랫폼 팀

점수 해석:

  • 7~15점: 모놀리스 (또는 모듈러 모놀리스)
  • 16~25점: 모듈러 모놀리스 (또는 하이브리드)
  • 26~35점: 마이크로서비스

13.2 기술 스택 추천

모놀리스:
├── Spring Boot + JPA
├── Django + PostgreSQL
├── Rails + PostgreSQL
└── Next.js Full Stack

모듈러 모놀리스:
├── Spring Modulith
├── .NET Aspire
├── Go (모듈 패턴)
└── Rust (Workspace)

마이크로서비스:
├── 통신: gRPC (내부) + REST (외부)
├── 메시징: Kafka / RabbitMQ
├── 오케스트레이션: Kubernetes
├── 서비스 메시: Istio / Linkerd
├── 관측성: OpenTelemetry + Grafana
├── CI/CD: ArgoCD / GitHub Actions
└── API Gateway: Kong / Envoy Gateway

14. 면접 대비 Q&A (15선)

Q1. 마이크로서비스의 핵심 특성 5가지를 설명하세요.

  1. 단일 책임: 각 서비스가 하나의 비즈니스 기능 담당
  2. 독립 배포: 다른 서비스와 무관하게 배포 가능
  3. 분산 데이터: 서비스별 자체 데이터 저장소 보유
  4. 기술 다양성: 서비스별 최적의 기술 스택 선택 가능
  5. 장애 격리: 한 서비스 장애가 전체 시스템에 전파되지 않음

Q2. CAP 정리와 마이크로서비스의 관계를 설명하세요.

CAP 정리에 따르면 분산 시스템은 일관성(Consistency), 가용성(Availability), 분할 내성(Partition Tolerance) 중 2가지만 보장할 수 있습니다. 마이크로서비스는 네트워크 분할이 불가피하므로 P를 항상 선택해야 하며, AP(가용성 우선) 또는 CP(일관성 우선) 중 선택합니다. 대부분의 MSA는 최종 일관성(Eventual Consistency)을 수용하는 AP 시스템을 선택합니다.

Q3. Saga 패턴의 두 가지 구현 방식과 차이점은?

코레오그래피: 각 서비스가 이벤트를 발행/구독하여 자율적으로 다음 단계를 수행합니다. 중앙 조정자가 없어 결합도가 낮지만, 서비스가 많아지면 이벤트 흐름 추적이 어렵습니다.

오케스트레이션: 중앙 오케스트레이터가 각 서비스를 순서대로 호출하고 보상 로직을 관리합니다. 플로우가 명확하지만 오케스트레이터가 단일 장애점이 될 수 있습니다.

Q4. Service Mesh의 Sidecar 패턴을 설명하세요.

각 서비스 Pod에 프록시 컨테이너(사이드카)를 함께 배치합니다. 서비스의 모든 인바운드/아웃바운드 트래픽이 사이드카를 통과하며, 인증(mTLS), 라우팅, 재시도, 관측성 등의 횡단 관심사를 애플리케이션 코드 수정 없이 처리합니다. Istio는 Envoy 프록시를, Linkerd는 linkerd2-proxy를 사이드카로 사용합니다.

Q5. 분산 추적(Distributed Tracing)의 원리를 설명하세요.

요청이 시스템에 진입할 때 고유한 Trace ID가 생성됩니다. 각 서비스는 이 Trace ID를 전파하며, 자신의 처리 단위를 Span으로 기록합니다. 부모-자식 관계로 연결된 Span들이 모여 하나의 Trace를 구성하고, 이를 통해 전체 요청 경로와 각 구간의 소요 시간을 시각화할 수 있습니다. W3C Trace Context 표준 헤더를 사용합니다.

Q6. Strangler Fig 패턴의 장점과 주의사항은?

장점: 점진적 전환으로 위험 최소화, 빅뱅 마이그레이션 회피, 모놀리스와 새 서비스 공존 가능, 롤백 용이

주의사항: 전환 기간 동안 두 시스템 동시 운영 비용, 데이터 동기화 복잡성, 라우팅 규칙 관리 부담, 전환 완료 시점 결정의 어려움

Q7. API Gateway와 Service Mesh의 차이는?

API Gateway: 외부 트래픽(North-South)을 관리합니다. 인증, Rate Limiting, 요청 변환, API 집약 등 에지 기능을 담당합니다.

Service Mesh: 내부 서비스 간 트래픽(East-West)을 관리합니다. mTLS, 서비스 디스커버리, 로드 밸런싱, 서킷 브레이커 등 서비스 간 통신을 담당합니다.

둘은 보완 관계이며, 대규모 MSA에서는 함께 사용합니다.

Q8. Event Sourcing과 CQRS의 관계를 설명하세요.

Event Sourcing: 상태를 직접 저장하는 대신, 상태 변경 이벤트의 시퀀스를 저장합니다. 현재 상태는 이벤트를 리플레이하여 재구성합니다.

CQRS: 명령(쓰기)과 조회(읽기) 모델을 분리합니다. 쓰기는 이벤트 스토어에, 읽기는 최적화된 뷰(Read Model)에서 처리합니다. Event Sourcing은 CQRS 없이도 사용 가능하지만, 함께 사용하면 쓰기 성능과 읽기 성능을 독립적으로 최적화할 수 있습니다.

Q9. 마이크로서비스에서 데이터 정합성을 어떻게 보장하나요?

강한 일관성 대신 최종 일관성(Eventual Consistency)을 수용합니다. 구체적 패턴으로는 Saga 패턴(분산 트랜잭션), Outbox 패턴(이벤트 발행 보장), Change Data Capture(DB 변경 감지), 멱등성 보장(중복 처리 안전)을 사용합니다. 데이터 정합성이 반드시 필요한 경우에는 해당 기능들을 같은 서비스에 배치합니다.

Q10. gRPC가 REST보다 유리한 시나리오는?

서비스 간 내부 통신에서 높은 처리량이 필요할 때, 양방향 스트리밍이 필요할 때, 강타입 계약(proto 파일)이 중요할 때, 바이너리 직렬화로 페이로드 크기를 줄여야 할 때 유리합니다. 반면, 브라우저 클라이언트, 서드파티 개발자용 공개 API, 단순 CRUD에서는 REST가 더 적합합니다.

Q11. Circuit Breaker 패턴을 설명하세요.

전기 회로의 차단기처럼, 하위 서비스 장애 시 호출을 차단하여 장애 전파를 방지합니다. 세 가지 상태가 있습니다. Closed(정상 호출), Open(호출 차단, 즉시 실패 반환), Half-Open(일부 요청으로 복구 확인). 연속 실패가 임계값을 넘으면 Open으로 전환하고, 일정 시간 후 Half-Open에서 복구를 테스트합니다.

Q12. 서비스 디스커버리 방식을 비교하세요.

클라이언트 사이드 디스커버리: 클라이언트가 서비스 레지스트리(Eureka, Consul)에 직접 질의하여 대상 인스턴스를 선택합니다. 로드 밸런싱 로직이 클라이언트에 있습니다.

서버 사이드 디스커버리: 로드 밸런서가 서비스 레지스트리를 참조하여 라우팅합니다. Kubernetes의 Service가 대표적입니다. 클라이언트가 디스커버리 로직을 알 필요가 없어 더 단순합니다.

Q13. 모듈러 모놀리스에서 MSA로 전환하는 기준은?

다음 조건이 충족될 때 전환을 고려합니다. 특정 모듈의 확장 요구가 다른 모듈과 현저히 다를 때, 팀 규모가 50명 이상으로 성장하여 독립 배포가 필요할 때, 특정 모듈에 다른 기술 스택이 필요할 때, 장애 격리가 반드시 필요한 비즈니스 요구가 있을 때입니다.

Q14. Outbox 패턴이란 무엇인가요?

로컬 트랜잭션에서 비즈니스 데이터와 함께 이벤트를 Outbox 테이블에 저장합니다. 별도의 프로세스(Polling Publisher 또는 CDC)가 Outbox 테이블에서 이벤트를 읽어 메시지 브로커에 발행합니다. 이를 통해 비즈니스 로직과 이벤트 발행의 원자성을 보장합니다.

Q15. 마이크로서비스 테스트 전략(Testing Pyramid)을 설명하세요.

단위 테스트(서비스 내부 로직), 통합 테스트(DB/외부 연동), 계약 테스트(서비스 간 API 호환성, Pact 등), 컴포넌트 테스트(단일 서비스 E2E), E2E 테스트(전체 시스템)로 구성합니다. 계약 테스트가 특히 중요한데, 프로바이더 서비스의 변경이 컨슈머를 깨뜨리지 않는지 자동으로 검증합니다.


15. 퀴즈

Q1. 모듈러 모놀리스의 주요 장점 3가지와 MSA 대비 한계 2가지를 설명하세요.

장점: (1) MSA의 복잡성(네트워크 통신, 분산 트랜잭션, 서비스 메시) 없이 모듈 단위 분리 가능, (2) ACID 트랜잭션 사용 가능으로 데이터 정합성 보장 용이, (3) 단일 배포 단위로 운영/배포 간편.

한계: (1) 서비스별 독립 확장 불가(특정 모듈만 수평 확장 어려움), (2) 기술 스택이 단일 언어/프레임워크로 제한됨.

Q2. Saga 패턴에서 보상 트랜잭션(Compensating Transaction)이 실패하면 어떻게 처리하나요?

보상 트랜잭션 자체의 실패는 시스템을 비정합 상태로 만들 수 있어 매우 중요한 문제입니다. 해결 방안: (1) 재시도 정책(지수 백오프)으로 보상 작업을 반복 시도, (2) Dead Letter Queue에 실패한 보상 이벤트를 저장하여 수동/자동 복구, (3) 보상 트랜잭션을 멱등하게 설계하여 안전한 재시도 보장, (4) 모니터링/알림으로 운영팀에 즉시 통보하여 수동 개입.

Q3. Service Mesh 도입 시 사이드카 프록시의 오버헤드를 최소화하는 방법은?

(1) 경량 프록시 선택 (Linkerd의 Rust 기반 프록시는 Envoy 대비 메모리 사용량이 크게 적음), (2) 사이드카 리소스 요청/제한을 적절히 설정 (CPU 100m, Memory 50Mi 등), (3) 메시 기능 중 필요한 것만 활성화 (불필요한 텔레메트리 비활성화), (4) eBPF 기반 서비스 메시(Cilium Service Mesh) 고려 - 사이드카 없이 커널 레벨에서 처리, (5) 지연시간에 극도로 민감한 서비스는 메시에서 제외.

Q4. Strangler Fig 패턴 적용 시 가장 먼저 분리해야 할 서비스를 고르는 기준은?

(1) 다른 모듈과의 의존성이 가장 적은 기능 (독립적으로 동작 가능), (2) 비즈니스 가치가 높아 팀의 동기 부여에 도움, (3) 잘 정의된 도메인 경계가 존재, (4) 독립적 확장이나 별도 기술이 필요한 영역, (5) 테스트가 충분하여 동작 검증이 용이. 반대로 핵심 결제, 인증 같은 고위험 영역은 나중에 분리하는 것이 안전합니다.

Q5. OpenTelemetry에서 Context Propagation이 중요한 이유와 동작 방식을 설명하세요.

Context Propagation은 분산 환경에서 하나의 요청을 추적하기 위해 Trace ID와 Span ID를 서비스 간에 전달하는 메커니즘입니다. 중요한 이유: 이것 없이는 각 서비스의 로그/메트릭/트레이스가 분리되어 요청 흐름 파악이 불가능합니다. 동작 방식: (1) 첫 서비스에서 Trace ID 생성, (2) HTTP 헤더(W3C traceparent) 또는 gRPC 메타데이터로 다음 서비스에 전파, (3) 메시지 큐 환경에서는 메시지 헤더에 컨텍스트 포함, (4) 각 서비스에서 수신한 컨텍스트를 바탕으로 자식 Span 생성.


참고 자료