마이크로서비스를 버린 42%가 선택한 것 — 모놀리스 vs 마이크로서비스, 아키텍처 선택의 현실
솔직히 고백하면, 저도 한때 마이크로서비스를 만병통치약처럼 여겼습니다. "서비스 하나 더 뜨면 되지"라는 말이 얼마나 가볍게 나왔는지, 그 대가를 치르기 전까지는 몰랐죠. Kubernetes 클러스터 디버깅에 밤을 새운 적도 있고, 분산 트랜잭션 실패로 데이터가 꼬였을 때의 막막함도 아직도 생생합니다. "이게 맞는 아키텍처였을까?"라는 의문이 들기 시작했을 때쯤, 저만 이런 경험을 한 게 아니라는 걸 알게 됐습니다.
업계 조사 결과들을 보면 마이크로서비스를 도입했던 조직 중 42%가 서비스를 더 큰 배포 단위로 통합하고 있다는 수치가 눈에 띕니다(byteiota, 2025년 CNCF 설문 재인용). Amazon Prime Video는 특정 미디어 파이프라인을 단일 프로세스로 전환해서 인프라 비용을 90% 절감했고, Shopify는 분당 3,200만 요청을 모놀리스 하나로 처리합니다. 뭔가 이상하지 않나요?
이 글은 모듈러 모놀리스와 마이크로서비스가 각각 어떤 상황에서 진짜 빛을 발하는지, 그리고 지금 우리 팀 상황에 맞는 선택이 무엇인지를 살펴봅니다. 신규 프로젝트를 앞두고 있거나 현재 아키텍처를 재검토 중이라면, 읽고 나서 "우리 팀에는 지금 무엇이 필요한가"를 체크리스트 하나로 정리할 수 있을 겁니다.
핵심 개념
모듈러 모놀리스 — "하나지만 정돈된"
모듈러 모놀리스는 단일 배포 단위이지만, 내부가 명확한 도메인 경계로 나뉘어 있는 구조입니다. 전통적인 스파게티 모놀리스와의 차이는 딱 하나 — 각 모듈이 자신의 비즈니스 로직과 데이터를 소유하고, 다른 모듈의 내부에 직접 손대지 않는다는 점입니다. 그리고 이 경계가 코드 수준에서 강제됩니다.
아래는 Spring Modulith를 사용한 Java 예시입니다. 패키지 구조 자체가 모듈 경계를 표현하고, 경계 위반은 테스트에서 자동으로 잡힙니다.
// 패키지 구조 예시
com.example.shop
├── order/ // Order 모듈 — 내부 구현은 외부에 비공개
│ ├── OrderService.java
│ ├── OrderRepository.java
│ └── internal/
│ └── OrderValidator.java // 모듈 외부에서 접근 불가
├── payment/ // Payment 모듈
│ ├── PaymentService.java
│ └── internal/
│ └── PaymentValidator.java // 외부 직접 참조 시 테스트 실패
└── inventory/ // Inventory 모듈
└── InventoryService.java// Spring Modulith: 모듈 경계 위반을 자동 검증
@ApplicationModuleTest
class OrderModuleTests {
// Order 모듈이 Payment 내부(internal/)에 직접 접근하면 아래 오류 발생
}경계를 위반하면 테스트가 이런 메시지와 함께 실패합니다:
org.springframework.modulith.core.Violations:
- Module 'order' depends on non-exposed type
'com.example.shop.payment.internal.PaymentValidator'
in com.example.shop.order.OrderService
→ Use the public API of 'payment' module instead"컴파일은 됐는데 왜 이게 문제냐"고 생각할 수도 있는데, 6개월 후 다른 팀원이 이 코드를 건드릴 때 경계가 무너지는 걸 막아주는 안전망이 바로 이겁니다.
Spring Modulith: Spring Boot 공식 지원 라이브러리로, 모듈 경계 위반을 자동 감지하고 모듈 관계도를 문서로 생성해 줍니다. 같은 역할을 하는 도구로는 ArchUnit(Java), NetArchTest(.NET), import-linter(Python), dependency-cruiser(Node.js)가 있습니다.
마이크로서비스 — "독립적이지만 복잡한"
마이크로서비스는 애플리케이션을 네트워크로 통신하는 작은 서비스들로 분해합니다. 독립 배포, 독립 스케일링, 서비스마다 다른 기술 스택이 가능해집니다. 대신 로컬 개발 환경부터 이미 이 정도 복잡도가 생깁니다:
# docker-compose.yml — 마이크로서비스 로컬 환경 예시
services:
order-service:
image: shop/order-service:latest
ports: ["8081:8080"]
environment:
- DB_URL=jdbc:postgresql://order-db:5432/orders
payment-service:
image: shop/payment-service:latest
ports: ["8082:8080"]
environment:
- DB_URL=jdbc:postgresql://payment-db:5432/payments
api-gateway:
image: kong:latest
ports: ["8000:8000"]
order-db:
image: postgres:16
payment-db:
image: postgres:16프로덕션에는 여기에 Kubernetes(컨테이너 오케스트레이션), Istio(서비스 간 통신 관리), Jaeger(분산 로그 추적), Consul(서비스 디스커버리)이 더 얹힙니다. 이 스택을 관리할 인력이 있는지가 선택의 핵심 변수입니다.
두 아키텍처의 핵심 차이
| 구분 | 모듈러 모놀리스 | 마이크로서비스 |
|---|---|---|
| 배포 단위 | 단일 프로세스 | 다수의 독립 서비스 |
| 통신 방식 | 인프로세스 호출 | 네트워크 호출 (HTTP/gRPC/메시지 큐) |
| 데이터 격리 | 논리적 분리 (물리적 DB 공유 가능) | 서비스별 독립 DB 권장 |
| 운영 복잡도 | 낮음 | 높음 |
| 스케일링 | 전체 애플리케이션 단위 | 서비스별 독립 스케일링 |
| 인프라 비용 | 기준선 | 워크로드에 따라 3.75배~6배 수준 |
인프로세스 호출: 네트워크를 거치지 않고 같은 프로세스 내에서 함수를 직접 호출하는 방식입니다. 레이턴시가 나노초 단위라서 네트워크 오버헤드가 전혀 없습니다.
실전 적용
예시 1: Amazon Prime Video — 이 워크로드엔 마이크로서비스가 맞지 않았다
이 사례를 처음 접했을 때 솔직히 좀 충격이었습니다. "Amazon도 마이크로서비스를 버렸다"는 식으로 많이 알려져 있는데, 정확히 말하면 Video Quality Analysis라는 특정 미디어 파이프라인 워크로드의 이야기입니다. 이 맥락을 빼고 읽으면 잘못된 결론을 내릴 수 있어서 짚고 넘어가고 싶었습니다.
처음 구조는 AWS Step Functions(분산 워크플로 오케스트레이션 서비스) 기반 분산 마이크로서비스였습니다. 각 분석 단계 사이에 영상 프레임 데이터를 S3를 경유해서 주고받는 구조였죠.
[기존 구조 — 마이크로서비스]
Frame Detector → S3 저장 → Defect Detector → S3 저장 → Alert Generator
↑ ↑
Lambda Lambda
(네트워크 I/O 발생) (네트워크 I/O 발생)
[전환 후 구조 — 단일 프로세스]
Frame Detector → Defect Detector → Alert Generator
인메모리 직접 전달 (In-process)프레임 데이터를 S3를 거쳐 전달하는 대신 인메모리로 직접 넘기자 결과가 극적으로 바뀌었습니다.
| 지표 | 변경 전 (마이크로서비스) | 변경 후 (단일 프로세스) |
|---|---|---|
| 인프라 비용 | 기준 | 90% 절감 |
| 데이터 전송 경로 | S3 경유 네트워크 I/O | 인메모리 직접 전달 |
| 스케일링 성능 | 제한적 | 향상됨 |
실제로 이 전환을 결정한 팀의 입장을 생각해보면, 이건 "마이크로서비스가 나쁘다"는 결론이 아닙니다. 프레임 분석처럼 중간 상태를 계속 주고받아야 하는 파이프라인 워크로드는 서비스 간 네트워크 홉이 성능 병목이 될 수밖에 없는 구조였고, 그 특성에 맞게 아키텍처를 바꾼 겁니다.
예시 2: Shopify — 초대용량 트래픽을 모놀리스로 처리하는 방법
Shopify는 전체 코드베이스를 단일 Rails 모놀리스로 유지합니다. 처리하는 트래픽 규모를 보면 "모놀리스는 한계가 있다"는 선입견이 흔들립니다.
| 지표 | 수치 |
|---|---|
| 최대 요청 처리량 | 분당 3,200만 요청 |
| DB 쿼리 처리량 | 초당 1,100만 MySQL 쿼리 |
| 데이터 처리량 | 분당 30TB |
비결은 두 가지입니다. 내부 도메인 경계를 Packwerk로 엄격하게 강제하고, Checkout이나 사기 탐지처럼 트래픽 패턴이 특이한 고부하 영역만 선택적으로 마이크로서비스로 분리합니다.
Packwerk가 어떻게 경계를 강제하는지 보면 이렇습니다:
# components/order/package.yml — 패키지 경계 선언
name: Order
dependencies:
- components/payment # Payment의 공개 API만 의존 가능
enforce_privacy: true # internal/ 하위 타입에 직접 접근 금지# components/order/app/services/order_service.rb
module Order
class OrderService
def process(cart)
# Payment의 공개 API를 통해서만 접근 가능
Order::OrderCreator.new.create_from_cart(cart)
end
end
end만약 Payment 내부 구현에 직접 접근하려 하면, packwerk check 실행 시 이렇게 위반이 잡힙니다:
components/order/app/services/order_service.rb:8:5
Privacy violation: '::Payment::Internal::PaymentProcessor' is private to 'Payment'
Is there a public entrypoint in 'Payment' that could be used instead?Packwerk: Shopify가 만든 Ruby 패키지 경계 강제 도구입니다. 설정 파일로 의존 허용 범위를 선언하고,
packwerk check를 CI에 넣으면 경계 위반이 자동으로 잡힙니다. ArchUnit(Java), NetArchTest(.NET)도 같은 역할을 합니다.
예시 3: 팀 규모에 따른 선택 기준
실무에서 자주 맞닥뜨리는 질문이 "언제 마이크로서비스로 가야 하냐"입니다. 저는 아래 기준을 체크리스트처럼 씁니다:
- 팀 규모 10명 미만 → 모듈러 모놀리스. 운영할 사람이 없습니다.
- 팀 규모 10~50명 → 모듈러 모놀리스로 시작하고, 병목이 생기는 모듈만 분리하는 방식을 권장합니다.
- 팀 규모 50명 초과 + 서비스별 독립 스케일링 필요 → 마이크로서비스를 본격적으로 고려해볼 수 있습니다.
- 스타트업 / MVP 단계 → 반드시 모듈러 모놀리스로 시작하는 것을 권장합니다. 도메인 경계가 명확하지 않은 초기에 서비스를 분리하면, 잘못된 경계를 나중에 수정하는 비용이 어마어마합니다.
- HIPAA, PCI 등 컴플라이언스 격리 요건 → 마이크로서비스가 사실상 필수인 상황입니다.
Martin Fowler가 오래전부터 강조해온 "Monolith First" 원칙이 지금 다시 주목받는 건, 잘못된 경계로 분리된 마이크로서비스를 재설계하는 고통을 경험한 팀들이 많아졌기 때문입니다.
장단점 분석
장점
지금까지 본 사례들로 어느 정도 감이 잡히셨겠지만, 두 아키텍처의 강점을 압축하면 이렇습니다.
모듈러 모놀리스
| 항목 | 내용 |
|---|---|
| 배포 단순성 | 단일 아티팩트, 하나의 배포 파이프라인 |
| 레이턴시 제로 | 인프로세스 호출로 네트워크 왕복 없음 |
| 데이터 정합성 | 단일 DB 트랜잭션으로 분산 트랜잭션 불필요 |
| 낮은 운영 비용 | 서비스 디스커버리, 분산 추적 불필요 |
| 점진적 전환 가능 | 나중에 특정 모듈만 서비스로 분리 가능 |
마이크로서비스
| 항목 | 내용 |
|---|---|
| 독립 스케일링 | 트래픽이 몰리는 서비스만 리소스 추가 |
| 빠른 릴리스 | 특정 서비스만 독립 배포 가능 |
| 기술 자유도 | 서비스별로 언어·프레임워크 선택 가능 |
| 장애 격리 | Circuit Breaker 적용 시 장애 전파 차단 |
| 팀 자율성 | 대규모 분산 팀이 서로 간섭 없이 개발 가능 |
단점 및 주의사항
솔직히 이 부분이 더 중요한 것 같습니다. 장점은 알고 도입하는데, 단점을 몰라서 뒤늦게 후회하는 경우가 더 많으니까요.
모듈러 모놀리스
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 경계 붕괴 위험 | 강제 메커니즘 없으면 Big Ball of Mud로 전락 | ArchUnit, NetArchTest, Packwerk로 경계 자동 검증 |
| 전체 단위 스케일링 | 특정 기능만 늘릴 수 없음 | 병목 모듈은 별도 서비스로 분리 |
| 빌드 시간 증가 | 대규모 팀에서 CI 병목 발생 | 모듈 단위 캐시, 증분 빌드 도입 |
마이크로서비스
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 네트워크 오버헤드 | 인터서비스 호출마다 레이턴시 발생 | gRPC, 메시지 큐로 최적화 |
| 분산 트랜잭션 | 여러 서비스에 걸친 트랜잭션 처리 복잡 | Saga 패턴, Outbox 패턴 적용 |
| 인프라 비용 폭증 | 동등 기능 대비 수 배 수준의 비용 발생 가능 | 그 비용이 정당화되는 규모인지 먼저 검증 |
| 디버깅 난이도 | 분산 로그 추적이 어려움 | OpenTelemetry + Jaeger/Zipkin 도입 |
| 플랫폼 팀 필요 | 최소 2~4명의 전담 인력 필요 | 그 인력이 없다면 도입 시기 재검토 |
Saga 패턴: 분산 트랜잭션을 여러 개의 로컬 트랜잭션과 보상 트랜잭션으로 쪼개서 처리하는 패턴입니다. 마이크로서비스에서 데이터 정합성을 맞출 때 쓰는 표준 접근법이지만, 구현과 디버깅이 만만치 않습니다.
Circuit Breaker: 연결된 서비스가 실패하기 시작하면 자동으로 요청을 차단해 장애가 연쇄적으로 퍼지는 것을 막는 패턴입니다. Resilience4j(Java), Polly(.NET)가 대표적인 구현체입니다.
실무에서 가장 흔한 실수
-
도메인 경계가 불분명한 상태에서 마이크로서비스 시작: 나중에 서비스 경계를 재조정하는 비용이 처음 설계보다 훨씬 큽니다. 모듈러 모놀리스에서 경계를 먼저 검증하고 분리하는 방식을 권장합니다.
-
모듈러 모놀리스에서 경계 강제 도구를 사용하지 않음: 아키텍처 테스트(ArchUnit, Packwerk 등) 없이 팀 규약만으로는 경계가 결국 무너집니다. 6개월 후에 Big Ball of Mud가 되어 있을 가능성이 높습니다.
-
팀 규모와 운영 인력을 고려하지 않은 마이크로서비스 도입: Netflix의 마이크로서비스 전략은 수백 명의 엔지니어와 수억 달러 규모의 인프라를 전제로 한 이야기입니다. 그 맥락을 빼고 아키텍처만 따라 하면 운영 부채만 쌓입니다.
마치며
그래서 지금 우리 팀에게 필요한 첫 번째 질문은 무엇일까요? 저는 이렇게 생각합니다: "지금 우리에게 전담 플랫폼 엔지니어 2명을 확보할 여유가 있는가?" 이 질문 하나에 "아니오"가 나오면, 나머지 선택은 거의 정해집니다.
42%가 마이크로서비스에서 돌아오고 있다는 수치는 마이크로서비스가 틀렸다는 뜻이 아닙니다. 준비되지 않은 상태에서, 혹은 맞지 않는 문제에 적용했다는 뜻이죠. 모듈러 모놀리스는 그 출발점으로 훌륭한 선택이고, 도메인 경계를 잘 잡아두면 나중에 필요한 부분만 서비스로 분리하는 경로도 자연스럽게 열립니다.
지금 바로 시작해볼 수 있는 3단계:
-
의존 관계 시각화: 언어에 상관없이 현재 코드베이스의 모듈 간 의존 관계를 먼저 그려보는 것을 권장합니다. 무엇이 어디에 얽혀 있는지 파악하는 것이 첫걸음입니다.
- Java:
ApplicationModules.of(App.class).verify()(Spring Modulith) - .NET: kgrzybek/modular-monolith-with-ddd 레퍼런스 구현 참고
- Python:
import-linter, Node.js:dependency-cruiser, Ruby:packwerk
- Java:
-
팀 규모 체크리스트 적용: 예시 3의 선택 기준에서 현재 팀 상황에 해당하는 항목을 확인해 보시면 됩니다. 특히 "전담 플랫폼 엔지니어 2~4명"이 없다면 마이크로서비스 도입을 서두르지 않는 것을 권장합니다.
-
아키텍처 경계 테스트 추가: 모듈러 모놀리스를 유지할 계획이라면, CI 파이프라인에 경계 위반 자동 감지 테스트를 추가해 보시면 됩니다. 이 테스트 하나가 6개월 후의 스파게티를 막아줍니다.
- Java: ArchUnit, .NET: NetArchTest, Python: import-linter, Node.js: dependency-cruiser
참고 자료
- Rethinking Microservices in 2026: When Modular Monolith Architecture Actually Win | Enqcode
- Modular Monolith: 42% Ditch Microservices in 2026 | byteiota
- Why Teams Are Moving Back From Microservices to Modular Monoliths in 2026 | Medium
- What Is a Modular Monolith? | Milan Jovanović
- Modular Monolith: A Primer | Kamil Grzybek
- Is the Modular Monolith Shopify's Best-kept Secret to Scaling? | Educative
- Monolith vs Microservices 2025: When Amazon Cuts Costs 90% | byteiota
- Microservice Trade-Offs | Martin Fowler
- Monolith vs Microservices vs Modular Monoliths | ByteByteGo
- Spring Modulith with DDD | GitHub
- Modular Monolith with DDD (.NET) | GitHub
- Crafting a self-documenting Modular Monolith with DDD | Spring I/O 2025