기존 인프라 시스템과 MSA(Microservices Architecture)는 소프트웨어를 구성하고 배포하는 방식에서 근본적인 차이를 보인다. 단순히 "어떻게 코드를 나누는가"의 문제가 아니라, 팀 구조, 배포 전략, 장애 대응, 확장 방식까지 모든 것이 달라진다.
모놀리식 아키텍처 (Monolithic Architecture)
모놀리식 아키텍처는 애플리케이션의 모든 기능(UI, 비즈니스 로직, 데이터 접근 계층)이 하나의 코드베이스에 존재하고, 단일 실행 파일로 배포되는 구조다.
모놀리식의 장점
개발 초기 단계에서 명확한 이점이 있다.
•
단순한 배포: JAR 또는 WAR 하나를 서버에 복사하면 끝난다
•
로컬 개발 편의성: 하나의 IDE, 하나의 실행 환경으로 전체 시스템을 구동할 수 있다
•
디버깅 용이: 모든 로직이 같은 프로세스 안에 있으므로 스택 트레이스 추적이 직관적이다
•
인메모리 통신: 서비스 간 호출이 네트워크가 아닌 메서드 호출이므로 오버헤드가 없다
•
통합 테스트: 전체 애플리케이션을 하나의 환경에서 E2E 테스트 가능하다
•
트랜잭션 관리 단순: 로컬 ACID 트랜잭션을 그대로 사용할 수 있다
모놀리식의 한계
서비스가 성장하면서 다음 문제가 반드시 발생한다.
// 문제 예시: 결제 로직의 버그가 전체 서비스를 다운시킨다
@Service
public class PaymentService {
public void processPayment(Order order) {
// OOM 또는 무한 루프 발생 시
// 사용자 인증, 재고 조회, 알림 등 모든 기능이 함께 중단된다
heavyProcessing(order);
}
}
Java
복사
문제 | 설명 |
부분 확장 불가 | 결제 기능만 트래픽이 몰려도 전체 앱을 스케일아웃해야 한다 |
배포 리스크 | 작은 버그 수정 하나를 위해 전체 앱을 재배포해야 한다 |
기술 고착 | 일부 기능에만 새 프레임워크나 언어를 적용하기 어렵다 |
빌드 시간 증가 | 코드베이스가 커질수록 빌드와 테스트에 수십 분이 소요된다 |
팀 병목 | 여러 팀이 같은 코드베이스에서 작업하면 충돌과 조율 비용이 급증한다 |
장애 전파 | 하나의 모듈 장애가 전체 서비스 가용성에 영향을 준다 |
MSA (Microservices Architecture)
MSA는 하나의 큰 애플리케이션을 독립적으로 배포 가능한 작은 서비스들의 집합으로 분해하는 아키텍처다. 각 서비스는 단일 비즈니스 기능에 집중하고, 자체 데이터베이스를 보유하며, 네트워크(HTTP/gRPC/메시지 큐)를 통해 다른 서비스와 통신한다.
MSA의 핵심 원칙
1. 단일 책임 (Single Responsibility)
각 서비스는 하나의 비즈니스 도메인에만 집중한다. 도메인 주도 설계(DDD)의 Bounded Context 개념을 인프라 수준에서 구현한 것이다.
2. 서비스별 독립 데이터베이스
MSA에서 가장 중요하면서도 가장 많이 위반되는 원칙이다. 각 서비스는 반드시 자체 데이터베이스를 소유해야 한다.
왜 DB 공유가 문제인가?
다른 서비스 데이터가 필요할 때는 어떻게 하는가?
// 잘못된 방법: 다른 서비스의 DB를 직접 조회 (강결합, 안티패턴)
@Repository
public class OrderRepository {
// Payment DB를 직접 참조 → 절대 하면 안 된다
@Query("SELECT o.*, p.status FROM orders o JOIN payment_db.payments p ON ...")
List<Order> findOrdersWithPayment();
}
// 올바른 방법 1: API 호출 (동기)
@Service
public class OrderService {
private final PaymentClient paymentClient; // Feign Client
public OrderDetailResponse getOrderDetail(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
// 결제 정보는 Payment Service API를 통해 조회
PaymentInfo payment = paymentClient.getPayment(order.getPaymentId());
return new OrderDetailResponse(order, payment);
}
}
// 올바른 방법 2: 이벤트 기반 데이터 동기화 (비동기)
// Payment Service에서 결제 완료 이벤트 발행
@Service
public class PaymentService {
public void completePayment(Long paymentId) {
payment.complete();
paymentRepository.save(payment);
// 이벤트 발행 → Order Service가 구독해서 자체 DB 업데이트
eventPublisher.publish(new PaymentCompletedEvent(paymentId, orderId));
}
}
// Order Service에서 이벤트 수신 후 자체 DB에 저장 (데이터 복제)
@KafkaListener(topics = "payment-completed")
public void handlePaymentCompleted(PaymentCompletedEvent event) {
// Order DB에 결제 상태를 비정규화하여 저장
// → 이후 조회 시 Payment Service 호출 없이 자체 DB만으로 응답 가능
orderRepository.updatePaymentStatus(event.getOrderId(), PaymentStatus.COMPLETED);
}
Java
복사
서비스별 DB 기술 선택 전략
서비스 | 권장 DB | 이유 |
사용자 서비스 | MySQL / PostgreSQL | 정형 데이터, 트랜잭션 중요 |
상품 카탈로그 | MongoDB | 스키마 유연성, 중첩 구조 |
세션 / 캐시 | Redis | 초고속 읽기/쓰기, TTL 지원 |
검색 서비스 | Elasticsearch | 풀텍스트 검색, 집계 분석 |
결제 서비스 | PostgreSQL | 강력한 ACID, 금융 정합성 |
로그 / 이벤트 | Kafka + S3 | 대용량 스트리밍, 장기 보관 |
데이터 정합성 문제와 Eventually Consistent
모놀리식에서는 @Transactional 하나로 Order와 Payment 테이블을 원자적으로 처리할 수 있었다. MSA에서는 서비스 간 DB가 분리되어 있어 즉각적인 정합성 보장이 불가능하다. 대신 최종 일관성(Eventually Consistent) 모델을 수용해야 한다.
3. 독립 배포 (Independent Deployment)
서비스 A를 배포할 때 서비스 B, C는 영향받지 않는다. 이를 위해 하위 호환성 유지와 API 버전 관리가 필수다.
// API 버전 관리 예시 - 하위 호환성 보장
@RestController
public class OrderController {
// v1: 기존 클라이언트가 계속 사용 가능
@GetMapping("/api/v1/orders/{id}")
public OrderV1Response getOrderV1(@PathVariable Long id) {
return orderService.getOrderV1(id);
}
// v2: 새 필드 추가 (기존 v1 클라이언트에 영향 없음)
@GetMapping("/api/v2/orders/{id}")
public OrderV2Response getOrderV2(@PathVariable Long id) {
return orderService.getOrderV2(id); // 추가 필드 포함
}
}
Java
복사
배포 전략으로는 Blue/Green 배포와 Canary 배포가 주로 사용된다.
# Kubernetes를 이용한 Canary 배포 예시
# 전체 트래픽의 10%만 새 버전으로 라우팅
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service-canary
spec:
replicas: 1 # stable: 9, canary: 1 → 10% 트래픽
selector:
matchLabels:
app: order-service
track: canary
template:
spec:
containers:
- name: order-service
image: codesche/order-service:v2.0.0 # 새 버전
YAML
복사
모놀리식 vs MSA 핵심 비교
항목 | 모놀리식 | MSA |
배포 단위 | 전체 애플리케이션 | 서비스별 독립 배포 |
확장 방식 | 전체 앱 수평 확장 | 부하가 몰리는 서비스만 확장 |
장애 격리 | 하나의 버그가 전체에 영향 | 서킷 브레이커로 장애 격리 |
기술 스택 | 단일 언어/프레임워크 강제 | 서비스별 최적 기술 선택 가능 |
팀 구조 | 기능별 팀 (수평 분리) | 서비스별 팀 (수직 분리) |
데이터베이스 | 중앙집중식 단일 DB | 서비스별 독립 DB |
통신 방식 | 메서드 직접 호출 | HTTP REST, gRPC, 메시지 큐 |
로컬 개발 | 단순 (하나의 실행 환경) | 복잡 (Docker Compose 등 필요) |
운영 복잡도 | 낮음 | 높음 (서비스 디스커버리, 분산 추적 필요) |
트랜잭션 | 로컬 ACID 트랜잭션 (간단) | 분산 트랜잭션 (Saga 패턴 필요) |
인프라 관점의 차이
모놀리식 인프라 구성
# 단순한 배포 구조
- app.jar 하나 배포
- Nginx (리버스 프록시)
- 단일 DB 서버
- 스케일아웃: 동일한 app.jar를 여러 서버에 복제
┌─────────────────────────────┐
│ Client │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Nginx (L4/L7) │
└──────┬──────────────┬───────┘
│ │
┌──────▼──────┐ ┌─────▼───────┐
│ App #1 │ │ App #2 │ ← 동일한 JAR 복제
│ (app.jar) │ │ (app.jar) │
└──────┬──────┘ └─────┬───────┘
└──────┬────────┘
┌──────▼───────┐
│ 단일 DB │
└──────────────┘
YAML
복사
MSA 인프라 필수 구성 요소
MSA를 운영하려면 다음 인프라 구성 요소가 필수다.
1. API Gateway
클라이언트의 단일 진입점. 라우팅, 인증, 속도 제한(Rate Limiting), 로드밸런싱을 담당한다.
# Spring Cloud Gateway 예시
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- AuthFilter
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
- id: payment-service
uri: lb://payment-service
predicates:
- Path=/api/payments/**
YAML
복사
2. 서비스 디스커버리 (Service Discovery)
서비스 인스턴스의 위치(IP, 포트)를 동적으로 관리한다. Netflix는 Eureka를 직접 개발해 오픈소스로 공개했다.
// Eureka 클라이언트 등록 예시
@SpringBootApplication
@EnableEurekaClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
// application.yml
eureka:
client:
service-url:
defaultZone: http://eureka-server:8761/eureka/
instance:
prefer-ip-address: true
Java
복사
3. 서킷 브레이커 (Circuit Breaker)
연쇄 장애(Cascading Failure)를 방지하는 핵심 패턴이다. 호출 대상 서비스가 일정 횟수 이상 실패하면 회로를 차단하고 즉시 폴백 응답을 반환한다.
// Resilience4j 서킷 브레이커 적용
@Service
public class OrderService {
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
@TimeLimiter(name = "paymentService")
public CompletableFuture<PaymentResult> processPayment(Long orderId) {
return CompletableFuture.supplyAsync(() ->
paymentClient.process(orderId)
);
}
// 결제 서비스 장애 시 임시 응답 반환
public CompletableFuture<PaymentResult> paymentFallback(Long orderId, Exception e) {
log.warn("Payment service unavailable. orderId={}", orderId, e);
return CompletableFuture.completedFuture(
PaymentResult.pending(orderId, "결제 서비스 일시 중단 - 잠시 후 다시 시도해주세요")
);
}
}
Java
복사
# Resilience4j 설정
resilience4j:
circuitbreaker:
instances:
paymentService:
failure-rate-threshold: 50 # 실패율 50% 이상이면 회로 차단
wait-duration-in-open-state: 30s # 30초 후 Half-Open 상태 전환
sliding-window-size: 10 # 최근 10번 요청 기준
YAML
복사
4. 분산 추적 (Distributed Tracing)
모놀리식에서는 스택 트레이스 하나로 문제를 찾지만, MSA에서는 요청이 여러 서비스를 거치기 때문에 분산 추적이 필수다.
# Spring Boot + Micrometer Tracing 설정
management:
tracing:
sampling:
probability: 1.0 # 전체 요청 추적 (운영환경은 0.1 권장)
spring:
zipkin:
base-url: http://zipkin:9411
YAML
복사
5. 컨테이너 오케스트레이션 (Kubernetes)
수십~수백 개의 서비스를 수동으로 관리하는 것은 불가능하다. Kubernetes가 서비스 배포, 롤링 업데이트, 자동 복구, 오토스케일링을 담당한다.
# Order Service Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: order-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0 # 무중단 배포
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: codesche/order-service:v1.2.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "500m"
memory: "1Gi"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
YAML
복사
6. 분산 트랜잭션 - Saga 패턴
모놀리식에서는 @Transactional 하나로 해결되던 것이, MSA에서는 Saga 패턴이 필요하다.
// Choreography-based Saga 예시 (Kafka 이벤트 기반)
// 1. 주문 서비스: 주문 생성 후 이벤트 발행
@Service
public class OrderService {
public Order createOrder(OrderRequest request) {
Order order = orderRepository.save(new Order(request, OrderStatus.PENDING));
eventPublisher.publish(new OrderCreatedEvent(order.getId(), order.getItems()));
return order;
}
}
// 2. 재고 서비스: 이벤트 수신 후 재고 차감
@KafkaListener(topics = "order-created")
public void handleOrderCreated(OrderCreatedEvent event) {
try {
inventoryService.reserve(event.getItems());
eventPublisher.publish(new InventoryReservedEvent(event.getOrderId()));
} catch (InsufficientStockException e) {
// 보상 트랜잭션: 주문 취소 이벤트 발행
eventPublisher.publish(new OrderCancelledEvent(event.getOrderId(), "재고 부족"));
}
}
// 3. 결제 서비스: 재고 확보 확인 후 결제 진행
@KafkaListener(topics = "inventory-reserved")
public void handleInventoryReserved(InventoryReservedEvent event) {
// 결제 처리 로직
}
Java
복사
실제 적용 사례
Netflix - MSA의 교과서
2008년 Netflix는 데이터베이스 손상 사고로 3일간 DVD 배송이 중단되었다. 이 사고가 MSA 전환의 직접적인 계기가 되었다.
"단일 장애점인 관계형 데이터베이스에서 벗어나 고가용성의 수평 확장 가능한 분산 시스템으로 이동해야 한다는 것을 깨달았다." — Netflix 엔지니어
전환 전략:
•
중요도가 낮은 서비스부터 점진적으로 분리
•
AWS로 인프라를 전환하면서 MSA를 함께 도입
•
Eureka(서비스 디스커버리), Zuul(API Gateway), Hystrix(서킷 브레이커)를 직접 개발해 오픈소스로 공개
현재 상태:
•
1,000개 이상의 마이크로서비스 운영
•
개발자들이 하루에 수백 번 코드를 배포
•
2억 3천만 명 이상의 사용자에게 중단 없는 서비스 제공
•
Federated GraphQL을 도입해 클라이언트가 여러 마이크로서비스 데이터를 단일 쿼리로 조회
Amazon - MSA를 AWS로 연결한 사례
2002년 Jeff Bezos는 직원들에게 다음 내용의 유명한 내부 지침 메일을 발송했다.
"모든 팀은 서비스 인터페이스를 통해 데이터와 기능을 노출해야 한다. 팀 간의 모든 통신은 이 인터페이스를 통해서만 이루어져야 한다. 다이렉트 링크, 다른 팀의 데이터 스토어 직접 읽기, 공유 메모리 모델, 어떠한 백도어도 허용하지 않는다."
이 내부 플랫폼이 결국 AWS(Amazon Web Services)의 기반이 되었다. Amazon은 MSA를 20년에 걸쳐 준비한 셈이다.
Uber - 급격한 성장으로 인한 전환
초창기 Uber는 단일 모놀리식 앱에서 출발했다. 탑승객과 드라이버가 REST API를 통해 모놀리스에 연결하는 구조였다.
글로벌 확장 과정에서 다음 문제가 발생했다:
•
새 기능 개발 속도 저하
•
여러 지역에서 모놀리식 앱을 운영하는 복잡도 증가
•
사소한 수정에도 전체 시스템에 대한 깊은 이해가 필요
MSA 전환 후, 각 팀이 특정 서비스 하나에만 집중하게 되면서 문제 해결 속도와 개발 품질이 즉각적으로 향상되었다. 트래픽이 몰리는 서비스만 집중적으로 스케일아웃할 수 있어 급격한 성장에도 효율적으로 대응 가능해졌다.
MSA가 항상 정답은 아니다
상황 | 권장 아키텍처 |
초기 스타트업, MVP 단계 | 모놀리식 |
팀 규모 5명 이하 | 모놀리식 |
도메인이 단순하고 기능이 적음 | 모놀리식 |
빠른 프로토타이핑 필요 | 모놀리식 |
팀 규모가 크고 서비스가 복잡 | MSA |
트래픽 패턴이 기능별로 불균일 | MSA |
독립적인 배포 주기가 필요 | MSA |
기술 스택 다양화가 필요 | MSA |
Shopify는 수십억 달러 규모의 블랙프라이데이 트래픽을 Rails 모놀리스로 처리하고, Stack Overflow는 마이크로서비스 없이 수백만 개발자를 서비스한다. 아키텍처는 트렌드가 아니라 현재 팀의 규모와 문제에 맞게 선택해야 한다.
모놀리식에서 MSA로 전환할 때의 전략
Strangler Fig Pattern (교살자 무화과 패턴)
기존 모놀리스를 한 번에 다 바꾸는 것은 매우 위험하다. Martin Fowler가 제안한 Strangler Fig Pattern은 기존 시스템을 유지하면서 새 기능은 마이크로서비스로 개발하고, 점진적으로 기존 기능을 이전하는 방식이다.
정리
모놀리식과 MSA는 서로 다른 문제를 해결하기 위한 도구다. 모놀리식은 초기 개발 속도와 단순성을 극대화하고, MSA는 서비스가 복잡해지고 팀이 커졌을 때 독립적인 확장성과 장애 격리를 제공한다.
Netflix, Uber, Amazon 모두 모놀리식으로 시작해 성장 과정에서의 구체적인 문제를 해결하기 위해 MSA로 전환했다. 아키텍처 결정은 항상 현재 팀의 규모, 기술 역량, 서비스의 복잡도를 기준으로 내려야 한다.

