이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Data Ownership은 테이블 소유와 무엇이 다른가?
- 서비스 경계는 성능보다 정합성과 변경 책임을 어떻게 바꾸는가?
- 공유 DB 접근, API 호출, 이벤트 동기화 중 무엇을 선택해야 하는가?
개요
Data Ownership은 “어느 서비스가 그 데이터를 가장 많이 읽는가”가 아니다. 그 데이터를 최종적으로 변경할 권한과 불변식을 책임지는 곳을 정하는 일이다. 주문 상태를 주문 서비스가 소유한다면 결제 서비스는 주문 테이블을 직접 수정하지 않는다. 결제 서비스는 결제 승인 결과를 자신의 원장에 기록하고, 주문 서비스가 이해할 수 있는 API나 이벤트로 결과를 전달한다. 이 경계가 없으면 처음에는 빠르게 만들 수 있지만 장애와 배포가 서로 묶인다.
이 문서가 바꾸는 설계 축
| 축 | 바뀌는 내용 |
|---|---|
| 성능 | 직접 DB join은 줄고 API/event 호출이 늘어 latency와 cache 전략이 달라진다. |
| 정합성 | 하나의 owner가 불변식을 책임하므로 중복 write와 충돌을 줄인다. |
| 장애 격리 | 한 서비스 장애가 다른 서비스의 DB write까지 깨뜨리지 않게 한다. |
| 운영 복잡도 | contract, event version, replay, read model 관리가 필요해진다. |
Owner를 정하는 질문
서비스 경계는 다음 질문으로 잡는다.
| 질문 | 의미 |
|---|---|
| 이 데이터의 불변식은 무엇인가? | 깨지면 안 되는 규칙을 찾는다. |
| 누가 상태 전이를 승인하는가? | 최종 write 권한을 정한다. |
| 누가 장애 후 복구를 책임지는가? | owner team을 정한다. |
| 다른 서비스는 어떤 view가 필요한가? | API, event, read model을 결정한다. |
| stale을 허용할 수 있는가? | 동기 조회와 비동기 복제를 나눈다. |
| owner가 없다는 것은 모든 팀이 쓸 수 있다는 뜻이 아니라 아무 팀도 끝까지 책임지지 않는다는 뜻이다. |
대표 시나리오
주문, 결제, 배송을 분리한다고 하자.
| 데이터 | Owner | 다른 서비스의 접근 |
|---|---|---|
| 주문 상태 | Order Service | API 조회, OrderStatusChanged event |
| 결제 승인 원장 | Payment Service | PaymentApproved event |
| 배송 송장 | Shipping Service | ShipmentCreated event |
| 고객 주소 | Member Service | snapshot 또는 member API |
| 주문 서비스는 결제 승인 결과를 받아 주문 상태를 바꾼다. | ||
결제 서비스가 주문 DB의 status를 직접 PAID로 바꾸면 빠르지만 소유권이 깨진다. | ||
| 나중에 주문 상태 전이 규칙이 바뀌어도 결제 서비스 배포를 같이 해야 한다. |
공유 DB 접근의 비용
공유 DB는 처음에는 편하다.
UPDATE orders
SET status = 'PAID'
WHERE order_id = :orderId;결제 서비스가 이 SQL을 직접 실행하면 API latency는 낮을 수 있다. 하지만 다음 비용이 생긴다.
| 비용 | 설명 |
|---|---|
| 정합성 비용 | 주문 상태 전이 규칙이 여러 코드에 흩어진다. |
| 배포 비용 | orders schema 변경이 결제 서비스까지 깨뜨린다. |
| 장애 비용 | 결제 장애가 주문 DB lock이나 connection을 점유할 수 있다. |
| 감사 비용 | 누가 상태를 바꿨는지 추적하기 어려워진다. |
| 공유 DB는 monolith 내부 module 사이에서는 괜찮을 수 있다. | |
| 하지만 독립 배포되는 서비스 사이에서는 강한 결합이 된다. |
API 경계
동기 API는 즉시 결과가 필요한 상태 전이에 적합하다.
POST /orders/{orderId}/payment-approval
Idempotency-Key: payment-evt-772
{
"paymentId": "pay-1004",
"approvedAt": "2026-07-01T10:00:00+09:00"
}주문 서비스는 이 요청을 받아 자신의 규칙으로 상태를 바꾼다. 결제 서비스는 주문 상태 전이 로직을 알 필요가 없다. 단점은 결제 완료 시점에 주문 서비스가 느리거나 장애이면 처리 지연이 생긴다는 점이다. 이때 retry와 idempotency key가 필요하다.
이벤트 경계
비동기 이벤트는 서비스 간 결합을 낮춘다.
{
"eventId": "payment-approved-772",
"type": "PaymentApproved",
"paymentId": "pay-1004",
"orderId": "order-42",
"schemaVersion": 1
}주문 서비스는 이 이벤트를 소비해 주문 상태를 바꾼다.
이 방식은 결제 서비스와 주문 서비스의 runtime 결합을 낮춘다.
대신 중복 이벤트, 순서, 지연, DLQ, replay가 설계에 들어온다.
사용자에게 결제 완료 직후 주문 상태가 잠시 PAYMENT_PENDING으로 보일 수 있다.
read model과 소유권
다른 서비스가 owner 데이터를 자주 읽는다면 매번 owner API를 호출하지 않을 수 있다. 예를 들어 배송 서비스가 주문 요약을 자주 필요로 하면 주문 이벤트로 배송 전용 read model을 만든다.
CREATE TABLE shipping_order_view (
order_id varchar(50) PRIMARY KEY,
member_id varchar(50) NOT NULL,
address_snapshot jsonb NOT NULL,
order_status varchar(30) NOT NULL,
updated_at timestamp NOT NULL
);이 테이블은 배송 서비스가 읽기 위해 소유하는 projection이다. 하지만 주문 상태의 원본 owner는 여전히 주문 서비스다. projection은 stale 가능성을 갖고, lag 지표와 rebuild 절차가 필요하다.
Spring 경계 예시
owner service는 상태 전이를 도메인 메서드로 감싼다.
@Transactional
public void approvePayment(String orderId, String paymentEventId) {
if (processedEventRepository.existsById(paymentEventId)) {
return;
}
Order order = orderRepository.findByIdForUpdate(orderId)
.orElseThrow();
order.markPaid();
processedEventRepository.save(new ProcessedEvent(paymentEventId));
}결제 이벤트를 중복으로 받아도 주문 상태는 한 번만 바뀌어야 한다. owner service는 외부 이벤트를 자기 언어의 command로 바꾼 뒤 불변식을 검증한다.
경계 선택 기준
| 선택 | 적합한 경우 | 비용 |
|---|---|---|
| 같은 서비스 내부 transaction | 강한 정합성과 단일 배포가 가능할 때 | 서비스가 커질 수 있다. |
| 동기 API | 즉시 결과와 명확한 실패 응답이 필요할 때 | latency, timeout, retry 설계가 필요하다. |
| 비동기 event | 지연 허용과 느슨한 결합이 중요할 때 | 중복, 순서, replay, 관측 복잡도 |
| read model | 조회 성능과 화면 전용 구조가 필요할 때 | stale data와 rebuild 절차 |
개인 프로젝트 기준
- 서비스가 하나라도 도메인별 owner를 문서에 적는다.
- 다른 module이 직접 수정해도 되는 테이블과 안 되는 테이블을 나눈다.
- 이벤트를 쓰지 않더라도 상태 전이의 최종 책임 클래스를 하나로 둔다.
- 포트폴리오 설명에서는
서비스 분리보다어떤 불변식을 누가 책임지는지를 먼저 말한다.
기업 운영 기준
- owner service, owner team, schema owner, on-call owner를 일치시킨다.
- 다른 서비스의 직접 DB write를 금지하거나 예외 승인을 문서화한다.
- event contract에는 schemaVersion, idempotency key, replay 가능성을 포함한다.
- read model lag와 owner API error rate를 같이 본다.
- 서비스 경계 변경은 migration, backfill, dual-read/write 기간을 계획한다.
위험 신호
- 여러 서비스가 같은 테이블을 직접 update한다.
조회만 한다는 이유로 owner DB 접근이 계속 늘어난다.- 이벤트에 schemaVersion과 eventId가 없다.
- projection table을 원본처럼 수정한다.
- 상태 전이 규칙이 controller, batch, consumer에 흩어져 있다.
- 장애 후 어떤 팀이 데이터를 복구해야 하는지 정해져 있지 않다.
확인 질문
확인 질문
- 확인 질문: Data Ownership에서 가장 먼저 정해야 할 것은 무엇인가?
- 답변: 데이터의 최종 변경 권한과 불변식 책임을 가진 owner를 정해야 한다.
- 확인 질문: 공유 DB 접근이 위험한 이유는 무엇인가?
- 답변: 빠른 구현 대신 정합성 규칙, schema 변경, 장애 영향이 여러 서비스에 퍼지기 때문이다.
- 확인 질문: read model은 owner 데이터인가?
- 답변: 아니다. 조회 성능을 위한 파생 상태이며 원본 owner와 lag, rebuild 기준을 따로 관리해야 한다.