hobokai 님의 블로그

마이크로서비스 아키텍처 완벽 가이드 1편: 기초와 모노리스 분해 전략 본문

System Architecture

마이크로서비스 아키텍처 완벽 가이드 1편: 기초와 모노리스 분해 전략

hobokai 2025. 7. 23. 16:43

목차

  1. 마이크로서비스란?
  2. 모노리스 vs 마이크로서비스
  3. 마이크로서비스 설계 원칙
  4. 모노리스 분해 전략
  5. 실전 분해 사례
  6. 다음 편 미리보기

마이크로서비스란?

마이크로서비스 아키텍처는 하나의 큰 애플리케이션을 작고 독립적인 서비스들로 분해하여 구축하는 소프트웨어 개발 접근법입니다. 각 서비스는 특정 비즈니스 기능을 담당하며, 독립적으로 개발, 배포, 확장할 수 있습니다.

핵심 특징

독립성 🔸

  • 각 서비스는 독립적인 프로세스로 실행
  • 서로 다른 기술 스택 사용 가능
  • 독립적인 데이터베이스 보유

분산성 🔸

  • 네트워크를 통한 서비스 간 통신
  • 여러 서버에 분산 배포
  • 장애 격리 및 복구 능력

비즈니스 중심 🔸

  • 도메인 기반 서비스 분해
  • 팀 단위 서비스 소유
  • 빠른 기능 개발 및 배포

왜 마이크로서비스인가?

📈 확장성: 필요한 서비스만 선택적 확장
🚀 개발 속도: 팀별 독립적 개발 및 배포
🛡️ 장애 격리: 한 서비스 장애가 전체에 영향 없음
🔄 기술 다양성: 서비스별 최적 기술 스택 선택
👥 팀 자율성: 작은 팀이 서비스 전체 책임

모노리스 vs 마이크로서비스

모노리스 아키텍처

┌─────────────────────────────────────┐
│           모노리스 애플리케이션           │
├─────────────────────────────────────┤
│  사용자 인터페이스                     │
├─────────────────────────────────────┤
│  비즈니스 로직                        │
│  ├── 주문 관리                       │
│  ├── 재고 관리                       │
│  ├── 결제 처리                       │
│  └── 사용자 관리                     │
├─────────────────────────────────────┤
│  데이터 접근 계층                     │
├─────────────────────────────────────┤
│           단일 데이터베이스             │
└─────────────────────────────────────┘

장점:

  • 단순한 개발 환경
  • 쉬운 테스트 및 디버깅
  • 간단한 배포 과정
  • 낮은 초기 복잡성

단점:

  • 확장성 제한
  • 기술 스택 종속성
  • 전체 재배포 필요
  • 대규모 팀 협업 어려움

마이크로서비스 아키텍처

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│   주문 서비스  │  │  재고 서비스  │  │   결제 서비스  │
├──────────────┤  ├──────────────┤  ├──────────────┤
│   API Gateway│  │   API Gateway│  │   API Gateway│
├──────────────┤  ├──────────────┤  ├──────────────┤
│  비즈니스 로직 │  │  비즈니스 로직 │  │  비즈니스 로직 │
├──────────────┤  ├──────────────┤  ├──────────────┤
│     주문 DB   │     재고 DB     │  │     결제 DB   │
└──────────────┘  └──────────────┘  └──────────────┘
        │                 │                 │
        └─────────────────┼─────────────────┘
                          │
              ┌──────────────────────┐
              │    메시지 브로커       │
              │  (Kafka/RabbitMQ)    │
              └──────────────────────┘

장점:

  • 독립적 확장 가능
  • 기술 다양성 지원
  • 빠른 개발 및 배포
  • 장애 격리 효과

단점:

  • 높은 초기 복잡성
  • 네트워크 통신 오버헤드
  • 분산 시스템 관리 부담
  • 데이터 일관성 문제

마이크로서비스 설계 원칙

1. 단일 책임 원칙 (Single Responsibility)

각 서비스는 하나의 비즈니스 기능만 담당해야 합니다.

# 좋은 예: 주문 서비스
class OrderService:
    def create_order(self, order_data):
        """주문 생성"""
        pass

    def update_order_status(self, order_id, status):
        """주문 상태 업데이트"""
        pass

    def get_order_history(self, user_id):
        """주문 이력 조회"""
        pass

# 나쁜 예: 모든 기능이 섞인 서비스
class MonolithService:
    def create_order(self): pass
    def manage_inventory(self): pass
    def process_payment(self): pass
    def send_notification(self): pass  # 너무 많은 책임

2. 데이터베이스 분리 원칙

각 서비스는 독립적인 데이터베이스를 가져야 합니다.

# 서비스별 데이터베이스 분리
services:
  order-service:
    database: order_db
    tables: [orders, order_items]

  inventory-service:
    database: inventory_db
    tables: [products, stock]

  payment-service:
    database: payment_db
    tables: [payments, transactions]

3. API 우선 설계 (API-First Design)

서비스 간 통신은 명확한 API 계약을 통해 이루어져야 합니다.

# OpenAPI 스펙 예시
paths:
  /orders:
    post:
      summary: 주문 생성
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
      responses:
        '201':
          description: 주문 생성 성공
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'

4. 장애 격리 (Fault Isolation)

한 서비스의 장애가 다른 서비스에 전파되지 않도록 설계해야 합니다.

# Circuit Breaker 패턴 구현
class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=60):
        self.failure_count = 0
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.last_failure_time = None
        self.state = 'CLOSED'  # CLOSED, OPEN, HALF_OPEN

    def call(self, func, *args, **kwargs):
        if self.state == 'OPEN':
            if self._should_attempt_reset():
                self.state = 'HALF_OPEN'
            else:
                raise Exception("Circuit breaker is OPEN")

        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            raise e

    def _on_success(self):
        self.failure_count = 0
        self.state = 'CLOSED'

    def _on_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.time()

        if self.failure_count >= self.failure_threshold:
            self.state = 'OPEN'

# 사용 예시
payment_circuit = CircuitBreaker()

def process_payment_with_circuit_breaker(payment_data):
    return payment_circuit.call(payment_service.process, payment_data)

모노리스 분해 전략

1. Domain-Driven Design (DDD) 접근법

Bounded Context 식별

전자상거래 도메인 분석:

┌─────────────────────────────────────────────────────┐
│                전자상거래 도메인                        │
├─────────────────────────────────────────────────────┤
│  주문 관리      │  재고 관리      │  결제 처리         │
│  (Order)       │  (Inventory)   │  (Payment)        │
│                │                │                   │
│ • 주문 생성      │ • 재고 조회      │ • 결제 처리        │
│ • 주문 추적      │ • 재고 업데이트   │ • 환불 처리        │
│ • 주문 취소      │ • 재고 예약      │ • 결제 이력        │
├─────────────────────────────────────────────────────┤
│  사용자 관리     │  배송 관리      │  알림 서비스        │
│  (User)        │  (Shipping)    │  (Notification)   │
│                │                │                   │
│ • 회원 가입      │ • 배송 주소      │ • 이메일 알림       │
│ • 로그인/로그아웃 │ • 배송 추적      │ • SMS 알림         │
│ • 프로필 관리    │ • 배송업체 관리   │ • 푸시 알림        │
└─────────────────────────────────────────────────────┘

2. 데이터 종속성 분석

# 데이터 종속성 매트릭스 분석
class DependencyAnalyzer:
    def __init__(self):
        self.dependencies = {
            'Order': ['User', 'Product', 'Payment'],
            'Inventory': ['Product'],
            'Payment': ['User', 'Order'],
            'Shipping': ['Order', 'User'],
            'Notification': ['User', 'Order', 'Payment']
        }

    def find_service_boundaries(self):
        """서비스 경계 찾기"""
        # 강한 결합도를 가진 엔티티들을 같은 서비스로 그룹화
        service_groups = {
            'order-service': ['Order', 'OrderItem'],
            'inventory-service': ['Product', 'Stock', 'Category'],
            'payment-service': ['Payment', 'Transaction', 'Refund'],
            'user-service': ['User', 'Profile', 'Authentication'],
            'shipping-service': ['Shipping', 'Address', 'Carrier'],
            'notification-service': ['Notification', 'Template', 'Channel']
        }
        return service_groups

3. Strangler Fig 패턴

기존 모노리스를 점진적으로 분해하는 전략입니다.

1단계: 새로운 기능을 마이크로서비스로 구현
┌─────────────────┐    ┌─────────────────┐
│   모노리스 기존   │    │   새 기능 서비스   │
│   기능들         │    │   (마이크로서비스) │
└─────────────────┘    └─────────────────┘

2단계: 기존 기능을 하나씩 마이크로서비스로 이전
┌─────────────────┐    ┌─────────────────┐
│   모노리스        │    │   주문 서비스     │
│   (일부 기능)     │    │                │
└─────────────────┘    └─────────────────┘
                       ┌─────────────────┐
                       │   결제 서비스     │
                       │                │
                       └─────────────────┘

3단계: 모노리스 완전 분해
┌─────────────────┐    ┌─────────────────┐
│   사용자 서비스   │    │   주문 서비스     │
│                 │    │                 │
└─────────────────┘    └─────────────────┘
┌─────────────────┐    ┌─────────────────┐
│   재고 서비스     │    │   결제 서비스     │
│                 │    │                 │
└─────────────────┘    └─────────────────┘

4. 분해 순서 결정

# 분해 우선순위 결정 프레임워크
class DecompositionPrioritizer:
    def __init__(self):
        self.criteria = {
            'business_value': 0.3,      # 비즈니스 가치
            'technical_debt': 0.25,     # 기술 부채
            'team_autonomy': 0.2,       # 팀 자율성
            'scalability_need': 0.15,   # 확장성 요구
            'complexity': 0.1           # 분해 복잡도
        }

    def calculate_priority(self, service_metrics):
        """서비스별 분해 우선순위 계산"""
        priority_scores = {}

        for service, metrics in service_metrics.items():
            score = 0
            for criterion, weight in self.criteria.items():
                score += metrics[criterion] * weight
            priority_scores[service] = score

        # 우선순위 정렬
        return sorted(priority_scores.items(), 
                     key=lambda x: x[1], reverse=True)

# 사용 예시
prioritizer = DecompositionPrioritizer()

service_metrics = {
    'order-service': {
        'business_value': 9,      # 높은 비즈니스 가치
        'technical_debt': 7,      # 기술 부채 존재
        'team_autonomy': 8,       # 팀 분리 가능
        'scalability_need': 9,    # 확장성 필요
        'complexity': 6           # 중간 복잡도
    },
    'payment-service': {
        'business_value': 10,
        'technical_debt': 8,
        'team_autonomy': 7,
        'scalability_need': 8,
        'complexity': 8
    }
}

priorities = prioritizer.calculate_priority(service_metrics)
print("분해 우선순위:", priorities)

실전 분해 사례

온라인 쇼핑몰 모노리스 분해 프로젝트

기존 모노리스 구조:

# 기존 모노리스 애플리케이션
class ECommerceApp:
    def __init__(self):
        self.user_manager = UserManager()
        self.product_catalog = ProductCatalog()
        self.order_processor = OrderProcessor()
        self.payment_handler = PaymentHandler()
        self.inventory_tracker = InventoryTracker()
        self.notification_sender = NotificationSender()

    def process_order(self, user_id, items):
        # 모든 기능이 하나의 애플리케이션에 결합
        user = self.user_manager.get_user(user_id)

        for item in items:
            product = self.product_catalog.get_product(item.product_id)
            if not self.inventory_tracker.check_stock(item.product_id, item.quantity):
                raise InsufficientStockError()

        order = self.order_processor.create_order(user_id, items)
        payment = self.payment_handler.process_payment(order.total_amount)

        self.inventory_tracker.reserve_items(items)
        self.notification_sender.send_order_confirmation(user.email, order)

        return order

분해 후 마이크로서비스 구조:

1. 주문 서비스 (Order Service)

# order-service/app.py
from flask import Flask, request, jsonify
import requests

app = Flask(__name__)

class OrderService:
    def __init__(self):
        self.user_service_url = "http://user-service:8080"
        self.inventory_service_url = "http://inventory-service:8080"
        self.payment_service_url = "http://payment-service:8080"
        self.notification_service_url = "http://notification-service:8080"

    @app.route('/orders', methods=['POST'])
    def create_order(self):
        order_data = request.json

        # 1. 사용자 검증 (User Service 호출)
        user_response = requests.get(
            f"{self.user_service_url}/users/{order_data['user_id']}"
        )
        if user_response.status_code != 200:
            return jsonify({"error": "Invalid user"}), 400

        # 2. 재고 확인 (Inventory Service 호출)
        inventory_check = requests.post(
            f"{self.inventory_service_url}/inventory/check",
            json={"items": order_data["items"]}
        )
        if not inventory_check.json()["available"]:
            return jsonify({"error": "Insufficient stock"}), 400

        # 3. 주문 생성
        order = self._create_order_record(order_data)

        # 4. 이벤트 발행 (비동기)
        self._publish_order_created_event(order)

        return jsonify(order), 201

    def _publish_order_created_event(self, order):
        """주문 생성 이벤트 발행"""
        event = {
            "event_type": "order_created",
            "order_id": order["id"],
            "user_id": order["user_id"],
            "items": order["items"],
            "total_amount": order["total_amount"]
        }
        # Kafka나 RabbitMQ로 이벤트 발행
        self.event_publisher.publish("order.created", event)

2. 재고 서비스 (Inventory Service)

# inventory-service/app.py
from flask import Flask, request, jsonify

app = Flask(__name__)

class InventoryService:
    def __init__(self):
        self.database = InventoryDatabase()
        self.event_subscriber = EventSubscriber()

    @app.route('/inventory/check', methods=['POST'])
    def check_availability(self):
        items = request.json["items"]

        for item in items:
            stock = self.database.get_stock(item["product_id"])
            if stock < item["quantity"]:
                return jsonify({"available": False}), 200

        return jsonify({"available": True}), 200

    @app.route('/inventory/reserve', methods=['POST'])
    def reserve_items(self):
        items = request.json["items"]

        try:
            with self.database.transaction():
                for item in items:
                    self.database.reserve_stock(
                        item["product_id"], 
                        item["quantity"]
                    )

            return jsonify({"reserved": True}), 200
        except Exception as e:
            return jsonify({"error": str(e)}), 400

    def handle_order_created_event(self, event):
        """주문 생성 이벤트 처리"""
        try:
            self.reserve_items_for_order(event["items"])
        except Exception as e:
            # 보상 트랜잭션 실행
            self._publish_inventory_reservation_failed_event(event["order_id"])

3. 결제 서비스 (Payment Service)

# payment-service/app.py
from flask import Flask, request, jsonify

class PaymentService:
    def __init__(self):
        self.payment_gateway = PaymentGateway()
        self.database = PaymentDatabase()

    @app.route('/payments', methods=['POST'])
    def process_payment(self):
        payment_data = request.json

        try:
            # 외부 결제 게이트웨이 호출
            gateway_response = self.payment_gateway.charge(
                amount=payment_data["amount"],
                payment_method=payment_data["payment_method"]
            )

            # 결제 기록 저장
            payment_record = self.database.save_payment(
                order_id=payment_data["order_id"],
                amount=payment_data["amount"],
                gateway_transaction_id=gateway_response["transaction_id"],
                status="completed"
            )

            # 결제 완료 이벤트 발행
            self._publish_payment_completed_event(payment_record)

            return jsonify(payment_record), 201

        except PaymentFailedException as e:
            # 결제 실패 이벤트 발행
            self._publish_payment_failed_event(payment_data["order_id"])
            return jsonify({"error": "Payment failed"}), 400

4. 이벤트 기반 통신

# 이벤트 기반 비동기 통신
class EventDrivenCommunication:
    def __init__(self):
        self.kafka_producer = KafkaProducer()
        self.kafka_consumer = KafkaConsumer()

    def setup_event_handlers(self):
        """이벤트 핸들러 설정"""

        # 주문 생성 → 재고 예약
        @self.kafka_consumer.subscribe("order.created")
        def handle_order_created(event):
            inventory_service.reserve_items(event["items"])

        # 재고 예약 완료 → 결제 처리
        @self.kafka_consumer.subscribe("inventory.reserved")
        def handle_inventory_reserved(event):
            payment_service.process_payment({
                "order_id": event["order_id"],
                "amount": event["total_amount"]
            })

        # 결제 완료 → 알림 발송
        @self.kafka_consumer.subscribe("payment.completed")
        def handle_payment_completed(event):
            notification_service.send_order_confirmation(event["order_id"])

        # 실패 시 보상 트랜잭션
        @self.kafka_consumer.subscribe("payment.failed")
        def handle_payment_failed(event):
            inventory_service.release_reservation(event["order_id"])
            order_service.cancel_order(event["order_id"])

분해 결과 비교

측면 모노리스 마이크로서비스
배포 속도 전체 재배포 (30분) 개별 서비스 배포 (5분)
팀 생산성 하나의 큰 팀 4개의 전문 팀
장애 영향 전체 서비스 중단 부분 서비스 영향
확장성 전체 스케일링 필요한 서비스만 확장
기술 선택 단일 기술 스택 서비스별 최적 기술

다음 편 미리보기

2편: API Gateway, 서비스 메시, 통신 패턴에서는:

🌐 API Gateway 패턴

  • Kong, Netflix Zuul, Spring Cloud Gateway
  • 인증/인가, 로드밸런싱, 레이트 리미팅
  • API 버저닝 및 문서화 전략

🕸️ 서비스 메시 (Service Mesh)

  • Istio, Linkerd 아키텍처
  • 사이드카 프록시 패턴
  • 트래픽 관리, 보안, 관찰성

🔄 서비스 간 통신 패턴

  • 동기 vs 비동기 통신
  • 이벤트 기반 아키텍처
  • Saga 패턴과 분산 트랜잭션

💻 실전 구현

  • Spring Cloud를 이용한 마이크로서비스
  • Docker/Kubernetes 배포
  • CI/CD 파이프라인 구축

마이크로서비스의 기초를 탄탄히 다졌다면, 이제 실제 서비스 간 통신과 인프라 구성을 다룰 차례입니다. 2편에서는 더욱 실무적이고 구체적인 구현 방법들을 살펴보겠습니다.

성공적인 마이크로서비스 전환은 기술보다 조직 문화와 프로세스 변화가 더 중요합니다. 점진적 접근과 지속적 학습이 핵심입니다.