전체 목록
설계Medium#49

마이크로서비스 아키텍처와 모놀리식 아키텍처의 장단점을 비교해주세요.

#설계#MSA#아키텍처#시스템설계
힌트

확장성, 복잡성, 팀 독립성, 운영 비용을 생각해보세요.

정답 및 해설

마이크로서비스 아키텍처와 모놀리식 아키텍처의 장단점을 비교해주세요.

소프트웨어 아키텍처를 설계할 때 가장 근본적인 결정 중 하나가 모놀리식(Monolithic)과 마이크로서비스(Microservices) 중 무엇을 선택할지입니다. 모놀리식은 하나의 단일 애플리케이션으로 모든 기능을 구성하는 방식이고, 마이크로서비스는 각 기능을 독립적으로 배포 가능한 작은 서비스들로 분리하는 방식입니다. 두 접근법 모두 장단점이 명확하여 팀 규모, 서비스 규모, 기술 성숙도에 따라 적합한 선택이 달라집니다.

모놀리식 아키텍처

구조

[모놀리식 애플리케이션]
┌─────────────────────────────────────────┐
│  사용자 인터페이스 (UI Layer)             │
├─────────────────────────────────────────┤
│  비즈니스 로직 (Business Logic Layer)    │
│  ├── 사용자 모듈                          │
│  ├── 주문 모듈                            │
│  ├── 결제 모듈                            │
│  └── 알림 모듈                            │
├─────────────────────────────────────────┤
│  데이터 접근 (Data Access Layer)          │
├─────────────────────────────────────────┤
│  단일 데이터베이스                         │
└─────────────────────────────────────────┘
          ↕ (단일 프로세스로 실행)
     [단일 배포 단위]

장점

1. 단순한 개발 환경
   - 단일 IDE/프로젝트에서 전체 코드 파악
   - 로컬 실행이 간단 (npm start 하나로)
   - 디버깅이 쉬움 (하나의 프로세스 추적)

2. 단순한 배포
   - 하나의 빌드, 하나의 배포 단위
   - CI/CD 파이프라인 구성이 단순

3. 성능 이점
   - 서비스 간 통신이 메서드 호출 (네트워크 오버헤드 없음)
   - 트랜잭션 관리가 단순 (단일 DB)

4. 단순한 테스트
   - 통합 테스트가 쉬움
   - 서비스 간 mock 불필요

5. 팀 협업
   - 소규모 팀에서 전체 코드를 파악하기 쉬움

단점

// 모놀리식의 문제점 예시
// 하나의 기능 변경이 전체에 영향을 미침

// 결제 모듈의 버그가 전체 서비스 다운으로 이어질 수 있음
class PaymentService {
  processPayment(order: Order) {
    // 이 함수에 버그가 생기면 전체 앱에 영향
    throw new Error('결제 시스템 오류')
  }
}

// 배포 시 전체 애플리케이션 재시작 필요
// 결제 코드 한 줄 수정해도 전체 서비스 배포 필요
단점:
1. 확장의 어려움
   - 특정 기능만 스케일 아웃 불가
   - 전체 앱을 복제해야 함 (메모리/CPU 낭비)

2. 기술 스택 고정
   - 전체가 동일한 언어/프레임워크 사용 강제
   - 새 기술 도입이 어려움

3. 장애 전파
   - 한 모듈의 장애가 전체 서비스에 영향

4. 배포 위험성
   - 작은 변경도 전체 배포 필요
   - 배포 실패 시 전체 서비스 중단

5. 코드베이스 복잡도
   - 시간이 지날수록 "빅볼 오브 머드" 현상
   - 코드 의존성이 뒤엉킴

마이크로서비스 아키텍처

구조

[마이크로서비스 아키텍처]

클라이언트
   ↓
[API Gateway]
   ↓
┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
│ 사용자   │  │ 주문     │  │ 결제     │  │ 알림     │
│ 서비스   │  │ 서비스   │  │ 서비스   │  │ 서비스   │
│          │  │          │  │          │  │          │
│ [DB]     │  │ [DB]     │  │ [DB]     │  │ [DB]     │
└──────────┘  └──────────┘  └──────────┘  └──────────┘
     ↕              ↕              ↕              ↕
         [메시지 브로커 (Kafka/RabbitMQ)]

서비스 간 통신 방식

// 동기 통신 - REST API
// 주문 서비스에서 사용자 정보 조회
class OrderService {
  async createOrder(userId: string, items: Item[]) {
    // 사용자 서비스 HTTP 호출
    const user = await fetch(`http://user-service/api/users/${userId}`)
      .then(res => res.json())

    // 결제 서비스 HTTP 호출
    const payment = await fetch('http://payment-service/api/payments', {
      method: 'POST',
      body: JSON.stringify({ userId, amount: this.calculateTotal(items) })
    }).then(res => res.json())

    return { orderId: Date.now(), user, payment, items }
  }
}

// 비동기 통신 - 메시지 브로커 (Kafka)
// 주문 완료 이벤트 발행
class OrderService {
  async completeOrder(orderId: string) {
    await this.orderRepository.complete(orderId)

    // 이벤트 발행 - 다른 서비스가 구독
    await this.kafka.publish('order.completed', {
      orderId,
      userId: order.userId,
      amount: order.totalAmount,
      timestamp: new Date()
    })
  }
}

// 알림 서비스에서 이벤트 구독
class NotificationService {
  @KafkaSubscribe('order.completed')
  async onOrderCompleted(event: OrderCompletedEvent) {
    await this.emailSender.send(event.userId, '주문이 완료되었습니다.')
    await this.smsSender.send(event.userId, '주문 완료')
  }
}

장점

1. 독립적 배포/확장
   - 각 서비스를 독립적으로 배포
   - 주문 서비스만 스케일 아웃 가능 (비용 효율)

2. 기술 스택 자유
   - 사용자 서비스: Node.js
   - 결제 서비스: Java (안정성)
   - 데이터 분석: Python
   - 각 서비스에 최적화된 DB 선택 가능

3. 장애 격리
   - 결제 서비스 장애 → 다른 서비스 정상 운영 가능 (Circuit Breaker 적용 시)

4. 팀 독립성
   - 각 서비스를 담당하는 팀이 독립적으로 개발
   - Conway's Law: 팀 구조 = 시스템 구조

5. 재사용성
   - 인증 서비스를 여러 프로덕트에서 공유

단점

// 마이크로서비스의 복잡성 예시

// 분산 트랜잭션 관리 - Saga 패턴 필요
class OrderSaga {
  async executeOrderCreation(order: Order) {
    try {
      // 1. 재고 서비스에서 재고 확인/감소
      await this.inventoryService.reserve(order.items)

      // 2. 결제 서비스에서 결제 처리
      const payment = await this.paymentService.charge(order.amount)

      // 3. 주문 생성
      await this.orderRepository.create(order)

    } catch (error) {
      // 실패 시 보상 트랜잭션 (Compensating Transaction)
      await this.inventoryService.release(order.items)  // 재고 복구
      await this.paymentService.refund(payment.id)      // 결제 취소
      throw error
    }
  }
}

// 서비스 간 네트워크 오류 처리 - Circuit Breaker
class PaymentServiceClient {
  private circuitBreaker = new CircuitBreaker({
    failureThreshold: 5,  // 5번 실패 시 차단
    resetTimeout: 60000,  // 60초 후 재시도
  })

  async charge(amount: number) {
    return this.circuitBreaker.execute(async () => {
      const response = await fetch('http://payment-service/charge', {
        method: 'POST',
        body: JSON.stringify({ amount }),
        signal: AbortSignal.timeout(3000) // 3초 타임아웃
      })
      if (!response.ok) throw new Error('결제 서비스 오류')
      return response.json()
    })
  }
}
단점:
1. 분산 시스템 복잡성
   - 네트워크 지연, 장애, 재시도 처리 필요
   - 분산 트랜잭션 (Saga 패턴 등) 구현 복잡

2. 운영 인프라 비용
   - 서비스 디스커버리, API Gateway, 메시지 브로커
   - 각 서비스별 모니터링/로깅 (중앙화 필요)
   - 컨테이너 오케스트레이션 (Kubernetes 등)

3. 개발 복잡도
   - 로컬 환경에서 여러 서비스 실행 (Docker Compose 등)
   - API 버전 관리 복잡
   - 서비스 간 계약(Contract) 관리

4. 데이터 일관성
   - 각 서비스가 별도 DB → 조인 쿼리 불가
   - 최종 일관성(Eventual Consistency) 패턴 필요

마이크로서비스 필수 인프라

# Docker Compose로 로컬 마이크로서비스 환경
version: '3.8'
services:
  api-gateway:
    image: nginx
    ports:
      - "80:80"

  user-service:
    build: ./user-service
    environment:
      - DB_URL=postgres://user-db:5432/users
    depends_on:
      - user-db

  order-service:
    build: ./order-service
    environment:
      - DB_URL=mongo://order-db:27017/orders
      - KAFKA_URL=kafka:9092

  payment-service:
    build: ./payment-service
    environment:
      - DB_URL=postgres://payment-db:5432/payments

  kafka:
    image: confluentinc/cp-kafka
    ports:
      - "9092:9092"

  user-db:
    image: postgres
  order-db:
    image: mongo
  payment-db:
    image: postgres

선택 기준

모놀리식을 선택해야 하는 경우:
✓ 스타트업 또는 MVP 단계
✓ 소규모 팀 (10명 이하)
✓ 도메인이 아직 불분명 (경계 찾기 어려움)
✓ 빠른 출시가 우선
✓ 팀에 DevOps/인프라 전문가 부족

마이크로서비스를 선택해야 하는 경우:
✓ 대규모 팀 (여러 독립 팀 운영)
✓ 트래픽이 서비스별로 크게 다름 (독립 확장 필요)
✓ 기술 다양성이 필요
✓ 높은 가용성이 필요 (장애 격리)
✓ DevOps 성숙도가 높음

마틴 파울러의 조언:
"마이크로서비스로 시작하지 말라. 모놀리스로 시작해서
시스템이 성숙하면 자연스럽게 분리하라."
(MonolithFirst 패턴)

중간 단계: 모듈러 모놀리스

// 모놀리스를 모듈로 잘 구조화하면 나중에 마이크로서비스로 분리 용이
// Next.js 풀스택 예시 - 도메인별 모듈 구조

// app/
// ├── (api)/
// │   ├── users/       → 나중에 user-service로 분리 가능
// │   ├── orders/      → 나중에 order-service로 분리 가능
// │   └── payments/    → 나중에 payment-service로 분리 가능
// ├── lib/
// │   ├── users/       (DB 접근, 비즈니스 로직)
// │   ├── orders/
// │   └── payments/
// └── components/

// 명확한 모듈 경계를 유지하면 마이크로서비스 전환이 쉬워짐
// lib/orders/service.ts
export class OrderService {
  // 외부에서는 이 서비스만 사용 (내부 구현 은닉)
  async createOrder(data: CreateOrderDto): Promise<Order> { /* ... */ }
}

// 다른 모듈에서 직접 DB 접근 금지 - 서비스를 통해서만
// ❌ import { db } from '@/lib/orders/db'  // 내부 구현 직접 접근
// ✅ import { OrderService } from '@/lib/orders/service'  // 공개 인터페이스만

정리 표

구분모놀리식마이크로서비스
개발 복잡도낮음높음
배포 단위전체 애플리케이션각 서비스 독립 배포
확장성전체 복제만 가능서비스별 독립 확장
장애 격리어려움 (장애 전파)가능 (Circuit Breaker)
기술 스택단일서비스별 자유 선택
데이터 관리단일 DB, 쉬운 트랜잭션분산 DB, 복잡한 트랜잭션
운영 인프라단순복잡 (쿠버네티스 등 필요)
팀 구조단일 팀에 적합다수 독립 팀에 적합
적합한 단계초기/소규모성장기/대규모
로컬 개발단순복잡 (Docker Compose 등)