
모놀리식 vs 마이크로서비스: 아키텍처 전쟁의 끝은? (완벽 가이드)
현대 소프트웨어 아키텍처 가이드. DDD, 콘웨이의 법칙, SAGA 패턴, CQRS, 모듈러 모놀리스, 그리고 실제 마이그레이션 전략(Strangler Fig)까지.

현대 소프트웨어 아키텍처 가이드. DDD, 콘웨이의 법칙, SAGA 패턴, CQRS, 모듈러 모놀리스, 그리고 실제 마이그레이션 전략(Strangler Fig)까지.
로버트 C. 마틴(Uncle Bob)이 제안한 클린 아키텍처의 핵심은 무엇일까요? 양파 껍질 같은 계층 구조와 의존성 규칙(Dependency Rule)을 통해 프레임워크와 UI로부터 독립적인 소프트웨어를 만드는 방법을 정리합니다.

왜 CPU는 빠른데 컴퓨터는 느릴까? 80년 전 고안된 폰 노이만 구조의 혁명적인 아이디어와, 그것이 남긴 치명적인 병목현상에 대해 정리했습니다.

프링글스 통(Stack)과 맛집 대기 줄(Queue). 가장 기초적인 자료구조지만, 이걸 모르면 재귀 함수도 메시지 큐도 이해할 수 없습니다.

ChatGPT는 질문에 답하지만, AI Agent는 스스로 계획하고 도구를 사용해 작업을 완료한다. 이 차이가 왜 중요한지 정리했다.

프로젝트가 커지면 코드 관리가 어려워진다. 처음엔 하나의 앱에 모든 기능을 담아서 시작하는데, 기능이 늘어날수록 빌드 시간이 길어지고, 여러 사람이 같은 코드를 고치다 보면 충돌이 잦아진다. 이 문제를 어떻게 풀었는지 찾아보다가 Monolithic과 Microservices라는 두 가지 아키텍처를 접하게 됐다.
쿠팡, 배달의민족, 넷플릭스. 모두가 마이크로서비스를 쓴다고 했다. "이게 정답이구나" 싶었는데, 막상 파고들어 보니 그렇게 단순하지 않았다.
MSA로 가면 서비스를 여러 개로 나눠야 하고, Docker Compose 파일이 길어지고, 로그인 하나 테스트하려면 Redis, RabbitMQ, PostgreSQL을 따로 띄워야 한다. 로그는 흩어지고, 배포 파이프라인도 서비스마다 관리해야 한다.
그러던 중 아마존 프라임 비디오 팀의 블로그 글을 접했다.
"We moved our service from Microservices to a Monolith and reduced costs by 90%."
의외였다. 아마존이 모놀리식으로 돌아갔다고?
결국 이렇게 이해했습니다. 아키텍처는 정답이 없다. 트레이드오프만 있을 뿐이다. 그리고 대부분의 경우, 모놀리식이 정답이다.
"모놀리식 = 레거시 = 나쁜 것"이라고 생각했습니다. 완전히 틀렸습니다.
모놀리식은 하나의 배포 단위(Deployment Unit)입니다. Spring Boot로 치면 .jar 파일 하나, Node.js로 치면 node index.js 하나. 모든 코드가 한 프로젝트에 있고, 한 프로세스에서 돌아갑니다.
배포가 쉽습니다. .jar 파일 하나를 EC2에 던지면 끝입니다. Kubernetes 따위 필요 없습니다.
# 모놀리식 배포
java -jar app.jar
# MSA 배포 (20개 서비스)
kubectl apply -f user-service.yaml
kubectl apply -f order-service.yaml
kubectl apply -f payment-service.yaml
# ... 17개 더
디버깅이 쉽습니다. IntelliJ에서 브레이크 포인트 하나 찍으면 요청 전체 흐름이 다 보입니다. 로그도 하나의 파일에 다 모입니다.
성능이 빠릅니다. 함수 호출은 나노초(ns) 단위입니다. 네트워크를 안 타니까요.
// 모놀리식: 함수 호출 (나노초)
User user = userService.getUser(userId);
// MSA: HTTP 호출 (밀리초)
User user = restTemplate.getForObject("http://user-service/users/" + userId, User.class);
100배 이상 차이입니다.
트랜잭션이 간단합니다. ACID를 100% 보장합니다. 주문 생성과 재고 차감을 하나의 트랜잭션으로 묶을 수 있습니다.
@Transactional
public void createOrder(OrderRequest req) {
orderRepository.save(new Order(req));
inventoryRepository.decreaseStock(req.getProductId(), req.getQuantity());
// 둘 다 성공하거나, 둘 다 롤백됩니다
}
MSA에서는 이게 불가능합니다.
처음엔 장점만 보입니다. 하지만 코드베이스가 커지면 문제가 터지기 시작합니다.
빌드 시간이 지옥입니다. Gradle 빌드가 15분. 작은 수정 하나 반영하려면 15분을 기다려야 합니다. 개발자 생산성이 바닥을 칩니다.
기술 스택이 고정됩니다. 10년 전 Java 6로 짰으면, 지금도 Java 6를 써야 합니다. Node.js로 바꾸려면 전체를 다 엎어야 합니다. 리팩토링은 꿈도 꾸지 마세요.
부분 장애가 전체를 죽입니다. 이미지 리사이징 기능에서 메모리 누수가 발생하면, 결제 기능까지 같이 죽습니다. 운영체제가 프로세스를 죽이니까요.
확장이 비효율적입니다. CPU를 많이 쓰는 건 '이미지 처리' 뿐인데, 메모리만 많이 쓰는 '채팅 서버'까지 같이 증설해야 합니다.
모놀리식 서버 1대 = 4 CPU + 16GB RAM
이미지 처리 때문에 CPU가 부족? 서버 전체를 2대로 늘려야 함.
-> 32GB RAM을 확보했지만 16GB는 놀고 있음 (비효율)
MSA는 이론상 완벽했습니다. 각 서비스가 독립적으로 배포되고, 장애가 격리되고, 기술 스택을 자유롭게 선택할 수 있다.
하지만 현실은 달랐습니다.
MSA의 진짜 장점은 기술이 아니라 조직(Organization)에 있습니다. 이걸 받아들이는 데 시간이 좀 걸렸습니다.
배포 독립성: 주문 팀은 새벽 2시에 배포하고, 정산 팀은 오후 2시에 배포해도 됩니다. 서로 기다릴 필요가 없습니다. Time-to-Market이 줄어듭니다.
장애 격리: 추천 서버가 터져도, 상품 구매는 잘 됩니다. Circuit Breaker 패턴을 쓰면 장애가 전파되지 않습니다.
// 추천 서비스 장애 시 폴백
async function getRecommendations(userId) {
try {
const res = await axios.get(`http://recommendation-service/users/${userId}/recommendations`);
return res.data;
} catch (error) {
console.error('Recommendation service down, returning default');
return DEFAULT_RECOMMENDATIONS; // 기본 추천 목록 반환
}
}
조직 확장성: 개발자가 500명일 때, 50명씩 10개 팀으로 쪼개서 일하기 좋습니다. 책임 소재가 명확해집니다.
기술 다양성: 검색은 Elasticsearch, AI는 Python, 백엔드는 Go. 각자 최적의 도구를 쓸 수 있습니다.
MSA를 하면 코드 복잡도가 줄어든다고 생각했습니다. 틀렸습니다.
복잡도는 사라지지 않습니다. 다른 곳으로 이동할 뿐입니다.
분산 데이터 관리: Join이 불가능합니다. 주문 데이터를 가져오려면 User Service에서 사용자 정보를 긁어오고, Product Service에서 상품 정보를 긁어와서 앱에서 합쳐야 합니다.
// MSA에서의 데이터 조합 (Application-side Join)
async function getOrderDetail(orderId) {
const order = await orderService.getOrder(orderId);
const user = await userService.getUser(order.userId);
const product = await productService.getProduct(order.productId);
return {
...order,
userName: user.name,
productName: product.name
};
}
3번의 HTTP 호출입니다. 모놀리식이라면 SQL JOIN 하나로 끝날 일입니다.
운영 복잡도: 서비스를 여러 개로 나누면 모니터링 대시보드도 그만큼 늘어납니다. 로그가 흩어져 있어서 문제 찾기가 지옥입니다. Distributed Tracing(Zipkin, Jaeger) 없이는 불가능합니다.
네트워크 비용: 단순한 데이터 조회도 HTTP 통신을 해야 하므로 느립니다. JSON 직렬화/역직렬화 비용이 발생합니다.
결국 이런 생각이 들었습니다. "이거 필요한가?"
MSA 책을 10권 읽어도 와닿지 않던 개념이 하나 있었습니다. 콘웨이의 법칙(Conway's Law)입니다.
"시스템 구조는 그 시스템을 만든 조직의 커뮤니케이션 구조를 닮는다."
처음엔 무슨 말인지 몰랐습니다. 그런데 직접 경험하고 나서 와닿았습니다.
팀원 5명일 때: 모놀리식이 정답입니다. 의자 돌려서 말하면 되는데 API 명세서 쓰고 있으면 시간 낭비입니다. 데이터베이스 테이블 하나 수정하는데 5개 팀 동의 받을 필요가 없습니다.
팀원 100명일 때: 모놀리식을 하면 커밋 충돌(Conflict)이 매일 나고, 배포 줄을 서느라 하루가 다 갑니다. 이때 MSA를 도입해서 10개 팀으로 쪼개야 합니다.
저는 5명이었는데 MSA를 했습니다. 완전히 잘못된 선택이었습니다.
MSA를 하려면 도메인 경계를 잘 나눠야 합니다. 이게 DDD입니다.
Bounded Context: '상품'이라는 단어는 주문 팀에서는 '가격/재고'를 의미하지만, 배송 팀에서는 '무게/부피'를 의미합니다. 같은 단어지만 다른 의미입니다.
각 서비스는 자신만의 데이터베이스를 가져야 합니다. Database per Service 원칙입니다.
# 잘못된 MSA (Shared Database - Anti-Pattern)
services:
order-service:
database: shared_db
payment-service:
database: shared_db # ❌ 같은 DB 공유
# 올바른 MSA
services:
order-service:
database: order_db
payment-service:
database: payment_db # ✅ 각자 DB
데이터베이스를 공유하는 순간 MSA가 아닙니다. 그건 "분산된 모놀리스(Distributed Monolith)"라는 최악의 괴물입니다.
모놀리식의 단점(느린 빌드, 배포 의존성)은 그대로인데, MSA의 단점(네트워크 오버헤드, 운영 복잡도)만 추가됩니다. 절대 하지 마세요.
MSA를 도입하는 순간 ACID 트랜잭션을 잃게 됩니다. 이건 정말 큰 문제입니다.
모놀리식에서는 이랬습니다:
@Transactional
public void createOrder(OrderRequest req) {
Order order = orderRepository.save(new Order(req));
inventoryRepository.decreaseStock(req.getProductId(), req.getQuantity());
paymentService.charge(req.getUserId(), req.getAmount());
// 하나라도 실패하면 전체 롤백
}
MSA에서는 불가능합니다. Order Service, Inventory Service, Payment Service가 다 다른 데이터베이스를 씁니다.
Saga 패턴은 분산 트랜잭션을 구현하는 방법입니다. 두 가지 방식이 있습니다.
1) Choreography (안무): 서비스끼리 이벤트를 주고받으며 자율적으로 처리합니다.
1. Order Service: 주문 생성 → OrderCreated 이벤트 발행
2. Payment Service: OrderCreated 이벤트 수신 → 결제 → PaymentSuccess 이벤트 발행
3. Inventory Service: PaymentSuccess 이벤트 수신 → 재고 차감 → StockReserved 이벤트 발행
장점: 중앙 지휘자가 없어서 단순합니다. 단점: 이벤트가 복잡하게 얽히면 추적이 불가능합니다.
2) Orchestration (지휘): 중앙 지휘자(Orchestrator)가 순서를 제어합니다.
// Order Orchestrator (중앙 제어)
async function createOrderSaga(orderRequest) {
try {
// Step 1: 주문 생성
const order = await orderService.createOrder(orderRequest);
// Step 2: 결제
const payment = await paymentService.charge(orderRequest.userId, orderRequest.amount);
// Step 3: 재고 차감
await inventoryService.decreaseStock(orderRequest.productId, orderRequest.quantity);
return order;
} catch (error) {
// 보상 트랜잭션 (Compensating Transaction)
if (payment) await paymentService.refund(payment.id);
if (order) await orderService.cancel(order.id);
throw error;
}
}
장점: 흐름을 추적하기 쉽습니다. 단점: Orchestrator가 SPOF(Single Point of Failure)가 됩니다.
중요한 건 보상 트랜잭션(Compensating Transaction)입니다. 재고가 부족하면 결제 취소 API를 호출해야 합니다.
CQRS(Command Query Responsibility Segregation)는 명령(Write)과 조회(Read)를 분리합니다.
Write: Order 생성 → PostgreSQL 저장 → Kafka 메시지 발행
Read: Kafka 메시지 수신 → Elasticsearch에 저장 (주문 목록 조회용)
주문 생성은 PostgreSQL에서, 주문 목록 조회는 Elasticsearch에서. 각자 최적화됩니다.
단점: 데이터 정합성이 즉시 보장되지 않습니다(Eventual Consistency). 주문 생성 직후 목록을 조회하면 안 보일 수 있습니다.
서비스가 많아지면 서비스 간 통신을 관리하는 게 지옥이 됩니다. 이때 Service Mesh(Istio, Linkerd)를 씁니다.
기능:코드를 수정하지 않고도 이 모든 기능을 적용할 수 있습니다. Sidecar 패턴으로 각 컨테이너 옆에 Proxy를 붙입니다.
# Istio를 쓰면 코드 수정 없이 Circuit Breaker 적용
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: payment-service
spec:
host: payment-service
trafficPolicy:
outlierDetection:
consecutiveErrors: 5
interval: 30s
baseEjectionTime: 30s
하지만 러닝 커브가 가파릅니다. Istio 문서는 성경보다 두껍습니다.
"기존 모놀리식을 한 번에 MSA로 바꾸자!" 이건 100% 실패합니다.
Big Bang 마이그레이션은 자살 행위입니다.
Martin Fowler가 제안한 Strangler Fig 패턴이 정답입니다. "교살 무화과나무"는 기생 식물입니다. 호스트 나무를 감싸며 자라다가 결국 호스트를 죽이고 자기가 나무가 됩니다.
전략:[Before]
클라이언트 → 모놀리식 (100% 트래픽)
[During Migration]
클라이언트 → API Gateway → 모놀리식 (70% 트래픽)
→ 새 서비스 (30% 트래픽)
[After]
클라이언트 → API Gateway → 새 서비스 (100% 트래픽)
이 방식의 장점은 리스크가 적다는 겁니다. 새 서비스가 터져도 트래픽을 다시 모놀리식으로 돌리면 됩니다.
많은 현자들이 깨달았습니다. "물리적으로 서버를 쪼개는 건 비용이 너무 크다."
그래서 논리적으로만 쪼개는 방식이 뜹니다. 모듈러 모놀리스(Modular Monolith)입니다.
원칙:모놀리식 프로젝트 구조:
src/
├── order/ (Order Bounded Context)
│ ├── OrderService.java
│ ├── OrderRepository.java
├── payment/ (Payment Bounded Context)
│ ├── PaymentService.java
│ ├── PaymentRepository.java
└── inventory/ (Inventory Bounded Context)
├── InventoryService.java
├── InventoryRepository.java
규칙: order 패키지는 payment 패키지의 내부 클래스를 직접 호출할 수 없음
-> 인터페이스를 통해서만 통신 (나중에 HTTP로 바꾸기 쉬움)
이것이 초기 스타트업에게 가장 권장되는 아키텍처입니다. Shopify, 배달의민족 초기가 이 방식이었습니다.
여러 사례와 문서를 파고들며 정리해본다면 이렇습니다:
1. Start Monolithic: 처음엔 무조건 하나로 시작하세요. MSA는 문제가 생기고 나서 도입하세요.
2. Split only when it hurts: 배포 대기 시간이 너무 길어지거나, 팀이 너무 커졌을 때 쪼개세요. 미리 쪼개지 마세요.
3. Observability First: 로그 통합(ELK), 모니터링(Prometheus), Distributed Tracing(Jaeger) 없이 쪼개면 장님 코끼리 만지기가 됩니다.
4. Database Separation: DB를 쪼개지 않을 거면 절대로 MSA를 하지 마세요. 분산된 모놀리스는 최악입니다.
5. Conway's Law: 조직 구조를 먼저 보세요. 5명이면 모놀리식, 100명이면 MSA.
6. Modular Monolith: 대부분의 경우 이게 답입니다.
저는 이제 확신합니다. 모놀리식이 나쁜 게 아닙니다. 잘못 쓰는 게 나쁜 겁니다.
As a project grows, managing the codebase gets harder. You start with everything in one app, but as features pile up, build times stretch longer and working in the same codebase with others leads to constant conflicts. Looking for answers to this problem, I came across two architectural approaches: Monolithic and Microservices.
Coupang, Netflix, Uber — everyone seemed to be on microservices. "This must be the answer," I thought. But the deeper I looked, the less straightforward it became.
Going MSA means splitting into multiple services, writing longer Docker Compose files, and spinning up Redis, RabbitMQ, and PostgreSQL separately just to test a login flow. Logs scatter across services. Every service needs its own deployment pipeline.
Then I came across the Amazon Prime Video blog post.
"We moved our service from Microservices to a Monolith and reduced costs by 90%."
Unexpected. Amazon went back to monolith?
That's when it clicked. There's no perfect architecture. Only trade-offs. And most of the time, monolith is the right answer.
I thought "monolith = legacy = bad." Completely wrong.
A monolith is just one deployment unit. One .jar file for Spring Boot, one node index.js for Node. All code in one project, one process.
Deployment is trivial. Throw one .jar file at EC2. Done. No Kubernetes needed.
# Monolith deployment
java -jar app.jar
# MSA deployment (20 services)
kubectl apply -f user-service.yaml
kubectl apply -f order-service.yaml
kubectl apply -f payment-service.yaml
# ... 17 more to go
Debugging is straightforward. One breakpoint in IntelliJ shows the entire request flow. All logs in one file.
Performance is fast. Function calls are nanoseconds. No network overhead.
// Monolith: function call (nanoseconds)
User user = userService.getUser(userId);
// MSA: HTTP call (milliseconds)
User user = restTemplate.getForObject("http://user-service/users/" + userId, User.class);
100x difference.
Transactions are simple. Full ACID guarantees. Order creation and inventory deduction in one transaction.
@Transactional
public void createOrder(OrderRequest req) {
orderRepository.save(new Order(req));
inventoryRepository.decreaseStock(req.getProductId(), req.getQuantity());
// Both succeed or both rollback
}
Impossible in MSA.
Everything looks great at first. But as the codebase grows, problems start to surface.
Build time became torture. 15-minute Gradle builds. Every small change meant waiting 15 minutes. Developer productivity tanked.
Tech stack got locked. Started with Java 6 ten years ago? Still using Java 6. Want Node.js? Rewrite everything. Refactoring? Forget it.
Partial failures killed everything. Memory leak in image resizing? Payment system goes down too. The OS kills the process.
Scaling was wasteful. Only image processing needed CPU, but I had to scale the entire app, wasting RAM on chat functionality that barely used any.
Monolith server = 4 CPU + 16GB RAM
Need more CPU for image processing? Scale to 2 servers.
-> Got 32GB RAM total, but 16GB sits idle (wasteful)
MSA looked perfect in theory. Independent deployments, fault isolation, technology freedom.
Reality was different.
MSA's true strength isn't technical—it's organizational. Took me a while to accept this.
Deployment independence: Order team deploys at 2 AM, Finance team at 2 PM. No waiting. Faster time-to-market.
Fault isolation: Recommendation service crashes? Shopping still works. Circuit breaker prevents cascade failures.
// Fallback when recommendation service fails
async function getRecommendations(userId) {
try {
const res = await axios.get(`http://recommendation-service/users/${userId}/recommendations`);
return res.data;
} catch (error) {
console.error('Recommendation service down, returning default');
return DEFAULT_RECOMMENDATIONS; // fallback to defaults
}
}
Team scaling: 500 developers? Split into 10 teams of 50. Clear ownership.
Technology diversity: Elasticsearch for search, Python for AI, Go for backend. Use the right tool for each job.
I thought MSA would reduce code complexity. Wrong.
Complexity doesn't disappear. It just moves elsewhere.
Distributed data management: No more JOINs. Need order details? Fetch from User Service, fetch from Product Service, merge in application code.
// Application-side JOIN in MSA
async function getOrderDetail(orderId) {
const order = await orderService.getOrder(orderId);
const user = await userService.getUser(order.userId);
const product = await productService.getProduct(order.productId);
return {
...order,
userName: user.name,
productName: product.name
};
}
Three HTTP calls. Would've been one SQL JOIN in a monolith.
Operational complexity: Splitting into many services means a monitoring dashboard per service. Logs scatter everywhere. Debugging hell without distributed tracing (Zipkin, Jaeger).
Network overhead: Simple data fetch requires HTTP calls. JSON serialization costs add up.
I started wondering, "Is this worth it?"
Read 10 MSA books but one concept never clicked. Conway's Law.
"Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations."
Made no sense at first. Then I lived it.
5-person team: Monolith is the answer. We sit next to each other. Writing API specs is wasted time. Changing a database table doesn't need 5 teams to approve.
100-person team: Monolith means daily Git conflicts and waiting in line to deploy. Time to split into 10 teams with MSA.
I had 5 people and did MSA. Completely wrong choice.
To do MSA right, you need clear domain boundaries. That's DDD (Domain-Driven Design).
Bounded Context: "Product" means different things to different teams. Order team cares about price and inventory. Shipping team cares about weight and volume. Same word, different meanings.
Each service must own its database. Database per Service principle.
# Wrong MSA (Shared Database - Anti-Pattern)
services:
order-service:
database: shared_db
payment-service:
database: shared_db # ❌ sharing DB
# Correct MSA
services:
order-service:
database: order_db
payment-service:
database: payment_db # ✅ own DB
Shared database kills MSA. That's Distributed Monolith—the worst of both worlds.
You get monolith's downsides (slow builds, deployment coupling) plus MSA's downsides (network overhead, operational complexity). Never do this.
Adopting MSA means losing ACID transactions. This was a real problem.
In monolith, it was simple:
@Transactional
public void createOrder(OrderRequest req) {
Order order = orderRepository.save(new Order(req));
inventoryRepository.decreaseStock(req.getProductId(), req.getQuantity());
paymentService.charge(req.getUserId(), req.getAmount());
// Any failure rolls back everything
}
MSA makes this impossible. Order Service, Inventory Service, Payment Service all use different databases.
Saga pattern enables distributed transactions. Two approaches exist.
1) Choreography: Services communicate via events autonomously.
1. Order Service: Create order → Publish OrderCreated event
2. Payment Service: Receive OrderCreated → Process payment → Publish PaymentSuccess
3. Inventory Service: Receive PaymentSuccess → Decrease stock → Publish StockReserved
Pros: No central coordinator, simple. Cons: Complex event chains become impossible to trace.
2) Orchestration: Central orchestrator controls the flow.
// Order Orchestrator (central control)
async function createOrderSaga(orderRequest) {
try {
// Step 1: Create order
const order = await orderService.createOrder(orderRequest);
// Step 2: Process payment
const payment = await paymentService.charge(orderRequest.userId, orderRequest.amount);
// Step 3: Decrease inventory
await inventoryService.decreaseStock(orderRequest.productId, orderRequest.quantity);
return order;
} catch (error) {
// Compensating Transaction
if (payment) await paymentService.refund(payment.id);
if (order) await orderService.cancel(order.id);
throw error;
}
}
Pros: Easy to trace flow. Cons: Orchestrator becomes SPOF (Single Point of Failure).
Key concept: Compensating Transactions. Out of stock? Call payment refund API.
CQRS (Command Query Responsibility Segregation) separates writes and reads.
Write: Create order → Save to PostgreSQL → Publish Kafka message
Read: Consume Kafka → Save to Elasticsearch (for order list queries)
Order creation hits PostgreSQL. Order list queries hit Elasticsearch. Each optimized for its purpose.
Downside: No immediate consistency (Eventual Consistency). Query order list right after creation? Might not show up yet.
Once services multiply, managing inter-service communication becomes hell. Enter Service Mesh (Istio, Linkerd).
Features:Apply all this without code changes. Sidecar pattern deploys a proxy next to each container.
# Istio applies circuit breaker without code changes
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: payment-service
spec:
host: payment-service
trafficPolicy:
outlierDetection:
consecutiveErrors: 5
interval: 30s
baseEjectionTime: 30s
But steep learning curve. Istio docs are thicker than the Bible.
"Let's rewrite the entire monolith to MSA at once!" This fails 100% of the time.
Big Bang migration is suicide.
Martin Fowler's Strangler Fig pattern is the answer. Strangler fig is a parasitic plant. It wraps around a host tree, grows, and eventually kills the host while becoming the tree itself.
Strategy:[Before]
Client → Monolith (100% traffic)
[During Migration]
Client → API Gateway → Monolith (70% traffic)
→ New Service (30% traffic)
[After]
Client → API Gateway → New Service (100% traffic)
Advantage: Low risk. New service crashes? Route traffic back to monolith.
Many wise developers realized: "Physical service splitting costs too much."
Solution: Split logically only. Enter Modular Monolith.
Principles:Monolith project structure:
src/
├── order/ (Order Bounded Context)
│ ├── OrderService.java
│ ├── OrderRepository.java
├── payment/ (Payment Bounded Context)
│ ├── PaymentService.java
│ ├── PaymentRepository.java
└── inventory/ (Inventory Bounded Context)
├── InventoryService.java
├── InventoryRepository.java
Rule: order package cannot directly call payment package internals
-> Communicate only through interfaces (easy to switch to HTTP later)
This is the recommended architecture for early-stage startups. Shopify and early Baedal Minjok used this approach.