4강 50분

Aggregate 심화와 트랜잭션 관리

DDD의 핵심인 Aggregate 패턴을 심화 학습하고, 분산 환경에서 트랜잭션을 관리하는 Saga 패턴을 실습합니다.

Aggregate 심화와 트랜잭션 관리

학습 목표

  • Aggregate 설계 원칙 심화
  • 분산 트랜잭션 문제 이해
  • Saga 패턴 구현 (Choreography vs Orchestration)
  • 보상 트랜잭션 (Compensating Transaction)

Aggregate 설계 심화

Aggregate의 정의

Aggregate는 데이터 변경의 단위입니다.

핵심 원칙:

  1. 하나의 트랜잭션 안에서만 변경
  2. 외부에서는 Aggregate Root를 통해서만 접근
  3. 다른 Aggregate는 ID로만 참조

Aggregate 크기 결정

너무 큰 Aggregate (❌)

// 주문이 수백 개의 상품을 포함
class Order {
  items: OrderItem[];  // 1000개 이상 가능

  addItem(item: OrderItem) {
    this.items.push(item);
    // 전체 Order를 잠금 → 성능 문제
  }
}

적절한 크기 (✅)

// 주문은 최소한의 정보만
class Order {
  id: string;
  customerId: string;
  totalAmount: number;
  status: OrderStatus;
  // items는 별도 Aggregate
}

class OrderItem {
  orderId: string;  // 참조
  productId: string;
  quantity: number;
}

Eventual Consistency (최종 일관성)

Aggregate 간에는 최종 일관성 허용:

// Order Aggregate
class Order {
  async create(data: CreateOrderData) {
    // 1. 주문 생성 (즉시 일관성)
    const order = new Order(data);
    await orderRepo.save(order);

    // 2. 이벤트 발행 (비동기)
    await eventBus.publish('order.created', {
      orderId: order.id,
      items: data.items,
    });

    // 재고 차감은 나중에 (최종 일관성)
  }
}

// Inventory Aggregate (별도 서비스)
eventBus.subscribe('order.created', async (event) => {
  // 몇 초 후 재고 차감 완료
  await inventory.decreaseStock(event.items);
});

분산 트랜잭션 문제

2PC (Two-Phase Commit)의 한계

전통적인 2PC는 마이크로서비스에 부적합:

Coordinator

    ├─ Prepare → Service A ✓
    ├─ Prepare → Service B ✓
    ├─ Prepare → Service C ✗ (실패)

    └─ Rollback All (긴 잠금 시간)

문제점:

  • 모든 서비스가 잠금 (blocking)
  • 코디네이터 SPOF
  • 성능 저하

Saga 패턴

분산 트랜잭션을 로컬 트랜잭션 + 보상 트랜잭션으로 해결

Saga 패턴: Choreography 방식

서비스들이 이벤트를 통해 자율적으로 조율

예제: 이커머스 주문 프로세스

// 1. Order Service: 주문 생성
class OrderService {
  async createOrder(data: OrderData) {
    const order = await Order.create(data);
    order.status = 'PENDING';
    await orderRepo.save(order);

    // 이벤트 발행
    await eventBus.publish('order.created', {
      orderId: order.id,
      customerId: data.customerId,
      items: data.items,
      totalAmount: order.totalAmount,
    });
  }
}

// 2. Payment Service: 결제 처리
class PaymentService {
  constructor() {
    eventBus.subscribe('order.created', this.handleOrderCreated);
  }

  async handleOrderCreated(event: OrderCreatedEvent) {
    try {
      const payment = await this.processPayment(
        event.orderId,
        event.totalAmount
      );

      // 성공 이벤트
      await eventBus.publish('payment.completed', {
        orderId: event.orderId,
        paymentId: payment.id,
      });
    } catch (error) {
      // 실패 이벤트
      await eventBus.publish('payment.failed', {
        orderId: event.orderId,
        reason: error.message,
      });
    }
  }
}

// 3. Inventory Service: 재고 차감
class InventoryService {
  constructor() {
    eventBus.subscribe('payment.completed', this.handlePaymentCompleted);
    eventBus.subscribe('payment.failed', this.handlePaymentFailed);
  }

  async handlePaymentCompleted(event: PaymentCompletedEvent) {
    try {
      await this.decreaseStock(event.orderId);

      await eventBus.publish('inventory.reserved', {
        orderId: event.orderId,
      });
    } catch (error) {
      await eventBus.publish('inventory.failed', {
        orderId: event.orderId,
      });
    }
  }

  // 보상 트랜잭션: 결제 실패 시 아무것도 안 함
  async handlePaymentFailed(event: PaymentFailedEvent) {
    console.log(`Order ${event.orderId} payment failed, no inventory reservation`);
  }
}

// 4. Order Service: 최종 상태 업데이트
class OrderService {
  constructor() {
    eventBus.subscribe('inventory.reserved', this.handleInventoryReserved);
    eventBus.subscribe('inventory.failed', this.handleInventoryFailed);
  }

  async handleInventoryReserved(event: InventoryReservedEvent) {
    await orderRepo.updateStatus(event.orderId, 'CONFIRMED');

    // 고객에게 알림
    await eventBus.publish('order.confirmed', {
      orderId: event.orderId,
    });
  }

  async handleInventoryFailed(event: InventoryFailedEvent) {
    // 보상 트랜잭션: 결제 취소
    await eventBus.publish('payment.refund', {
      orderId: event.orderId,
    });

    await orderRepo.updateStatus(event.orderId, 'CANCELLED');
  }
}

// 5. Payment Service: 환불 처리 (보상 트랜잭션)
class PaymentService {
  constructor() {
    eventBus.subscribe('payment.refund', this.handleRefund);
  }

  async handleRefund(event: PaymentRefundEvent) {
    await this.refundPayment(event.orderId);

    await eventBus.publish('payment.refunded', {
      orderId: event.orderId,
    });
  }
}

Choreography 흐름도

성공 시나리오:

sequenceDiagram
    participant O as Order Service
    participant P as Payment Service
    participant I as Inventory Service

    O->>P: order.created
    P->>P: 결제 처리
    P->>I: payment.completed
    I->>I: 재고 차감
    I->>O: inventory.reserved
    O->>O: 주문 상태: CONFIRMED

실패 시나리오 (재고 부족):

sequenceDiagram
    participant O as Order Service
    participant P as Payment Service
    participant I as Inventory Service

    O->>P: order.created
    P->>P: 결제 처리
    P->>I: payment.completed
    I->>I: 재고 부족 확인
    I->>O: inventory.failed
    O->>P: payment.refund (보상 트랜잭션)
    P->>P: 환불 처리
    P->>O: payment.refunded
    O->>O: 주문 상태: CANCELLED

Saga 패턴: Orchestration 방식

중앙 Orchestrator가 전체 흐름을 제어

// Saga Orchestrator
class OrderSagaOrchestrator {
  async execute(orderData: OrderData) {
    const saga = new SagaInstance(orderData);

    try {
      // Step 1: 주문 생성
      const order = await this.orderService.createOrder(orderData);
      saga.addCompensation(() => this.orderService.cancelOrder(order.id));

      // Step 2: 결제 처리
      const payment = await this.paymentService.processPayment(
        order.id,
        order.totalAmount
      );
      saga.addCompensation(() => this.paymentService.refund(payment.id));

      // Step 3: 재고 차감
      await this.inventoryService.reserveStock(order.items);
      saga.addCompensation(() => this.inventoryService.releaseStock(order.items));

      // Step 4: 주문 확정
      await this.orderService.confirmOrder(order.id);

      return { success: true, orderId: order.id };
    } catch (error) {
      // 실패 시 보상 트랜잭션 실행
      await saga.compensate();
      return { success: false, error: error.message };
    }
  }
}

// Saga Instance
class SagaInstance {
  private compensations: (() => Promise<void>)[] = [];

  addCompensation(compensationFn: () => Promise<void>) {
    this.compensations.push(compensationFn);
  }

  async compensate() {
    // 역순으로 보상 실행
    for (const compensation of this.compensations.reverse()) {
      try {
        await compensation();
      } catch (error) {
        console.error('Compensation failed:', error);
        // 보상 실패 처리 (알림, 수동 개입 등)
      }
    }
  }
}

Choreography vs Orchestration 비교

측면ChoreographyOrchestration
복잡도분산되어 있음중앙 집중
결합도낮음 (느슨한 결합)높음 (Orchestrator 의존)
디버깅어려움 (흐름 추적 힘듦)쉬움 (중앙에서 관리)
확장성높음보통
SPOF없음Orchestrator
적합한 경우간단한 흐름, 서비스 자율성 중요복잡한 흐름, 중앙 제어 필요

보상 트랜잭션 설계 원칙

1. 멱등성 (Idempotency) 보장

같은 요청을 여러 번 실행해도 결과가 동일해야 함:

class PaymentService {
  async refund(orderId: string) {
    const existing = await this.refundRepo.findByOrderId(orderId);

    if (existing) {
      // 이미 환불됨, 중복 실행 방지
      return existing;
    }

    const refund = await this.processRefund(orderId);
    await this.refundRepo.save(refund);

    return refund;
  }
}

2. 보상 불가능한 작업 식별

일부 작업은 보상 불가능:

  • 이메일 발송 (이미 보냄)
  • 외부 API 호출 (취소 불가)
  • 로그 기록

해결책: 가능한 마지막에 수행

3. Saga 상태 저장

Saga 실행 중 장애 대비:

interface SagaLog {
  sagaId: string;
  status: 'STARTED' | 'COMPENSATING' | 'COMPLETED' | 'FAILED';
  steps: {
    stepName: string;
    status: 'PENDING' | 'COMPLETED' | 'COMPENSATED';
    timestamp: Date;
  }[];
}

// Saga 진행 상황 저장
await sagaLogRepo.save({
  sagaId: saga.id,
  status: 'STARTED',
  steps: [
    { stepName: 'CREATE_ORDER', status: 'COMPLETED', timestamp: new Date() },
    { stepName: 'PROCESS_PAYMENT', status: 'PENDING', timestamp: new Date() },
  ],
});

실전 체크리스트

Aggregate 설계

  • 트랜잭션 경계 명확히 정의
  • Aggregate 크기 최소화
  • 다른 Aggregate는 ID로 참조
  • Invariant (불변 조건) 정의

Saga 구현

  • 성공/실패 시나리오 문서화
  • 보상 트랜잭션 로직 구현
  • 멱등성 보장
  • Saga 상태 영속화
  • 타임아웃 설정

핵심 정리

  • Aggregate는 트랜잭션의 경계이자 일관성의 단위
  • Aggregate 간에는 최종 일관성 허용
  • Saga 패턴으로 분산 트랜잭션 해결
  • Choreography: 이벤트 기반, 낮은 결합도
  • Orchestration: 중앙 제어, 명확한 흐름
  • 보상 트랜잭션은 멱등성 보장 필수