DB/PostgreSQL

샤딩 환경의 조인, 분산 트랜잭션, Saga 패턴 정리

Joonfluence 2026. 7. 11.

1. 문서 제목

  • 샤딩 환경의 조인, 분산 트랜잭션, Saga 패턴 정리

2. 기술 개요 요약

샤딩은 데이터를 여러 독립적인 DB 서버로 나누어 저장하는 수평 분할 방식이다. 쓰기 트래픽과 데이터량을 분산할 수 있지만, 단일 DB 안에서는 자연스럽게 처리되던 JOIN과 트랜잭션이 샤드 경계를 넘는 순간 복잡해진다. 서로 다른 샤드는 물리적으로 다른 서버, 다른 커넥션, 다른 트랜잭션 경계를 가지기 때문이다.

샤딩 환경에서 크로스 샤드 조인은 애플리케이션 레벨 조인, 비정규화, 분산 쿼리 엔진, CQRS 기반 조회 모델 등으로 해결한다. 여러 샤드에 걸친 트랜잭션은 전통적으로 2PC(Two-Phase Commit)를 사용할 수 있지만, 블로킹과 가용성 저하 문제가 크다. 실무에서는 강한 일관성을 일부 포기하고 로컬 트랜잭션과 보상 트랜잭션을 조합하는 Saga 패턴을 더 자주 고려한다.


3. 핵심 기능/개념 정리

3-1. 샤딩이란

항목 설명 실무 포인트
샤딩 데이터를 여러 DB 서버 또는 파티션으로 나누어 저장하는 수평 분할 방식 데이터량과 쓰기 부하를 분산할 수 있음
샤드 분할된 데이터가 저장되는 독립 DB 단위 각 샤드는 별도 트랜잭션 경계를 가짐
샤딩 키 어떤 데이터를 어느 샤드에 저장할지 결정하는 기준 user_id, tenant_id, order_id 등 접근 패턴 기반으로 선택
Co-location 자주 함께 조회되는 데이터를 같은 샤드에 배치하는 설계 크로스 샤드 조인과 분산 트랜잭션을 줄이는 핵심
Resharding 샤딩 구조를 다시 나누는 작업 초기 샤딩 키가 부적절하면 운영 중 재분배 비용이 큼

예시:

user_id % 4 = 0 -> shard_0
user_id % 4 = 1 -> shard_1
user_id % 4 = 2 -> shard_2
user_id % 4 = 3 -> shard_3

이 방식은 단순하지만, 나중에 샤드 수를 바꿀 때 데이터 재배치가 필요할 수 있다.

3-2. 샤딩 환경에서 JOIN이 어려워지는 이유

단일 DB에서는 같은 인스턴스 안의 테이블끼리 SQL JOIN을 수행할 수 있다. 하지만 샤딩된 환경에서는 테이블 또는 row가 서로 다른 물리 DB에 흩어져 있을 수 있다.

상황 단일 DB 샤딩된 DB
데이터 위치 같은 DB 인스턴스 내부 여러 DB 서버에 분산
JOIN 처리 DB 엔진이 직접 처리 애플리케이션, 미들웨어, 조회 모델 등이 처리
트랜잭션 경계 하나의 DB 트랜잭션 여러 로컬 트랜잭션으로 분리
장애 영향 단일 DB 장애 범위 네트워크, 샤드별 장애, 부분 실패 고려 필요

3-3. 크로스 샤드 조인 해결 방식

방식 설명 장점 단점
애플리케이션 레벨 조인 각 샤드에서 데이터를 조회한 뒤 애플리케이션 메모리에서 합침 구현 개념이 단순함 데이터량이 많으면 느리고 애플리케이션 복잡도 증가
Scatter-Gather 여러 샤드에 병렬 질의 후 결과를 모음 전체 샤드 대상 조회 가능 fan-out 비용, timeout, partial failure 처리 필요
비정규화 자주 함께 조회되는 데이터를 중복 저장 조회 성능이 좋고 조인 제거 가능 중복 데이터 정합성 관리 필요
Co-location 설계 같은 샤딩 키를 기준으로 관련 데이터를 같은 샤드에 배치 크로스 샤드 접근 감소 샤딩 키 선택이 어려움
분산 쿼리 엔진/미들웨어 Vitess, Trino 등으로 여러 샤드 쿼리 처리 애플리케이션 코드 부담 감소 도구 운영 복잡도와 쿼리 제약 존재
CQRS 조회 모델 쓰기 모델과 조회 모델을 분리하고 조회 전용 저장소 구성 복잡한 조회 최적화 가능 최종적 일관성, 동기화 파이프라인 필요

3-4. 분산 트랜잭션과 2PC

여러 샤드에 걸쳐 하나의 비즈니스 작업을 수행해야 할 때 분산 트랜잭션 문제가 발생한다.

예시:

1. shard_A에서 재고 차감
2. shard_B에서 주문 생성
3. shard_C에서 결제 기록 생성

이 세 작업을 모두 성공시키거나 모두 실패시키려면 단일 DB 트랜잭션만으로는 부족하다.

2PC는 분산 트랜잭션의 전통적인 atomic commit 프로토콜이다.

단계 설명
Prepare phase 코디네이터가 모든 참여자에게 커밋 가능 여부를 묻고, 참여자는 준비 상태로 대기
Commit phase 모든 참여자가 OK를 보내면 커밋, 하나라도 실패하면 롤백

2PC의 핵심 한계:

한계 설명
블로킹 코디네이터 장애 시 참여자가 결과를 알 수 없어 대기할 수 있음
락 유지 시간 증가 prepare 이후 commit/rollback 결정 전까지 리소스를 오래 잡을 수 있음
가용성 저하 일부 참여자나 네트워크 문제가 전체 트랜잭션을 막음
성능 비용 참여 샤드 수가 늘수록 네트워크 왕복과 조율 비용 증가

주의할 점은 2PC가 "여러 참여자가 모두 커밋하거나 모두 롤백한다"는 atomic commit 문제를 다루는 프로토콜이라는 점이다. 격리 수준, 읽기 일관성, 애플리케이션 레벨의 불변식까지 자동으로 해결해주는 만능 분산 트랜잭션 모델로 설명하면 과장이다.

3-5. Saga 패턴

Saga는 하나의 큰 분산 트랜잭션을 여러 개의 로컬 트랜잭션으로 나누고, 실패 시 이미 완료된 작업을 보상 트랜잭션으로 되돌리는 패턴이다.

항목 설명 실무 포인트
Local Transaction 각 서비스 또는 샤드 안에서 즉시 커밋되는 단위 작업 각 단계는 자체 DB 트랜잭션으로 완료
Compensating Transaction 이미 완료된 작업을 비즈니스적으로 취소하는 작업 재고 차감의 보상은 재고 복구, 주문 생성의 보상은 주문 취소
Eventual Consistency 모든 단계가 즉시 동시에 일치하지는 않지만, 최종적으로 일관된 상태에 도달 중간 불일치를 허용할 수 있는 도메인에 적합
Idempotency 같은 보상/재시도 명령이 여러 번 실행돼도 결과가 깨지지 않게 설계 메시지 중복, 재시도, 장애 복구에 필수

정상 흐름:

[재고 차감 로컬 TX]
  -> [주문 생성 로컬 TX]
  -> [결제 기록 로컬 TX]
  -> 완료

실패 흐름:

[재고 차감 완료]
  -> [주문 생성 완료]
  -> [결제 기록 실패]
  -> [주문 취소 보상 TX]
  -> [재고 복구 보상 TX]

3-6. Saga 구현 방식: Choreography vs Orchestration

방식 설명 장점 단점
Choreography 각 서비스가 이벤트를 발행/구독하며 다음 단계를 자율적으로 진행 중앙 제어자가 없어 결합도가 낮음 전체 흐름 추적이 어렵고 이벤트 체인이 복잡해질 수 있음
Orchestration 중앙 오케스트레이터가 각 단계를 순서대로 지시 흐름 파악과 장애 제어가 쉬움 오케스트레이터에 비즈니스 흐름이 집중됨

선택 기준:

상황 더 적합한 방식
참여 서비스가 적고 흐름이 단순함 Choreography
단계가 많고 실패/보상 분기가 복잡함 Orchestration
전체 진행 상태를 명확히 추적해야 함 Orchestration
서비스 간 자율성과 느슨한 결합이 중요함 Choreography

3-7. 2PC와 Saga 비교

항목 2PC Saga
일관성 모델 atomic commit을 제공하지만 블로킹 가능 최종적 일관성
트랜잭션 방식 여러 참여자를 하나의 atomic commit으로 조율 여러 로컬 트랜잭션을 순차 실행
실패 처리 전체 commit 또는 rollback 완료된 단계에 대해 보상 트랜잭션 실행
가용성 낮아질 수 있음 상대적으로 높음
락 유지 길어질 수 있음 각 로컬 트랜잭션은 짧게 유지 가능
복잡도 인프라/프로토콜 복잡도 비즈니스 보상 로직 복잡도
적합한 상황 여러 참여자의 atomic commit이 필요한 제한된 범위 분산 서비스/샤드 간 장기 비즈니스 프로세스

4. 사용 예시 및 코드 스니펫

4-1. 샤딩 키 기반 라우팅 예시

function getShardId(userId: number, shardCount: number): number {
  return userId % shardCount;
}

const shardId = getShardId(12345, 4);
// shardId = 1

실무에서는 단순 modulo보다 consistent hashing, range sharding, directory 기반 라우팅 등을 고려할 수 있다. 중요한 것은 샤딩 키가 주요 조회 패턴과 트랜잭션 경계를 얼마나 잘 반영하는가다.

4-2. 애플리케이션 레벨 조인 예시

type Order = {
  id: string;
  userId: number;
  totalAmount: number;
};

type User = {
  id: number;
  name: string;
};

async function getOrdersWithUsers(userIds: number[]) {
  const ordersByShard = await Promise.all(
    groupUserIdsByShard(userIds).map(({ shardId, ids }) =>
      orderRepository.findByUserIds(shardId, ids),
    ),
  );

  const usersByShard = await Promise.all(
    groupUserIdsByShard(userIds).map(({ shardId, ids }) =>
      userRepository.findByIds(shardId, ids),
    ),
  );

  const users = new Map(
    usersByShard.flat().map((user: User) => [user.id, user]),
  );

  return ordersByShard.flat().map((order: Order) => ({
    ...order,
    user: users.get(order.userId),
  }));
}

이 방식은 이해하기 쉽지만, 데이터가 많아질수록 네트워크 호출 수, 메모리 사용량, timeout 처리가 중요해진다.

4-3. Saga 흐름 예시

CreateOrderSaga

1. reserveInventory(orderId)
2. createOrder(orderId)
3. authorizePayment(orderId)

if authorizePayment fails:
  compensate createOrder -> cancelOrder(orderId)
  compensate reserveInventory -> releaseInventory(orderId)

보상 트랜잭션은 기술적 rollback이 아니라 비즈니스적으로 의미 있는 취소 작업이다. 예를 들어 결제 취소, 쿠폰 복구, 재고 복구, 주문 취소는 각각 별도의 도메인 규칙을 가진다.

4-4. 멤버십 마이그레이션 프로젝트 맥락과의 차이

기존 멤버십 마이그레이션 프로젝트에서는 Saga 보상 조합이 복잡해져 append-only 원장 방식으로 문제를 재정의했다. 이 판단은 “Saga가 나쁘다”는 뜻이 아니라, 해당 문제는 단일 DB 안의 상태 전이 복잡도를 줄이는 것이 핵심이었다는 뜻이다.

반면 이 문서의 맥락은 여러 물리 샤드 또는 서비스에 걸친 진짜 분산 트랜잭션이다. 이 경우 단일 DB의 append-only 원장만으로 모든 샤드의 원자성을 보장할 수 없으므로, 2PC나 Saga 같은 분산 트랜잭션 전략을 별도로 검토해야 한다.


5. 실제 사용 시 주의점 / Best Practice

5-1. 샤딩은 최후의 확장 수단에 가깝다

샤딩은 쓰기 부하와 데이터량을 분산할 수 있지만, 조인·트랜잭션·운영 복잡도를 크게 높인다. 먼저 인덱스 튜닝, 쿼리 개선, 캐싱, read replica, 파티셔닝, 아카이빙 등으로 해결 가능한지 검토하는 것이 좋다.

5-2. 샤딩 키는 조회 패턴과 트랜잭션 경계를 기준으로 고른다

샤딩 키를 단순히 카디널리티만 보고 고르면 안 된다. 자주 함께 조회되고 함께 변경되는 데이터가 같은 샤드에 위치하도록 설계해야 크로스 샤드 조인과 분산 트랜잭션을 줄일 수 있다.

5-3. 크로스 샤드 조인은 가능한 한 설계로 줄인다

애플리케이션 레벨 조인이나 분산 쿼리 엔진은 해결책이지만, 모든 문제를 싸게 해결해주지는 않는다. 조회 패턴이 명확하다면 비정규화, CQRS 조회 모델, 검색엔진/분석 저장소 등으로 읽기 모델을 분리하는 편이 더 안정적일 수 있다.

5-4. Saga의 보상 트랜잭션은 완벽한 rollback이 아니다

보상 트랜잭션은 DB rollback처럼 과거 상태를 기계적으로 복원하는 것이 아니다. 이미 외부 시스템에 결제가 요청됐거나, 사용자에게 알림이 발송됐거나, 시간이 지나 환불 정책이 달라졌다면 보상 로직은 도메인 규칙을 따라야 한다.

5-5. Saga 단계와 보상 명령은 멱등해야 한다

분산 환경에서는 메시지 중복, timeout, 재시도, partial failure가 자연스럽게 발생한다. cancelOrder(orderId)releaseInventory(orderId)가 여러 번 호출되어도 상태가 깨지지 않도록 idempotency key, 상태 머신, 중복 처리 방지 테이블 등을 설계해야 한다.

5-6. Orchestration은 복잡한 비즈니스 흐름에서 유리하다

단계가 많고 보상 분기가 복잡한 경우에는 중앙 오케스트레이터가 전체 상태를 추적하는 방식이 운영과 디버깅에 유리하다. 반대로 단순한 이벤트 전파 흐름이라면 Choreography가 더 가볍다.

5-7. 면접 답변으로 압축하기

RDBMS를 샤딩하면 데이터가 여러 물리 DB에 나뉘기 때문에 단일 DB에서 가능했던 JOIN과 트랜잭션이 어려워집니다. 크로스 샤드 조인은 애플리케이션 레벨 조인, 비정규화, co-location 설계, 분산 쿼리 엔진, CQRS 조회 모델 등으로 해결합니다. 여러 샤드에 걸친 트랜잭션은 2PC를 사용할 수 있지만 코디네이터 장애 시 블로킹되고 락 유지 시간이 길어져 가용성이 떨어질 수 있습니다. 그래서 실무에서는 로컬 트랜잭션을 순차 실행하고 실패 시 보상 트랜잭션으로 되돌리는 Saga 패턴을 많이 고려합니다. 다만 Saga는 강한 일관성을 포기하고 최종적 일관성을 받아들이는 트레이드오프가 있으며, 보상 로직과 멱등성 설계가 중요합니다.


검증 메모

  • Saga 설명은 Microservices.io, AWS Prescriptive Guidance, Azure Compensating Transaction 패턴 설명과 대조했다.
  • 2PC는 "강한 일관성 전체"가 아니라 atomic commit 프로토콜이라는 범위로 보완했다.

6. 참고자료 / 공식 문서 출처

반응형

댓글