들어가며
서비스가 성장하면서 코드베이스가 비대해지고, 배포 한 번에 전체 시스템이 흔들리는 경험을 해본 적 있으신가요? 많은 팀이 이 시점에서 MSA(Microservice Architecture) 전환을 고민합니다. 하지만 MSA는 단순히 서비스를 쪼개는 것이 아닙니다. 잘못 도입하면 모놀리식보다 더 복잡한 "분산 모놀리스"라는 최악의 상황을 만들 수도 있습니다.
이 글에서는 모놀리식에서 MSA로 전환할 때 반드시 알아야 할 설계 원칙, 핵심 패턴, 그리고 실무에서 자주 겪는 함정까지 정리해 보겠습니다.
1. MSA vs 모놀리식 아키텍처 비교
모놀리식 아키텍처
모놀리식은 하나의 배포 단위로 전체 애플리케이션이 구성되는 전통적인 방식입니다. 모든 비즈니스 로직, 데이터 접근 계층, UI가 단일 프로세스 안에서 동작합니다.
- 장점: 개발 초기 생산성이 높고, 트랜잭션 처리가 단순하며, 디버깅과 테스트가 상대적으로 쉽습니다. 로컬에서 전체 시스템을 띄우기도 간편합니다.
- 단점: 코드베이스가 커질수록 빌드·배포 시간이 길어지고, 한 모듈의 변경이 다른 모듈에 영향을 줄 수 있습니다. 특정 기능만 스케일 아웃하기 어렵고, 기술 스택 변경이 전체에 영향을 미칩니다.
MSA(마이크로서비스 아키텍처)
MSA는 비즈니스 도메인 단위로 독립적인 서비스를 구성하고, 각 서비스가 자체 데이터 저장소를 가지며 네트워크를 통해 통신하는 아키텍처입니다.
- 장점: 서비스별 독립 배포가 가능하고, 특정 서비스만 스케일 아웃할 수 있습니다. 팀별 자율성이 높아지며, 서비스별로 최적의 기술 스택을 선택할 수 있습니다(Polyglot).
- 단점: 분산 시스템의 본질적 복잡도가 증가합니다. 네트워크 지연, 부분 실패 처리, 분산 트랜잭션, 서비스 간 데이터 일관성 등 모놀리식에서는 고려하지 않아도 됐던 문제들을 다뤄야 합니다. 운영 인프라(모니터링, 로깅, 배포 파이프라인) 비용도 크게 늘어납니다.
2. MSA 핵심 설계 원칙
단일 책임 원칙 (Single Responsibility)
각 마이크로서비스는 하나의 비즈니스 도메인 또는 기능에 집중해야 합니다. "주문 서비스"가 결제 로직까지 포함하고 있다면, 그건 제대로 분리되지 않은 것입니다. DDD(Domain-Driven Design)의 Bounded Context 개념을 활용하면 서비스 경계를 명확하게 정의할 수 있습니다.
독립 배포 (Independent Deployment)
MSA의 핵심 가치 중 하나는 각 서비스를 다른 서비스에 영향 없이 독립적으로 배포할 수 있다는 점입니다. 이를 위해서는 서비스 간 인터페이스(API 계약)를 명확히 정의하고, 하위 호환성을 유지해야 합니다. API 버저닝 전략도 미리 수립해 두는 것이 좋습니다.
데이터 분리 (Database per Service)
이것이 가장 어렵지만 가장 중요한 원칙입니다. 각 서비스는 자체 데이터베이스를 소유하고, 다른 서비스의 DB에 직접 접근해서는 안 됩니다. 공유 데이터베이스는 서비스 간 결합도를 높이고, 독립 배포를 불가능하게 만듭니다.
// 잘못된 예: 주문 서비스가 사용자 DB에 직접 쿼리
SELECT * FROM user_db.users WHERE user_id = ?
// 올바른 예: 주문 서비스가 사용자 서비스 API를 호출
GET /api/users/{userId}
3. 주요 MSA 패턴
API Gateway 패턴
클라이언트가 수십 개의 마이크로서비스 엔드포인트를 직접 알아야 한다면 관리가 불가능해집니다. API Gateway는 단일 진입점을 제공하여 라우팅, 인증/인가, 속도 제한(Rate Limiting), 요청 집계(Aggregation)를 처리합니다.
대표적인 구현체로는 Spring Cloud Gateway, Kong, AWS API Gateway 등이 있습니다.
// Spring Cloud Gateway 라우팅 설정 예시
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://ORDER-SERVICE
predicates:
- Path=/api/orders/**
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/api/users/**
Service Discovery 패턴
MSA 환경에서 서비스 인스턴스는 동적으로 생성되고 소멸됩니다. 고정 IP나 포트로는 관리할 수 없습니다. Service Discovery는 서비스 레지스트리에 인스턴스 정보를 등록하고, 호출 측에서 이를 조회하여 통신하는 메커니즘입니다.
- 클라이언트 사이드 디스커버리: 호출하는 쪽에서 레지스트리를 직접 조회 (Netflix Eureka + Ribbon 방식)
- 서버 사이드 디스커버리: 로드밸런서가 레지스트리를 조회하여 라우팅 (AWS ALB, Kubernetes Service 방식)
Kubernetes 환경이라면 별도의 Service Discovery 솔루션 없이 K8s Service와 DNS를 활용할 수 있습니다.
Circuit Breaker 패턴
마이크로서비스 환경에서 하나의 서비스 장애가 연쇄적으로 전파되는 것(Cascading Failure)은 치명적입니다. Circuit Breaker는 전기 회로의 차단기처럼, 일정 횟수 이상 실패하면 해당 서비스 호출을 일시적으로 차단하고 빠르게 실패(Fail-Fast)하거나 대체 응답(Fallback)을 반환합니다.
// Resilience4j Circuit Breaker 예시
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public UserResponse getUser(Long userId) {
return userServiceClient.getUser(userId);
}
public UserResponse getUserFallback(Long userId, Throwable t) {
log.warn("User service 호출 실패, fallback 응답 반환: {}", t.getMessage());
return UserResponse.defaultUser(userId);
}
Circuit Breaker에는 세 가지 상태가 있습니다: Closed(정상 호출), Open(호출 차단), Half-Open(일부 호출을 시도하여 복구 여부 확인). Resilience4j나 Spring Cloud Circuit Breaker를 사용하면 간결하게 적용할 수 있습니다.
4. 서비스 간 통신 방식
동기(Synchronous) 통신
호출자가 응답을 기다리는 방식입니다. 직관적이지만 서비스 간 결합도가 높아지고, 호출 체인이 길어질수록 전체 응답 시간이 늘어납니다.
- REST (HTTP): 가장 보편적인 방식. 이해하기 쉽고 도구 지원이 풍부합니다. 다만 텍스트 기반(JSON)이라 직렬화/역직렬화 오버헤드가 있습니다.
- gRPC: Protocol Buffers 기반의 바이너리 통신. REST보다 직렬화 성능이 뛰어나고, HTTP/2 기반으로 스트리밍을 지원합니다. 서비스 간 내부 통신에 적합합니다. 다만 브라우저에서 직접 호출이 어려워 외부 API로는 잘 쓰이지 않습니다.
비동기(Asynchronous) 통신
메시지 브로커를 통해 느슨하게 결합된 방식입니다. 서비스 간 직접적인 의존성을 줄이고, 일시적인 장애에도 메시지가 유실되지 않습니다.
- 메시지 큐 (Kafka, RabbitMQ): 이벤트를 발행하고 구독하는 Pub/Sub 모델. 주문 생성 이벤트를 발행하면, 결제 서비스, 재고 서비스, 알림 서비스가 각자 구독하여 처리합니다.
// Spring Kafka Producer 예시
@Service
public class OrderEventPublisher {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
public void publishOrderCreated(Order order) {
OrderEvent event = new OrderEvent("ORDER_CREATED", order);
kafkaTemplate.send("order-events", order.getId(), event);
}
}
실무 권장: 외부 API나 즉각적인 응답이 필요한 경우 동기(REST/gRPC), 서비스 간 이벤트 전파나 비핵심 후속 처리는 비동기(메시지 큐)로 구분하여 적용하세요.
5. 분산 트랜잭션 처리: Saga 패턴
모놀리식에서는 하나의 데이터베이스 트랜잭션으로 데이터 일관성을 보장했습니다. 하지만 MSA에서 각 서비스가 자체 DB를 가지면, 여러 서비스에 걸친 작업의 원자성을 어떻게 보장할까요?
2PC(Two-Phase Commit)는 성능 저하와 가용성 문제가 있어 MSA에서는 잘 쓰이지 않습니다. 대신 Saga 패턴을 사용합니다.
Saga란?
Saga는 각 서비스의 로컬 트랜잭션을 순차적으로 실행하고, 중간에 실패하면 이전 단계의 보상 트랜잭션(Compensating Transaction)을 실행하여 데이터를 원래 상태로 되돌리는 패턴입니다.
Choreography vs Orchestration
- Choreography(이벤트 기반): 각 서비스가 이벤트를 발행하고 다음 서비스가 이를 구독하여 처리합니다. 중앙 제어가 없어 느슨한 결합이 가능하지만, 전체 흐름을 파악하기 어렵고 디버깅이 복잡합니다.
- Orchestration(중앙 관리): Saga Orchestrator가 전체 워크플로를 관리합니다. 흐름이 명확하고 모니터링이 쉽지만, Orchestrator가 단일 장애 지점이 될 수 있습니다.
// Saga 흐름 예시: 주문 처리
1. 주문 서비스: 주문 생성 (로컬 트랜잭션)
2. 결제 서비스: 결제 처리 (로컬 트랜잭션)
3. 재고 서비스: 재고 차감 (로컬 트랜잭션)
// 3단계에서 재고 부족으로 실패 시 보상 트랜잭션 실행
3-1. 결제 서비스: 결제 취소 (보상 트랜잭션)
3-2. 주문 서비스: 주문 취소 (보상 트랜잭션)
실무에서는 3~4개 이하의 단순한 흐름에는 Choreography, 복잡한 비즈니스 흐름에는 Orchestration을 적용하는 것이 일반적입니다.
6. MSA 도입 시 흔히 하는 실수와 주의사항
실수 1: 너무 잘게 쪼개기
서비스를 지나치게 세분화하면 네트워크 호출이 폭증하고, 서비스 간 의존성이 복잡해져서 오히려 개발 생산성이 떨어집니다. 처음부터 완벽한 분리를 목표로 하지 말고, 비즈니스 도메인 단위로 적절한 크기를 유지하세요. "2 Pizza Team"(한 팀이 관리할 수 있는 크기)이 좋은 기준입니다.
실수 2: 공유 데이터베이스 사용
서비스는 분리했지만 DB를 공유하는 경우가 의외로 많습니다. 이렇게 하면 스키마 변경 시 여러 서비스가 동시에 영향을 받아 독립 배포가 불가능해집니다. 처음엔 귀찮더라도 데이터를 반드시 분리하세요.
실수 3: 모니터링·관측 가능성(Observability) 부재
분산 시스템에서 문제가 발생했을 때, 어디서 어떤 오류가 났는지 추적할 수 없다면 운영이 불가능합니다. MSA 도입 전에 반드시 다음을 갖추세요:
- 분산 추적(Distributed Tracing): Zipkin, Jaeger 등으로 요청의 전체 흐름을 추적
- 중앙 집중 로깅: ELK Stack(Elasticsearch + Logstash + Kibana)이나 Grafana Loki로 로그 통합
- 메트릭 수집: Prometheus + Grafana로 서비스별 성능 지표 모니터링
실수 4: CI/CD 파이프라인 없이 MSA 도입
서비스가 10개인데 수동 배포를 한다면? MSA의 장점인 독립 배포가 오히려 운영 부담이 됩니다. 서비스별 자동화된 빌드·테스트·배포 파이프라인은 MSA의 전제 조건입니다.
실수 5: 동기 호출 체인 남용
A → B → C → D 식으로 동기 호출 체인이 길어지면, 하나의 서비스 지연이 전체 응답 시간에 영향을 줍니다. 가능한 곳에서는 비동기 이벤트 기반 통신으로 결합도를 낮추세요.
7. 언제 MSA를 도입해야 하는가?
MSA는 만능이 아닙니다. 도입 전에 다음 기준을 냉정하게 판단해 보세요.
MSA가 적합한 경우
- 팀 규모가 커져서(보통 20명 이상) 하나의 코드베이스에서 협업이 어려울 때
- 서비스별로 트래픽 패턴이 달라서 독립적인 스케일링이 필요할 때
- 배포 주기를 서비스별로 다르게 가져가야 할 때
- 특정 모듈에 다른 기술 스택이 필요할 때 (예: ML 파이프라인은 Python, API 서버는 Java)
- 충분한 DevOps 역량과 인프라(K8s, CI/CD 등)가 갖춰져 있을 때
모놀리식이 나은 경우
- 스타트업 초기 단계로 비즈니스 도메인 경계가 아직 불명확할 때
- 팀 규모가 작아서(10명 이하) 하나의 코드베이스로 충분히 관리 가능할 때
- 빠른 프로토타이핑이나 MVP 개발이 목표일 때
- DevOps 인프라나 운영 역량이 부족할 때
Martin Fowler는 이를 "Monolith First"라고 표현했습니다. 모놀리식으로 시작해서 도메인을 충분히 이해한 후, 필요한 시점에 점진적으로 MSA로 전환하는 것이 가장 현실적인 접근입니다.
마치며
MSA는 강력한 아키텍처이지만, 그만큼 높은 수준의 엔지니어링 역량을 요구합니다. 서비스를 쪼개기 전에 도메인을 깊이 이해하고, 인프라와 조직이 준비되어 있는지 먼저 점검하세요. "기술적으로 멋있어 보여서"가 아니라, "우리 팀의 문제를 실제로 해결해 주는가"를 기준으로 판단하는 것이 중요합니다.
다음 글에서는 Spring Cloud를 활용한 MSA 실습 구현 예제를 다뤄보겠습니다.
'Architecture' 카테고리의 다른 글
| 헥사고날 아키텍처 완벽 가이드 - 포트와 어댑터로 클린 아키텍처 구현 (0) | 2026.04.08 |
|---|---|
| DDD(Domain-Driven Design) 실전 가이드 - 전략적 설계부터 전술적 구현까지 (2) | 2026.04.07 |
| MSA 관측 가능성(Observability) 완벽 가이드 (0) | 2026.03.31 |
| MSA 관측 가능성(Observability) 완벽 가이드 (0) | 2026.03.30 |
| CQRS & 이벤트 소싱 패턴 - 언제 왜 사용하는가 (0) | 2026.03.27 |