참조 아키텍처: 마이크로서비스 데이터 계층
마이크로서비스에서 가장 중요한 결정은 데이터 소유권이다: 각 서비스는 자신의 데이터를 소유해야 하며(database-per-service) 다른 서비스의 테이블에 직접 접근해서는 안 된다. 이 트리는 공유 DB 안티패턴, 분산 트랜잭션 없이 서비스 간 일관성을 확보하는 방법(saga + outbox), CDC를 통한 데이터 동기화, 서비스 간 쿼리(API composition vs CQRS read model)를 단계별로 다룬다. 목표는 서비스를 진정으로 독립적이고 개별 배포 가능하게 유지하는 것이다.
서비스는 데이터를 어떻게 소유하는가: 공유 DB인가, 서비스별 DB인가?
데이터 격리는 마이크로서비스 아키텍처의 근간이다. 서비스들이 단일 공유 데이터베이스를 사용하는가(숨은 의존성), 각 서비스가 자체 데이터베이스를 가지는가(올바른 패턴), 아니면 실제 과제가 서비스 간 일관성/쿼리인가? 먼저 현재 상태를 파악한다.
- 모든 서비스가 단일 공유 DB를 사용함
- 각 서비스가 자체 DB를 가지지만 작업에 서비스 간 일관성이 필요함
- 각 서비스가 자체 DB를 가지지만 결합 쿼리/보고서가 필요함
서비스 간 워크플로를 어떻게 일관되게 유지할 것인가?
여러 서비스에 걸친 작업(주문 생성 → 결제 → 재고 차감)에서 전통적인 분산 트랜잭션(2PC)은 취약하고 확장되지 않는다. 대신 흐름을 로컬 트랜잭션 + 이벤트 단계(saga)로 나누고 이벤트를 안정적으로 발행한다(outbox). 어떤 문제를 다루고 있는가?
- 다중 서비스 워크플로를 원자적으로 유지하려 함(2PC?)
- 로컬 쓰기와 이벤트 발행을 일관되게 만들 수 없음(dual write)
여러 서비스의 데이터를 보여 주는 쿼리를 어떻게 제공할 것인가?
데이터가 여러 서비스로 분리되면 단일 조인으로 통합 화면을 만들 수 없다. 두 가지 주요 방식이 있다: 요청 시점에 관련 서비스의 API를 호출해 결과를 병합하는 방식(API composition), 또는 이벤트로 갱신되는 사전 조인된 읽기 모델을 유지하는 방식(CQRS). 쿼리 빈도와 복잡도에 따라 선택한다.
- 단순하고 드문 조인. 몇 개 서비스를 호출해 병합하면 충분
- 복잡하거나 잦은 통합 쿼리. 미리 준비된 읽기 모델이 필요
권장: Database-per-service(공유 DB 분리)
하나의 데이터베이스를 공유하는 서비스는 숨겨진 위험한 의존성이다: 한 서비스의 스키마 변경이 다른 서비스를 깨뜨리고, 테이블 소유권이 불분명해지며, 독립 배포가 불가능해진다. 각 서비스는 자신의 데이터를 소유해야 한다.
권장: Saga 패턴(분산 트랜잭션 회피)
여러 서비스에 걸친 작업을 단일 분산 트랜잭션(2PC)으로 원자화하려 하지 않는다. 2PC는 락을 유지하며 단일 지점 취약성과 낮은 가용성을 가져온다. 대신 흐름을 각 서비스의 로컬 트랜잭션인 단계들로 나누고, 실패 시 보상 단계로 되돌린다.
권장: Transactional outbox(dual-write 함정)
서비스가 자체 DB에 쓰면서 이벤트도 발행하려 할 때 두 작업을 따로 수행하면 'dual write' 함정에 빠진다: 하나는 성공하고 다른 하나는 실패하면 시스템이 불일치 상태로 남는다. 해결책은 업무 데이터와 같은 로컬 트랜잭션 안에서 이벤트를 outbox 테이블에 쓰고, 별도 프로세스로 발행하는 것이다.
권장: API composition(요청 시점 병합)
결합 데이터가 필요한 곳에서 호출자(예: gateway/BFF)가 관련 서비스의 API를 호출하고 결과를 메모리에서 병합한다. 소수 서비스에 걸친 단순 조인에 가장 간단한 솔루션이며, 추가 데이터 사본을 유지하지 않는다.
권장: CQRS read model(이벤트로 공급)
복잡하고 빈번한 결합 쿼리에는 서비스가 발행하는 이벤트로 공급되는, 미리 조인되고 쿼리에 최적화된 read model(materialized view/비정규화 사본)을 유지한다. 쓰기 측은 서비스에 남고, 읽기 측은 준비된 데이터를 한곳에서 빠르게 제공한다.