대규모 모놀리스를 실전에서 쪼개는 법:
⸻
1. 왜 단순 마이크로서비스 분리로는 실패하는가
현업에서 모놀리스를 마이크로서비스로 나누는 작업은 “리팩터링”이 아니라
시스템 전체를 해체 후 재조립하는 외과적 절차에 가깝습니다.
실패하는 전형적인 시나리오:
• 경계 없이 Controller/Service만 분리해 호출 관계가 엉키는 경우
• 공용 DB를 공유한 채 API만 분리한 ‘분산 모놀리스’
• 도메인과 무관한 팀 단위 분리 → 조직 구조와 아키텍처가 충돌
따라서 도메인 중심 분할, 데이터 소유권 정리, 인프라 자동화가 반드시 병행되어야 합니다.
⸻
2. 도메인 경계 기반 코드 자동 분할 구조
2.1 폴더 구조 재설계 예시 (NestJS 기준)
apps/
user-service/
src/
user/
auth/
main.ts
order-service/
src/
order/
payment/
main.ts
libs/
shared/
logging/
config/
• apps/: 독립 배포 가능한 서비스 단위
• libs/: 공통 유틸(단, DB나 서비스 레이어는 공유 금지)
2.2 Nx를 활용한 경계 강제 (monorepo용)
nx.json
{
"projects": {
"user-service": {
"tags": ["scope:user"]
},
"order-service": {
"tags": ["scope:order"]
},
"shared-logger": {
"tags": ["type:shared"]
}
}
}
• lint rules를 통해 scope:user는 scope:order 호출 금지하도록 설정 가능
• 서비스 간 호출은 반드시 API 호출(gRPC/REST) 또는 Event 기반 통신만 허용
⸻
3. 데이터베이스 스키마 분리 전략
3.1 PostgreSQL 내 Schema 단위 분리
-- user-service migration
CREATE SCHEMA user;
CREATE TABLE user.accounts (
id UUID PRIMARY KEY,
name TEXT,
email TEXT UNIQUE
);
-- order-service migration
CREATE SCHEMA order;
CREATE TABLE order.orders (
id UUID PRIMARY KEY,
user_id UUID,
amount INTEGER
);
• DB는 하나, 하지만 서비스 간 테이블 명확히 분리
• Flyway/Liquibase로 schema별 migration 디렉토리 구분
3.2 서비스별 독립 DB 구성 + 동기화
• Debezium + Kafka: 변경된 데이터를 이벤트로 추출해 동기화
• Materialized View: 필요한 데이터만 복제하여 읽기 전용 사용
{
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "user-db",
"database.server.name": "user-service",
"table.include.list": "public.accounts"
}
⸻
4. CI/CD 파이프라인 자동 분리 및 매트릭스 구성
4.1 GitHub Actions 매트릭스 전략
jobs:
deploy-microservices:
runs-on: ubuntu-latest
strategy:
matrix:
service: [user-service, order-service]
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Deploy ${{ matrix.service }}
run: ./scripts/deploy.sh ${{ matrix.service }}
• 서비스별 별도 빌드/배포 논리 적용
• ./scripts/deploy.sh 내부에서 helm/kustomize로 k8s 배포
⸻
5. 트래픽 경계와 서비스 간 통신
5.1 gRPC 통신 예시 (NestJS)
user-service.proto
service UserService {
rpc GetUser (GetUserRequest) returns (User);
}
order-service 호출 코드
const client = this.client.getService<UserService>('UserService');
const user = await client.getUser({ id: 'abc' }).toPromise();
• 서비스 간 강한 스키마 기반 통신 가능
• REST 대비 데이터 정합성·버전 관리 유리
⸻
6. 배포 경계: Helm + Kustomize로 서비스 분리
deploy/
base/
deployment.yaml
service.yaml
overlays/
user/
kustomization.yaml
order/
kustomization.yaml
# kustomization.yaml (user)
bases:
- ../../base
patchesStrategicMerge:
- deployment-user.yaml
• 공통 템플릿은 base로 관리, 각 서비스는 overlay로 확장
• 서비스 단위로 rollout, rollback 가능
⸻
7. 모니터링 및 Alert 자동 분리
Prometheus relabeling 규칙
- source_labels: [__meta_kubernetes_service_label_app]
regex: user-service
action: keep
Grafana 대시보드 자동 생성 (jsonnet or mixin)
local grafana = import 'grafonnet/grafana.libsonnet';
grafana.dashboard.new('User Service')
.addPanel(
grafana.panel.timeseries.new('Login Error Rate')
.addTarget(
grafana.prometheus.target.new('rate(login_errors_total[5m])')
)
)
⸻
결론: 모놀리스 분리는 자동화로 설계되지 않으면 절반의 성공에 그친다
• 코드만 나눈 서비스는 결국 다시 얽히고, DB를 공유하면 장애도 공유됩니다.
• 진정한 분리는 도메인, 데이터, 배포, 통신, 모니터링 전 과정을 자동화했을 때 완성됩니다.
• Terraform, GitHub Actions, Debezium, gRPC, Helm, Jsonnet 등을 활용해
“마이크로서비스 전환을 자동화 가능한 인프라 수준의 절차”로 승격해야 합니다.
당신의 시스템이 아직 단일 빌드, 단일 DB, 단일 배포라면
**분산 시스템이 아니라 분산된 위험(monolith of pain)**일 수 있습니다.
분리는 선택이 아니라 생존 전략입니다.
지금 자동화 기반으로 리디자인하지 않으면, 1년 내 확장 한계에 부딪히게 될 것입니다.
카테고리 없음