Aggregate 심화와 트랜잭션 관리
학습 목표
- Aggregate 설계 원칙 심화
- 분산 트랜잭션 문제 이해
- Saga 패턴 구현 (Choreography vs Orchestration)
- 보상 트랜잭션 (Compensating Transaction)
Aggregate 설계 심화
Aggregate의 정의
Aggregate는 데이터 변경의 단위입니다.
핵심 원칙:
- 하나의 트랜잭션 안에서만 변경
- 외부에서는 Aggregate Root를 통해서만 접근
- 다른 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 비교
| 측면 | Choreography | Orchestration |
|---|---|---|
| 복잡도 | 분산되어 있음 | 중앙 집중 |
| 결합도 | 낮음 (느슨한 결합) | 높음 (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: 중앙 제어, 명확한 흐름
- 보상 트랜잭션은 멱등성 보장 필수