설계Medium#49
마이크로서비스 아키텍처와 모놀리식 아키텍처의 장단점을 비교해주세요.
#설계#MSA#아키텍처#시스템설계
힌트
확장성, 복잡성, 팀 독립성, 운영 비용을 생각해보세요.
정답 및 해설
마이크로서비스 아키텍처와 모놀리식 아키텍처의 장단점을 비교해주세요.
소프트웨어 아키텍처를 설계할 때 가장 근본적인 결정 중 하나가 모놀리식(Monolithic)과 마이크로서비스(Microservices) 중 무엇을 선택할지입니다. 모놀리식은 하나의 단일 애플리케이션으로 모든 기능을 구성하는 방식이고, 마이크로서비스는 각 기능을 독립적으로 배포 가능한 작은 서비스들로 분리하는 방식입니다. 두 접근법 모두 장단점이 명확하여 팀 규모, 서비스 규모, 기술 성숙도에 따라 적합한 선택이 달라집니다.
모놀리식 아키텍처
구조
[모놀리식 애플리케이션]
┌─────────────────────────────────────────┐
│ 사용자 인터페이스 (UI Layer) │
├─────────────────────────────────────────┤
│ 비즈니스 로직 (Business Logic Layer) │
│ ├── 사용자 모듈 │
│ ├── 주문 모듈 │
│ ├── 결제 모듈 │
│ └── 알림 모듈 │
├─────────────────────────────────────────┤
│ 데이터 접근 (Data Access Layer) │
├─────────────────────────────────────────┤
│ 단일 데이터베이스 │
└─────────────────────────────────────────┘
↕ (단일 프로세스로 실행)
[단일 배포 단위]
장점
1. 단순한 개발 환경
- 단일 IDE/프로젝트에서 전체 코드 파악
- 로컬 실행이 간단 (npm start 하나로)
- 디버깅이 쉬움 (하나의 프로세스 추적)
2. 단순한 배포
- 하나의 빌드, 하나의 배포 단위
- CI/CD 파이프라인 구성이 단순
3. 성능 이점
- 서비스 간 통신이 메서드 호출 (네트워크 오버헤드 없음)
- 트랜잭션 관리가 단순 (단일 DB)
4. 단순한 테스트
- 통합 테스트가 쉬움
- 서비스 간 mock 불필요
5. 팀 협업
- 소규모 팀에서 전체 코드를 파악하기 쉬움
단점
// 모놀리식의 문제점 예시
// 하나의 기능 변경이 전체에 영향을 미침
// 결제 모듈의 버그가 전체 서비스 다운으로 이어질 수 있음
class PaymentService {
processPayment(order: Order) {
// 이 함수에 버그가 생기면 전체 앱에 영향
throw new Error('결제 시스템 오류')
}
}
// 배포 시 전체 애플리케이션 재시작 필요
// 결제 코드 한 줄 수정해도 전체 서비스 배포 필요
단점:
1. 확장의 어려움
- 특정 기능만 스케일 아웃 불가
- 전체 앱을 복제해야 함 (메모리/CPU 낭비)
2. 기술 스택 고정
- 전체가 동일한 언어/프레임워크 사용 강제
- 새 기술 도입이 어려움
3. 장애 전파
- 한 모듈의 장애가 전체 서비스에 영향
4. 배포 위험성
- 작은 변경도 전체 배포 필요
- 배포 실패 시 전체 서비스 중단
5. 코드베이스 복잡도
- 시간이 지날수록 "빅볼 오브 머드" 현상
- 코드 의존성이 뒤엉킴
마이크로서비스 아키텍처
구조
[마이크로서비스 아키텍처]
클라이언트
↓
[API Gateway]
↓
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 사용자 │ │ 주문 │ │ 결제 │ │ 알림 │
│ 서비스 │ │ 서비스 │ │ 서비스 │ │ 서비스 │
│ │ │ │ │ │ │ │
│ [DB] │ │ [DB] │ │ [DB] │ │ [DB] │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
↕ ↕ ↕ ↕
[메시지 브로커 (Kafka/RabbitMQ)]
서비스 간 통신 방식
// 동기 통신 - REST API
// 주문 서비스에서 사용자 정보 조회
class OrderService {
async createOrder(userId: string, items: Item[]) {
// 사용자 서비스 HTTP 호출
const user = await fetch(`http://user-service/api/users/${userId}`)
.then(res => res.json())
// 결제 서비스 HTTP 호출
const payment = await fetch('http://payment-service/api/payments', {
method: 'POST',
body: JSON.stringify({ userId, amount: this.calculateTotal(items) })
}).then(res => res.json())
return { orderId: Date.now(), user, payment, items }
}
}
// 비동기 통신 - 메시지 브로커 (Kafka)
// 주문 완료 이벤트 발행
class OrderService {
async completeOrder(orderId: string) {
await this.orderRepository.complete(orderId)
// 이벤트 발행 - 다른 서비스가 구독
await this.kafka.publish('order.completed', {
orderId,
userId: order.userId,
amount: order.totalAmount,
timestamp: new Date()
})
}
}
// 알림 서비스에서 이벤트 구독
class NotificationService {
@KafkaSubscribe('order.completed')
async onOrderCompleted(event: OrderCompletedEvent) {
await this.emailSender.send(event.userId, '주문이 완료되었습니다.')
await this.smsSender.send(event.userId, '주문 완료')
}
}
장점
1. 독립적 배포/확장
- 각 서비스를 독립적으로 배포
- 주문 서비스만 스케일 아웃 가능 (비용 효율)
2. 기술 스택 자유
- 사용자 서비스: Node.js
- 결제 서비스: Java (안정성)
- 데이터 분석: Python
- 각 서비스에 최적화된 DB 선택 가능
3. 장애 격리
- 결제 서비스 장애 → 다른 서비스 정상 운영 가능 (Circuit Breaker 적용 시)
4. 팀 독립성
- 각 서비스를 담당하는 팀이 독립적으로 개발
- Conway's Law: 팀 구조 = 시스템 구조
5. 재사용성
- 인증 서비스를 여러 프로덕트에서 공유
단점
// 마이크로서비스의 복잡성 예시
// 분산 트랜잭션 관리 - Saga 패턴 필요
class OrderSaga {
async executeOrderCreation(order: Order) {
try {
// 1. 재고 서비스에서 재고 확인/감소
await this.inventoryService.reserve(order.items)
// 2. 결제 서비스에서 결제 처리
const payment = await this.paymentService.charge(order.amount)
// 3. 주문 생성
await this.orderRepository.create(order)
} catch (error) {
// 실패 시 보상 트랜잭션 (Compensating Transaction)
await this.inventoryService.release(order.items) // 재고 복구
await this.paymentService.refund(payment.id) // 결제 취소
throw error
}
}
}
// 서비스 간 네트워크 오류 처리 - Circuit Breaker
class PaymentServiceClient {
private circuitBreaker = new CircuitBreaker({
failureThreshold: 5, // 5번 실패 시 차단
resetTimeout: 60000, // 60초 후 재시도
})
async charge(amount: number) {
return this.circuitBreaker.execute(async () => {
const response = await fetch('http://payment-service/charge', {
method: 'POST',
body: JSON.stringify({ amount }),
signal: AbortSignal.timeout(3000) // 3초 타임아웃
})
if (!response.ok) throw new Error('결제 서비스 오류')
return response.json()
})
}
}
단점:
1. 분산 시스템 복잡성
- 네트워크 지연, 장애, 재시도 처리 필요
- 분산 트랜잭션 (Saga 패턴 등) 구현 복잡
2. 운영 인프라 비용
- 서비스 디스커버리, API Gateway, 메시지 브로커
- 각 서비스별 모니터링/로깅 (중앙화 필요)
- 컨테이너 오케스트레이션 (Kubernetes 등)
3. 개발 복잡도
- 로컬 환경에서 여러 서비스 실행 (Docker Compose 등)
- API 버전 관리 복잡
- 서비스 간 계약(Contract) 관리
4. 데이터 일관성
- 각 서비스가 별도 DB → 조인 쿼리 불가
- 최종 일관성(Eventual Consistency) 패턴 필요
마이크로서비스 필수 인프라
# Docker Compose로 로컬 마이크로서비스 환경
version: '3.8'
services:
api-gateway:
image: nginx
ports:
- "80:80"
user-service:
build: ./user-service
environment:
- DB_URL=postgres://user-db:5432/users
depends_on:
- user-db
order-service:
build: ./order-service
environment:
- DB_URL=mongo://order-db:27017/orders
- KAFKA_URL=kafka:9092
payment-service:
build: ./payment-service
environment:
- DB_URL=postgres://payment-db:5432/payments
kafka:
image: confluentinc/cp-kafka
ports:
- "9092:9092"
user-db:
image: postgres
order-db:
image: mongo
payment-db:
image: postgres
선택 기준
모놀리식을 선택해야 하는 경우:
✓ 스타트업 또는 MVP 단계
✓ 소규모 팀 (10명 이하)
✓ 도메인이 아직 불분명 (경계 찾기 어려움)
✓ 빠른 출시가 우선
✓ 팀에 DevOps/인프라 전문가 부족
마이크로서비스를 선택해야 하는 경우:
✓ 대규모 팀 (여러 독립 팀 운영)
✓ 트래픽이 서비스별로 크게 다름 (독립 확장 필요)
✓ 기술 다양성이 필요
✓ 높은 가용성이 필요 (장애 격리)
✓ DevOps 성숙도가 높음
마틴 파울러의 조언:
"마이크로서비스로 시작하지 말라. 모놀리스로 시작해서
시스템이 성숙하면 자연스럽게 분리하라."
(MonolithFirst 패턴)
중간 단계: 모듈러 모놀리스
// 모놀리스를 모듈로 잘 구조화하면 나중에 마이크로서비스로 분리 용이
// Next.js 풀스택 예시 - 도메인별 모듈 구조
// app/
// ├── (api)/
// │ ├── users/ → 나중에 user-service로 분리 가능
// │ ├── orders/ → 나중에 order-service로 분리 가능
// │ └── payments/ → 나중에 payment-service로 분리 가능
// ├── lib/
// │ ├── users/ (DB 접근, 비즈니스 로직)
// │ ├── orders/
// │ └── payments/
// └── components/
// 명확한 모듈 경계를 유지하면 마이크로서비스 전환이 쉬워짐
// lib/orders/service.ts
export class OrderService {
// 외부에서는 이 서비스만 사용 (내부 구현 은닉)
async createOrder(data: CreateOrderDto): Promise<Order> { /* ... */ }
}
// 다른 모듈에서 직접 DB 접근 금지 - 서비스를 통해서만
// ❌ import { db } from '@/lib/orders/db' // 내부 구현 직접 접근
// ✅ import { OrderService } from '@/lib/orders/service' // 공개 인터페이스만
정리 표
| 구분 | 모놀리식 | 마이크로서비스 |
|---|---|---|
| 개발 복잡도 | 낮음 | 높음 |
| 배포 단위 | 전체 애플리케이션 | 각 서비스 독립 배포 |
| 확장성 | 전체 복제만 가능 | 서비스별 독립 확장 |
| 장애 격리 | 어려움 (장애 전파) | 가능 (Circuit Breaker) |
| 기술 스택 | 단일 | 서비스별 자유 선택 |
| 데이터 관리 | 단일 DB, 쉬운 트랜잭션 | 분산 DB, 복잡한 트랜잭션 |
| 운영 인프라 | 단순 | 복잡 (쿠버네티스 등 필요) |
| 팀 구조 | 단일 팀에 적합 | 다수 독립 팀에 적합 |
| 적합한 단계 | 초기/소규모 | 성장기/대규모 |
| 로컬 개발 | 단순 | 복잡 (Docker Compose 등) |