70장. Database per Service — 서비스별 DB 분리 패턴
이 장에서 말하고자 하는 것
지금까지 우리는 RDS · DynamoDB · ElastiCache 를 보았다.
마이크로서비스 구조에서 마지막 결정이 남아 있다.
“DB를 서비스마다 따로 두는가, 한 DB를 공유하는가?”
이 결정이 MSA의 성패를 가른다.
이 장은 Database per Service 원칙을 정리한다.
1. 한 DB 공유의 문제
[orders 서비스]
[users 서비스] → 하나의 RDS
[payments 서비스]
처음에는 단순해 보인다.
하지만 곧 다음 문제들이 나타난다.
1. 스키마 변경의 위험
users 서비스의 스키마 변경이 orders 서비스를 깨뜨릴 수 있다.
2. 한 서비스의 부하가 다른 서비스를 마비
payments 의 무거운 쿼리가 orders 응답을 느리게 만든다.
3. 배포 속도가 묶임
같은 DB 마이그레이션을 여러 팀이 함께 조정해야 한다.
4. 기술 선택이 묶임
orders 는 관계형이 자연스러운데catalog 는 DynamoDB가 맞다 — 한 DB로는 풀 수 없다.
2. Database per Service 원칙
[orders 서비스] → RDS PostgreSQL (orders 전용)
[users 서비스] → RDS PostgreSQL (users 전용)
[payments 서비스] → RDS PostgreSQL (payments 전용)
[catalog 서비스] → DynamoDB
[sessions 서비스] → ElastiCache
각 서비스가 자기 DB를 소유한다.
- 다른 서비스는 그 DB에 직접 접근 못 한다
- 데이터는 오직 그 서비스의 API를 통해서만 본다
3. 데이터를 어떻게 공유하는가
DB가 분리되면 자연스럽게 질문이 생긴다.
“주문 화면에 사용자 이름이 필요한데, users DB에 못 들어가면 어떻게?”
세 가지 패턴이 있다.
1. 동기 API 호출
orders → GET users-service/users/u-1 → 사용자 정보
- 단순함
- 다른 서비스가 죽으면 영향받음
2. 이벤트 + 로컬 복제
users 서비스 → "사용자 정보 변경" 이벤트 → SNS/EventBridge
orders 서비스가 받아서 자기 DB에 일부 정보 보관
- 다른 서비스의 가용성에 덜 의존
- 약간의 데이터 지연 가능
3. 공유 읽기 모델 (CQRS · BFF)
여러 서비스의 데이터를 합쳐서 보여주는 별도 읽기 전용 서비스를 둔다.
대규모 환경에서 쓴다.
4. Saga — 분산 트랜잭션의 현실
한 DB의 트랜잭션 (BEGIN ~ COMMIT) 은
여러 서비스에 걸친 작업을 한 번에 묶을 수 없다.
"주문 생성" 작업:
1. orders.orders 테이블에 주문 INSERT
2. payments.payments 테이블에 결제 시도
3. inventory.products 재고 차감
이 세 단계를 한 트랜잭션에 못 묶는다.
해결 패턴이 Saga 다 — 77장에서 다룬다.
5. 운영적 분리 — DB만 나누는 게 끝이 아니다
진짜 분리는 다음을 다 포함한다.
- DB 인스턴스/클러스터 분리
- 백업 · 복구 책임 분리
- 모니터링 · 알람 분리
- IAM 정책 분리 (Task Role)
- 스키마 마이그레이션 도구 (Flyway · Liquibase 등) 별도 운영
“한 RDS에 스키마만 다르게” 는 진정한 Database per Service가 아니다
6. 우리 서비스에서
[ECS "orders"]
↓ orders-task-role
[RDS PostgreSQL "orders-db"] (Multi-AZ)
[ECS "users"]
↓ users-task-role
[RDS PostgreSQL "users-db"] (Multi-AZ)
[ECS "payments"]
↓ payments-task-role
[RDS PostgreSQL "payments-db"] (Multi-AZ)
[ECS "catalog"]
↓ catalog-task-role
[DynamoDB "products"]
[ECS "sessions"]
↓ sessions-task-role
[ElastiCache "sessions-cache"]
각 서비스 Task Role은 자기 DB에만 접근할 수 있다.
7. 어떻게 시작할까
처음부터 모든 서비스에 DB를 따로 둘 필요는 없다.
Phase 1: 모놀리스 (한 DB)
Phase 2: 서비스 경계가 보이기 시작
Phase 3: 분리한 서비스만 별도 DB로
Phase 4: 새 서비스는 처음부터 자기 DB
“DB 분리” 가 곧 서비스 분리의 진짜 완성이다
8. 직접 확인해보기 — CLI
서비스별 IAM Role + DB 접근 정책
aws iam create-role --role-name orders-task-role ...
aws iam put-role-policy \
--role-name orders-task-role \
--policy-name orders-db-only \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["rds-db:connect"],
"Resource": "arn:aws:rds-db:ap-northeast-2:...:dbuser:orders-db/orders-app"
}]
}'
다른 서비스의 Task Role로는 orders-db에 못 들어간다
9. 코드로는 이렇게 생겼다 — Terraform (스케치)
module "orders" {
source = "./modules/microservice"
name = "orders"
db_engine = "postgres"
db_size = "db.t4g.small"
task_role_arn = aws_iam_role.orders_task.arn
}
module "users" {
source = "./modules/microservice"
name = "users"
db_engine = "postgres"
db_size = "db.t4g.small"
task_role_arn = aws_iam_role.users_task.arn
}
module "catalog" {
source = "./modules/microservice"
name = "catalog"
db_engine = "dynamodb"
task_role_arn = aws_iam_role.catalog_task.arn
}
같은 모듈을 서비스마다 인스턴스화 — 운영의 표준 모양이다.
10. 이렇게 쓰면 망한다 — 안티패턴
안티패턴 1. 다른 서비스의 DB에 직접 SELECT
“잠깐 한 줄만” 이 결국 강한 결합이 된다.
다른 서비스의 데이터는 그 서비스의 API로만
안티패턴 2. 한 RDS에 스키마만 다르게
인스턴스가 한 대인 이상 부하 · 백업 · 마이그레이션이 묶인다.
안티패턴 3. “공통 DB” 를 두고 모두 거기 쓰자
공통 DB는 모든 서비스의 SPOF가 된다.
안티패턴 4. 분산 트랜잭션을 강요한다
서비스 사이에 ACID 트랜잭션을 묶으려 하면 끝없는 복잡도가 된다.
Saga 패턴을 받아들이고 최종 일관성을 설계한다
11. 한 줄로 정리
Database per Service는 각 서비스가 자기 DB를 소유하는 원칙이며,
MSA의 진짜 경계는 데이터 분리에서 완성된다
12. 이 장의 핵심 정리
- 한 DB 공유는 스키마 충돌 · 부하 전염 · 배포 묶임을 만든다.
- 각 서비스가 자기 DB를 소유한다 — 다른 서비스는 API로만 접근.
- 데이터 공유는 동기 호출 · 이벤트 복제 · 별도 읽기 모델 중에 고른다.
- 분산 트랜잭션 대신 Saga와 최종 일관성을 받아들인다.
- 운영적 분리 (백업 · 알람 · IAM) 까지 함께 가야 진짜 분리다.
- 처음부터 완벽히 분리할 필요는 없다 — 서비스 경계가 보이는 곳부터.