스키마를 바꾸는 방법을 알았다면, 다음 질문은 그 스키마를 어디까지 하나로 둘 것인가입니다.
1. 왜 경계를 고민하는가
하나의 DB에 모든 것을 넣으면 처음에는 편하지만, 시간이 지나면:
- 어느 팀이 어떤 테이블을 소유하는지 불분명해지고
- 한 팀의 스키마 변경이 다른 팀의 쿼리를 깨뜨리며
- 무거운 분석 쿼리가 운영 트랜잭션을 방해합니다
반대로 너무 일찍 나누면 조인이 불가능해지고, 분산 트랜잭션과 데이터 동기화라는 훨씬 비싼 문제가 생깁니다.
2. 바운디드 컨텍스트
같은 단어가 영역마다 다른 의미를 가집니다.
| 컨텍스트 | "주문항목"의 의미 | 관심 속성 |
|---|---|---|
| 판매 | 상품 선택 단위 | 상품, 수량, 주문유형 |
| 결제 | 결제 대상 | 결제 금액, 결제 시점, 고객 |
| 물류 | 처리 대상 | 처리단계 계획, 실적, 물류센터 |
| 정산 | 정산 집계 단위 | 셀러, 매출 금액, 처리 수량 |
하나의 거대한 order_item 테이블에 모든 컨텍스트의 속성을 넣으면, 컬럼 수십 개와 서로 다른 변경 주기, 서로 다른 팀의 요구가 충돌합니다. 컨텍스트마다 자기 관점의 주문항목 모델을 두고, 공통 식별자(item_id)로 연결하는 것이 도메인 주도 설계의 접근입니다.
3. 분리의 단계
| 단계 | 구조 | 경계 강도 | 조인 |
|---|---|---|---|
| 0 | 단일 스키마 | 없음 | 자유 |
| 1 | 같은 DB, 컨텍스트별 스키마 + 권한 | 네임스페이스·권한 | 가능 (권한 부여 시) |
| 2 | 같은 DB, 스키마 간 직접 조인 금지 (뷰·API만 허용) | 규약 | 공개 뷰만 |
| 3 | 컨텍스트별 DB | 물리적 | 불가 |
앞서 정리한 원칙대로 처음에는 덜 나누는 쪽이 안전합니다. 1~2단계로 경계를 연습하고, 경계가 실제로 안정적임이 확인된 컨텍스트만 3단계로 옮깁니다. 나누는 것보다 합치는 것이 훨씬 어렵기 때문입니다.
4. 공개 인터페이스로서의 뷰
-- 물류 컨텍스트가 소유
CREATE SCHEMA fulfillment;
CREATE TABLE fulfillment.item_stage (...);
-- 다른 컨텍스트에 공개하는 계약
CREATE VIEW fulfillment.v_item_progress_public AS
SELECT item_id, stage_code, actual_date
FROM fulfillment.item_stage;
GRANT SELECT ON fulfillment.v_item_progress_public TO inspection_app;
-- 내부 테이블에는 권한을 주지 않음물류 컨텍스트는 내부 테이블을 자유롭게 바꾸고, 공개 뷰의 형태만 유지하면 됩니다. 이것이 1강의 외부 스키마를 조직 경계에 적용한 모습입니다.
5. DB를 분리했을 때 잃는 것과 대안
| 잃는 것 | 대안 |
|---|---|
| 컨텍스트 간 조인 | API 조합, 데이터 복제(읽기 모델), 분석 저장소 |
| 컨텍스트 간 FK | 식별자 규약 + 비동기 정합성 검사 |
| 단일 트랜잭션 | 사가(Saga), 보상 트랜잭션 |
| 일관된 스냅샷 | 이벤트 시각 기준 정렬, 최종 일관성 수용 |
데이터 복제 (읽기 모델) 정산 컨텍스트가 주문항목의 출고건 정보를 자주 필요로 한다면, 물류 DB를 매번 호출하는 대신 필요한 속성만 로컬에 복제합니다. 복제는 이미 학습한 트랜잭셔널 아웃박스 + CDC 패턴으로 구현합니다.
-- 물류 DB: 비즈니스 변경과 이벤트를 같은 트랜잭션으로
-- 바뀌는 부모는 출고건. 주문항목의 소속 주문(order_no)은 불변이라 건드리지 않음
BEGIN;
UPDATE fulfillment.order_item SET shipment_no = 'SH-26030-002' WHERE item_id = 7;
INSERT INTO fulfillment.outbox (aggregate_type, aggregate_id, event_type, payload)
VALUES ('OrderItem', '7', 'ItemReassigned', '{"item_id":7,"shipment_no":"SH-26030-002"}');
COMMIT;
-- Debezium이 outbox를 읽어 Kafka로 발행 → 정산 DB가 구독해 로컬 사본 갱신복제된 데이터는 읽기 전용 사본이며, 원천 소유권은 물류 컨텍스트에 있다는 것을 스키마 이름과 문서로 명확히 합니다.
6. 공유 DB 안티패턴
여러 서비스가 같은 테이블을 각자 직접 읽고 쓰는 구조입니다.
- 테이블 변경 시 영향 범위를 알 수 없음
- 서비스별 배포가 독립적이지 않음
- 락 경합과 성능 문제의 원인 추적이 어려움
"DB는 하나지만 테이블 소유자는 하나"라는 규칙만 지켜도 많은 문제가 사라집니다. 소유 서비스만 쓰기, 다른 서비스는 공개 뷰로 읽기.
7. 분리 판단 기준
| 질문 | 분리 쪽 |
|---|---|
| 변경 주기와 배포 주기가 다른가? | 예 |
| 다른 팀이 소유하는가? | 예 |
| 부하 특성이 크게 다른가? (OLTP vs 분석) | 예 |
| 보안·규제 등급이 다른가? | 예 |
| 강한 일관성이 필요한 트랜잭션이 경계를 넘는가? | 아니오여야 분리 가능 |
| 경계를 넘는 조인이 핵심 기능인가? | 아니오여야 분리 가능 |
23강 정리
- 바운디드 컨텍스트마다 같은 개념을 다르게 모델링할 수 있다.
- 경계는 스키마·권한·공개 뷰 → 물리 분리 순으로 점진적으로 강화한다.
- DB를 나누면 조인·FK·트랜잭션을 잃고, 복제·사가·최종 일관성으로 보완한다.
- 컨텍스트 간 복제는 아웃박스 + CDC로 구현한다.
- 강한 일관성이 경계를 넘는다면 아직 나눌 때가 아니다.