당신은 마이크로서비스가 필요하지 않다
“마이크로서비스로 갈 거야, 모놀리스로 갈 거야?”
새 프로젝트를 시작한다고 하면, 어김없이 이런 질문이 따라오곤 했다. 마치 모놀리스는 레거시이고, 마이크로서비스는 모던한 것처럼.
하지만 마이크로서비스는 아키텍처 스타일일 뿐, 진화의 방향이 아니다.
마이크로서비스의 숨겨진 비용¶
마이크로서비스의 장점은 이제는 충분히 잘 알려져 있다. 독립 배포, 기술 스택 자유, 팀 자율성. 하지만 이 장점이 실현되려면 전제 조건이 있다. 서비스가 충분히 많고, 팀이 충분히 크고, 트래픽이 충분히 다양해야 한다.1
그 전제가 없는 상태에서 마이크로서비스를 도입하면, 비용이 장점보다 커지게 된다.
| 문제 | 모놀리스 | 마이크로서비스 |
|---|---|---|
| 함수 호출 | 나노초 | 네트워크 왕복 (밀리초~) |
| 트랜잭션 | DB 트랜잭션 한 번 | Saga 패턴, 보상 트랜잭션, 이벤트 큐 |
| 디버깅 | 스택 트레이스 | 분산 트레이싱 (Jaeger, Zipkin) |
| 배포 | 바이너리 하나 | 서비스 N개의 버전 호환성 관리 |
| 로컬 개발 환경 | 단일 커맨드 | Docker Compose + 서비스 N개 띄우기 |
| 데이터 정합성 | JOIN 한 번 | API 호출 체이닝, 이벤트 소싱 |
서비스 3개를 운영하는 팀이 Saga 패턴을 구현하고, 분산 트레이싱을 붙이고, 서비스 간 API 버전을 관리하는 건 문제를 해결하는 게 아니라 문제를 만드는 것이다.
모듈러 모놀리스¶
모듈러 모놀리스는 코드의 경계는 마이크로서비스처럼 나누되, 배포는 하나로 유지하는 구조다.
핵심은 간단하다.
- 모듈 간 직접 import 금지
- 모듈 간 통신은 명시적 인터페이스를 통해서만
- 데이터베이스 스키마도 모듈별로 분리
배포 단위만 하나일 뿐, 내부는 이미 분리되어 있다. 나중에 특정 모듈을 떼어내야 할 때, 네트워크 호출로 바꾸기만 하면 된다.
프로젝트 구조¶
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── user/
│ │ ├── handler.go
│ │ ├── service.go
│ │ ├── repository.go
│ │ ├── model.go
│ │ └── event.go
│ ├── order/
│ │ ├── handler.go
│ │ ├── service.go
│ │ ├── repository.go
│ │ ├── model.go
│ │ └── event.go
│ ├── payment/
│ │ ├── handler.go
│ │ ├── service.go
│ │ ├── repository.go
│ │ ├── model.go
│ │ └── event.go
│ └── platform/
│ ├── eventbus/
│ ├── database/
│ └── config/
├── go.mod
└── go.sum
각 모듈은 자신만의 handler, service, repository, model을 가진다.
internal/order가 internal/user를 직접 import하는 일은 없다.
모듈 간 통신¶
모듈러 모놀리스의 가장 큰 이점은 같은 프로세스, 같은 데이터베이스라는 것이다. 마이크로서비스에서는 불가능한 크로스 모듈 트랜잭션 때문에 머리를 싸맬 필요가 없다.
| |
주문 생성과 결제 생성이 하나의 트랜잭션으로 묶인다.
마이크로서비스였다면 Saga 패턴, 보상 트랜잭션, 이벤트 큐가 필요했을 것이다.
여기서는 tx.Commit() 한 번이면 된다.
모듈 간 호출은 인터페이스를 통해 이루어진다.
| |
order 모듈은 payment 패키지를 직접 import하지 않는다.
인터페이스만 알고, 구현체는 main.go에서 주입한다.
정합성이 필요 없는 후속 작업(알림 발송, 로그 기록 등)은 이벤트로 처리한다.
| |
| |
트랜잭션이 필요한 곳은 트랜잭션으로, 느슨한 결합이 필요한 곳은 이벤트로. 이 구분이 모듈러 모놀리스의 핵심이다.
마이크로서비스가 정말 필요한 순간¶
모듈러 모놀리스에도 한계는 있다.
- 독립적 스케일링 — 특정 모듈만 10배 트래픽을 받는다면, 전체를 스케일링하는 건 낭비다.
- 기술 스택 다양성 — 모듈마다 적합한 언어가 다른 경우.
- 팀 독립성 — 10개 이상의 팀이 하나의 저장소에서 배포 일정을 조율하는 건 병목이다.
- 장애 격리 — 결제 모듈의 메모리 누수가 전체 서비스를 죽이면 안 되는 경우.
하지만 이 요구사항들이 실제로 발생하는 시점은, 역시 생각보다 훨씬 늦다.
| MSA가 필요한 조건 | 모듈러 모놀리스로 충분한 경우 |
|---|---|
| 팀이 10개 이상 | 팀이 1~3개 |
| 모듈별 트래픽 편차가 극심 | 전체 트래픽이 균일 |
| 서로 다른 언어/런타임 필요 | 단일 기술 스택 |
| 서비스별 독립 SLA | 전체 SLA가 동일 |
결론¶
경계는 코드로 먼저 나누고, 인프라는 나중에 나눠도 늦지 않다. 모듈러 모놀리스에서 마이크로서비스로의 전환은 설계가 아니라 운영의 문제다.