마이크로서비스패턴 트랜잭션 관리: 사가

존스노우·2024년 7월 10일
0

마이크로서비스 아키텍처에서의 트랜잭션 관리

트랜잭션 관리의 복잡성

  1. 단일 DB 모놀리식 애플리케이션: 간단한 트랜잭션 관리
  2. 다중 DB, 다중 서비스 환경: 복잡한 트랜잭션 관리 필요

분산 트랜잭션의 필요성

1.마이크로서비스 환경에서 여러 서비스의 데이터 일관성 유지 필요
예: createOrder() 작업에서 여러 서비스 데이터 접근 필요

분산 트랜잭션의 문제점

  1. 현대적 NoSQL DB, 메시지 브로커와 호환성 부족
  2. 동기 IPC로 인한 가용성 저하
  3. CAP 정리에 따른 일관성과 가용성 사이의 트레이드오프

사가 패턴 소개

  • 사가는 비동기 메시징을 이용하여 편성한 일련의 로컬 트랜잭션이다. 서비스 간 데이터 일관성은 사가로 유지
  1. 정의: 비동기 메시징을 이용한 일련의 로컬 트랜잭션
  2. 목적: 분산 트랜잭션 없이 데이터 일관성 유지

주문 생성 사가 예제

  1. 6단계의 로컬 트랜잭션으로 구성
  2. 각 단계별 서비스 작업 설명

사가의 롤백 메커니즘

  1. 보상 트랜잭션을 통한 변경사항 롤백
  2. 실패 시 역순으로 보상 트랜잭션 실행

주문 생성 사가의 트랜잭션 유형

  1. 보상 트랜잭션 (1~3번째 단계): 실패할 가능성이 있는 단계 다음에 오는 트랜잭션
  2. 피봇 트랜잭션 (4번째 단계): 절대로 실패하지 않는 단계 다음에 오는 트랜잭션
  • 4번째 단계 자체는 "절대로 실패하지 않는 단계 다음에 있다"고 표현되어 있음
  • 3번째 단계(주방 서비스의 createTicket())가 성공적으로 완료되면, 4번째 단계는 항상 실행될 수 있다는 의미
  1. 재시도 가능 트랜잭션 (5~6번째 단계): 항상 성공하는 트랜잭션
  • 이 단계들은 "항상 성공하기 때문"이라고 명시
간단한 보상트랜잭션 예시
public class CreateOrderSaga {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private KitchenService kitchenService;
    
    @Autowired
    private AccountingService accountingService;

    public void createOrder(Order order) {
        try {
            // 1. 주문 생성
            String orderId = orderService.createOrder(order);

            // 2. 주방 티켓 생성
            String ticketId = kitchenService.createTicket(orderId, order.getItems());

            // 3. 신용카드 승인
            boolean paymentAuthorized = accountingService.authorizePayment(order.getPaymentDetails());

            if (!paymentAuthorized) {
                // 신용카드 승인 실패 시 보상 트랜잭션 실행
                compensateCreateOrder(orderId, ticketId);
                throw new PaymentDeclinedException("Payment was declined");
            }

            // 4. 주문 승인
            orderService.approveOrder(orderId);
            kitchenService.approveTicket(ticketId);

        } catch (Exception e) {
            // 예외 발생 시 보상 트랜잭션 실행
            compensateCreateOrder(orderId, ticketId);
            throw e;
        }
    }

    private void compensateCreateOrder(String orderId, String ticketId) {
        // 보상 트랜잭션
        if (ticketId != null) {
            kitchenService.cancelTicket(ticketId);
        }
        if (orderId != null) {
            orderService.cancelOrder(orderId);
        }
    }
}


-----------------------------------------------------------------


public class KitchenService {
    public void cancelTicket(String ticketId) {
        // 티켓 상태를 CANCELLED로 변경
        Ticket ticket = ticketRepository.findById(ticketId);
        ticket.setStatus(TicketStatus.CANCELLED);
        ticketRepository.save(ticket);
        
        // 필요한 경우 관련 리소스 해제 또는 알림 전송
        notifyKitchenTicketCancelled(ticketId);
    }
}

public class OrderService {
    public void cancelOrder(String orderId) {
        // 주문 상태를 CANCELLED로 변경
        Order order = orderRepository.findById(orderId);
        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.save(order);
        
        // 고객에게 주문 취소 알림
        notifyCustomerOrderCancelled(orderId);
    }
}

구체적인 롤백 예시

  1. 신용카드 승인 실패 시의 보상 트랜잭션 순서 설명
  • 마이크로서비스 아키텍처에서 데이터 일관성을 유지하기 위해 분산 트랜잭션 대신 사가 패턴을 사용할 수 있다.
  • 사가는 비동기 메시징을 통해 여러 서비스의 로컬 트랜잭션을 조율하며, 실패 시 보상 트랜잭션을 통해 변경사항을 롤백한다.

사가 편성

사가 편성 방식

  1. 코레오그래피 (Choreography)
  • 참여자들이 이벤트를 교환하며 직접 의사 결정과 순서 조정
  1. 오케스트레이션 (Orchestration)
  • 중앙화된 사가 오케스트레이터가 참여자들에게 커맨드를 보내 작업 지시

코레오그래피 사가 구현 예시 (주문 생성 사가)

  1. 참여자들이 이벤트를 주고받으며 진행
  2. 각 단계에서 DB 업데이트 후 다음 참여자를 트리거하는 이벤트 발행
  3. 실패 시 보상 트랜잭션을 통한 롤백 처리
  • 어떤 사가 참여자가 주문을 거부해서 실패 이벤트가 발행되는 경우(예: 소비자 신용카드 승인 거부)를 대비해야 함

코레오그래피 사가의 통신 이슈

  1. 트랜잭셔널 메시징 필요성
  • 첫째, 사가 참여자가 자신의 DB를 업데이트하고, DB 트랜잭션의 일부로 이벤트를 발행하도록 해야 합니다.
  • 코레오그래피 사가는 단계별로 DB를 업데이트한 후 이벤트를 발행
  • DB를 업데이트하는 작업과 이벤트를 발행하는 작업은 원자적으로(atomically) 일어나야 함
  • 둘째 사가 참여자는 자신이 수신한 이벤트와 자신이 가진 데이터를 연관 지을 수 있어야 합니다.
  • 데이터를 매핑할 수 있도록 다른 사가 참여자가 상관관계 ID가 포함된 이벤트를 발행하는 것입니다.
  1. 상관관계 ID 사용
  • 이벤트와 관련 데이터를 연결하기 위해 사용 (예: orderId)

코레오그래피 사가의 장단점

  1. 장점:
  • 단순성: 비즈니스 객체 변경 시 자연스러운 이벤트 발행
  • 느슨한 결합: 참여자들이 직접적으로 서로를 알지 못함
  1. 단점:
  • 이해의 어려움: 구현 로직이 여러 서비스에 분산됨
  • 서비스 간 순환 의존성 가능성
  • 단단한 결합 위험: 참여자들이 많은 이벤트를 구독해야 할 수 있음

코레오그래피 vs 오케스트레이션

  1. 간단한 사가는 코레오그래피 방식 적합
  2. 복잡한 사가는 오케스트레이션 방식이 더 적합
  • 코레오그래피 사가는 분산 시스템에서 서비스 간 협력을 구현하는 한 방법으로,
  • 이벤트 기반 통신을 통해 느슨한 결합을 제공하지만,
  • 복잡성이 증가할수록 관리와 이해가 어려워질 수 있습니다.
  • 따라서 사가의 복잡성에 따라 적절한 편성 방식을 선택해야 합니다.

오케스트레이션 사가 개념

  1. 사가 오케스트레이터 클래스가 중심이 되어 사가 참여자들의 작업을 조정
  2. 커맨드/비동기 응답 방식으로 참여자들과 통신
  3. 오케스트레이터가 각 단계에서 수행할 작업을 참여자에게 지시
  4. 참여자의 응답에 따라 다음 단계를 결정하고 진행

주문 생성 사가 예시 (오케스트레이션 방식)

  • CreateOrderSaga 클래스가 오케스트레이터 역할 수행
    진행 순서:
    a) 오케스트레이터가 소비자 확인 커맨드를 소비자 서비스에 전송
    b) 소비자 서비스가 확인 후 응답
    c) 오케스트레이터가 티켓 생성 커맨드를 주방 서비스에 전송
    d) 주방 서비스가 티켓 생성 후 응답
    e) 오케스트레이터가 신용카드 승인 커맨드를 회계 서비스에 전송
    f) 회계 서비스가 승인 처리 후 응답
    g) 오케스트레이터가 티켓 승인 커맨드를 주방 서비스에 전송
    h) 오케스트레이터가 주문 승인 커맨드를 주문 서비스에 전송
    각 단계에서 참여자의 응답에 따라 다음 단계 결정 또는 보상 트랜잭션 실행

사가 오케스트레이터의 상태 기계 모델링

  • 상태 기계 모델링이란:
    상태 기계는 시스템이 가질 수 있는 모든 상태와, 한 상태에서 다른 상태로 변화하는 과정을 표현하는 방법
    이를 사가에 적용하면 복잡한 프로세스를 더 이해하기 쉽고 관리하기 쉽게 만들 수 있다.
  1. 상태 (States):
  • 주문 프로세스의 각 단계를 나타냅니다.
  • 예: "소비자 확인 중", "티켓 생성 중", "결제 승인 중", "주문 완료", "주문 실패"
  1. 이벤트 (Events):
  • 상태 변화를 일으키는 사건들입니다.
  • 예: "소비자 확인됨", "티켓 생성됨", "결제 승인됨", "결제 거부됨"
  1. 전이 (Transitions):
  • 하나의 상태에서 다른 상태로 변화하는 과정입니다.
  • 예: "소비자 확인 중" -> "소비자 확인됨" -> "티켓 생성 중"
  1. 액션 (Actions):
  • 각 상태나 전이 시 수행되는 작업입니다.
  • 예: "소비자 서비스에 확인 요청 보내기", "주방 서비스에 티켓 생성 요청 보내기"
  1. 구성 요소: 상태, 이벤트, 전이, 액션
  • 사가의 액션은 사가 참여자를 호출하는 작용
  • 주문 생성 사가의 상태:
    a) 소비자 확인: 초기 상태
    b) 티켓 생성: 주방 서비스의 응답 대기
    c) 신용카드 승인: 회계 서비스의 승인 대기
    d) 주문 승인됨: 성공적 완료를 나타내는 최종 상태
    e) 주문 거부됨: 실패를 나타내는 최종 상태
    상태 전이: 각 참여자의 응답에 따라 다음 상태로 이동
    액션: 각 상태에서 수행되는 작업 (예: 커맨드 전송)
    이점: 효율적인 테스트 가능, 설계/구현/테스트 용이성 증가

트랜잭셔널 메시징의 필요성

  1. 각 단계에서 DB 업데이트와 메시지 발행의 원자성 보장 필요
  2. 사가 참여자: DB 업데이트 후 응답 메시지 전송
  3. 오케스트레이터: 상태 업데이트 후 다음 참여자에게 커맨드 메시지 전송
  4. 트랜잭셔널 메시징으로 DB 작업과 메시지 처리의 일관성 유지

오케스트레이션 사가의 장단점

  • 장점:
    의존 관계 단순화: 순환 의존성 제거 (참여자는 오케스트레이터에 의존하지 않음)
    낮은 결합도: 서비스는 오케스트레이터 API만 구현, 다른 참여자의 이벤트 몰라도 됨
    관심사 분리와 비즈니스 로직 단순화:

  • 사가 편성 로직이 오케스트레이터에 집중
    도메인 객체 (예: Order 클래스)가 사가에 대해 알 필요 없음
    도메인 객체의 상태 기계 모델 단순화 (중간 상태 없이 APPROVAL_PENDING → APPROVED)

  • 단점:
    비즈니스 로직의 과도한 중앙화 가능성:
    "똑똑한 오케스트레이터"가 "깡통 서비스"들을 지시하는 구조 위험
    해결책: 오케스트레이터는 순서화만 담당, 비즈니스 로직은 각 서비스에 분산

비격리 문제

  1. 사가 사용 시 발생하는 주요 이슈로 언급
  2. ACID 트랜잭션의 격리성(I)이 사가에는 없음
  3. 이로 인한 문제 해결이 사가 구현의 주요 과제 중 하나
  • 오케스트레이션 사가의 개념, 구현 방식, 장단점, 그리고 고려사항을 상세히 설명.
  • 특히 주문 생성 사가 예시를 통해 실제 구현 과정을 구체적으로 보여주며,
  • 상태 기계 모델링을 통해 복잡한 사가의 관리 방법을 제시
  • 또한 트랜잭셔널 메시징의 필요성을 강조하여 분산 환경에서의 데이터 일관성 유지 방법을 설명

비격리 처리 문제

비격리 문제의 개요

  1. ACID 트랜잭션의 격리성(I)이 사가에서는 보장되지 않음
  2. 사가는 ACD(원자성, 일관성, 지속성) 트랜잭션으로 볼 수 있음
  • 비격리로 인한 문제:
    a) 소실된 업데이트: 한 사가의 변경을 다른 사가가 덮어씀
    b) 더티 읽기: 아직 완료되지 않은 변경을 다른 사가가 읽음
    c) 퍼지/반복 불가능한 읽기: 같은 데이터를 읽어도 결과가 다름

소실된 업데이트 예시

  • 소실된 업데이트는 한 사가의 변경분을 다른 사가가 덮어 쓸 때 일어납니다.
  1. 주문 생성 사가와 주문 취소 사가가 동시에 실행될 때 발생
  2. 주문 취소 사가의 변경이 주문 생성 사가에 의해 덮어쓰여짐
  3. 결국 주문 생서사가 마지막 단계에서 주문이 승인되 버림.

더티 읽기 예시

  • 더티 읽기는 한 사가가 업데이트 중인 데이터를 다른 사가가 읽을 때 발생
  1. 주문 취소 사가와 주문 생성 사가가 겹쳐 실행될 때 발생
  2. 신용 잔고가 일시적으로 증가한 상태에서 다른 사가가 읽어 문제 발생
  • ex) 소비자 서비스: 신용 잔고(available credit)를 늘립니다.

  • 주문 서비스: 주문을 취소 상태로 변경합니다.

  • 배달 서비스: 배달을 취소합니다

주문 취소 사가와 주문 생성 사가의 실행이 서로 겹쳐(interleaved) 실행 중인데, 소비자가 배달을 취소하기는 너무 늦어서 주문 취소 사가가 롤백되는 경우를 생각 소비자 서비스를 호출하는 트랜잭션 순서가 이렇게 엉켜 버릴 수 있겠죠.

  1. 주문 취소 사가: 신용 잔고를 늘립니다.

  2. 주문 생성 사가: 신용 잔고를 줄입니다.

  3. 주문 취소 사가: 신용 잔고를 줄이는 보상 트랜잭션이 가동됩니다.

  4. 결국 신용한도 초과 주문도 할 수 있게 되버림.

비격리 대책

  • a) 시맨틱 락 - 애플리케이션 수준의 락

레코드에 플래그를 세팅하여 처리 중임을 표시
예: Order.state 필드를 APPROVAL_PENDING 상태로 설정
장점: ACID 트랜잭션의 격리 기능 유사하게 구현 가능
단점: 락 관리 및 데드락 처리 로직 필요

  1. 주문 처리 과정:
  • 주문 생성
  • 재고 확인
  • 결제 처리
  • 배송 준비
  1. 시맨틱 락 사용:
  • 각 단계에서 다음과 같은 상태를 사용합니다.

  • ORDER_CREATED

  • STOCK_CHECKING

  • PAYMENT_PROCESSING

  • SHIPPING_PREPARING

  1. 데드락 발생 가능 시나리오:
  • 주문 A와 주문 B가 동시에 처리되고 있다고 가정해봅시다.

  • 주문 A:

  • 주문 생성 (ORDER_CREATED)

  • 재고 확인 중 (STOCK_CHECKING)

  • 결제 처리를 위해 대기 중

  • 주문 B:

  • 주문 생성 (ORDER_CREATED)

  • 결제 처리 중 (PAYMENT_PROCESSING)

  • 재고 확인을 위해 대기 중

  • 이 상황에서 주문 A는 결제 시스템의 락을 기다리고 있고, 주문 B는 재고 시스템의 락을 기다리고 있습니다. 둘 다 서로가 가진 리소스를 기다리면서 영원히 진행되지 않는 데드락 상황이 발생할 수 있습니다.

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class OrderProcessingSystem {

    private static final Lock stockLock = new ReentrantLock();
    private static final Lock paymentLock = new ReentrantLock();

    public void processOrder(String orderId) {
        try {
            setOrderState(orderId, "ORDER_CREATED");

            if (!acquireLock(stockLock, 5)) {
                throw new DeadlockException("재고 확인 락 획득 실패");
            }

            try {
                checkStock(orderId);
                setOrderState(orderId, "STOCK_CHECKING");

                if (!acquireLock(paymentLock, 5)) {
                    throw new DeadlockException("결제 처리 락 획득 실패");
                }

                try {
                    processPayment(orderId);
                    setOrderState(orderId, "PAYMENT_PROCESSING");

                    prepareShipping(orderId);
                    setOrderState(orderId, "SHIPPING_PREPARING");
                } finally {
                    paymentLock.unlock();
                }
            } finally {
                stockLock.unlock();
            }
        } catch (DeadlockException e) {
            handleDeadlock(orderId, e.getMessage());
        }
    }

    private boolean acquireLock(Lock lock, int timeoutSeconds) {
        try {
            return lock.tryLock(timeoutSeconds, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

    private void setOrderState(String orderId, String state) {
        System.out.println("주문 " + orderId + " 상태 변경: " + state);
        // 실제로는 데이터베이스에 상태를 저장하는 로직이 들어갑니다.
    }

    private void checkStock(String orderId) {
        System.out.println("주문 " + orderId + " 재고 확인 중");
        // 재고 확인 로직
    }

    private void processPayment(String orderId) {
        System.out.println("주문 " + orderId + " 결제 처리 중");
        // 결제 처리 로직
    }

    private void prepareShipping(String orderId) {
        System.out.println("주문 " + orderId + " 배송 준비 중");
        // 배송 준비 로직
    }

    private void handleDeadlock(String orderId, String errorMessage) {
        System.err.println("주문 " + orderId + "에서 데드락 발생: " + errorMessage);
        rollbackOrder(orderId);
        retryLater(orderId);
    }

    private void rollbackOrder(String orderId) {
        System.out.println("주문 " + orderId + " 롤백 중");
        // 주문 롤백 로직
    }

    private void retryLater(String orderId) {
        System.out.println("주문 " + orderId + " 나중에 재시도 예정");
        // 재시도 스케줄링 로직
    }

    private static class DeadlockException extends Exception {
        public DeadlockException(String message) {
            super(message);
        }
    }

    public static void main(String[] args) {
        OrderProcessingSystem system = new OrderProcessingSystem();
        system.processOrder("ORDER-001");
    }
}
  • b) 교환적 업데이트

업데이트를 순서에 상관없이 실행 가능하게 설계
예: 계좌의 입금과 출금 연산
장점: 소실된 업데이트 문제 방지

  • c) 비관적 관점

사가 단계의 순서를 재조정하여 비즈니스 리스크 최소화
예: 주문 취소 사가에서 신용 잔고 증가를 마지막 단계로 이동
장점: 더티 읽기로 인한 문제 감소

  • d) 값 다시 읽기

업데이트 전 값을 다시 읽어 변경 여부 확인
변경 시 사가 중단 후 재시작
장점: 소실된 업데이트 방지
단점: 성능 저하 가능성

  • e) 버전 파일

레코드에 수행된 작업을 순서대로 기록
비교환적 작업을 교환적 작업으로 변환
장점: 순서가 맞지 않는 요청 처리 가능

  • f) 값에 의한

비즈니스 위험성에 따라 동시성 메커니즘 선택
위험도 낮음: 사가 사용, 위험도 높음: 분산 트랜잭션 사용
장점: 유연한 전략 선택 가능

사가의 구조
사가는 세 가지 유형의 트랜잭션으로 구성

  • 사가의 피봇 트랜잭션은 authorizeCreditCard()입니다. 소비자 신용카드가 승인되면 이 사가는 반드시 완료됩니다.

  • a) 보상 가능 트랜잭션 (Compensatable Transaction):
    롤백이 가능한 트랜잭션
    실패 시 보상 트랜잭션을 통해 변경사항을 취소할 수 있음

  • b) 피봇 트랜잭션 (Pivot Transaction):
    사가의 진행/중단을 결정하는 지점
    이 트랜잭션이 커밋되면 사가는 반드시 완료됨
    보상 가능 트랜잭션도 아니고 재시도 가능 트랜잭션도 아님
    마지막 보상 가능 트랜잭션이거나 첫 번째 재시도 가능 트랜잭션일 수 있음

  • c) 재시도 가능 트랜잭션 (Retriable Transaction):
    피봇 트랜잭션 이후에 실행되는 트랜잭션
    반드시 성공해야 함
    실패 시 계속 재시도함

주문 생성 사가 예시:

  1. 보상 가능 트랜잭션: createOrder(), verifyConsumerDetails(), createTicket()
  2. 피봇 트랜잭션: authorizeCreditCard()
  3. 재시도 가능 트랜잭션: approveTicket(), approveOrder()

비격리 대책들

a) 시맨틱 락 (Semantic Lock):

  • 플래그를 세팅해서 다른 트랜잭션이 레코드에 접근하지 못하게 락(lock, 잠금)을 걸어 놓거나, 다른 트랜잭션이 해당 레코드를 처리할 때 조심하도록 경고(warning)합니다. 플래그는 재시도 가능 트랜잭션(사가 완료) 또는 보상 트랜잭션(사가 롤백)에 의해 해제됩니다.

  • Order.state 필드가 좋은 예입니다. *_PENDING 상태가 바로 시맨틱 락을 구현한 것

  • 보상 가능 트랜잭션이 레코드를 생성/수정할 때 플래그를 설정
    목적: 다른 트랜잭션에게 해당 레코드가 처리 중임을 알림
    예: Order.state 필드를 APPROVAL_PENDING으로 설정

  • 장점: ACID 트랜잭션의 격리 기능을 유사하게 구현 가능

  • 단점: 락 관리 및 데드락 처리 로직 필요

  • 구현 방법:
    실패 처리 후 클라이언트에 재시도 요청
    락 해제까지 블로킹

b) 교환적 업데이트 (Commutative Updates):

  • 업데이트를 순서에 상관없이 실행 가능하게 설계
  • 예: 계좌의 입금(credit)과 출금(debit) 연산
  • 장점: 소실된 업데이트 문제 방지

c) 비관적 관점 (Pessimistic View):

  • 사가 단계의 순서를 재조정하여 비즈니스 리스크 최소화
  • 예: 주문 취소 사가에서 신용 잔고 증가를 마지막 단계로 이동
  • 장점: 더티 읽기로 인한 문제 감소

d) 값 다시 읽기 (Reread Value):

  • 업데이트 전 값을 다시 읽어 변경 여부 확인
  • 변경 시 사가 중단 후 재시작
  • 낙관적락 패턴.
  • 장점: 소실된 업데이트 방지
  • 예: 주문 승인 전 주문 상태 재확인

e) 버전 파일 (Version File):

  • 레코드에 수행된 작업을 순서대로 기록

  • 비교환적 작업을 교환적 작업으로 변환

  • 장점: 순서가 맞지 않는 요청 처리 가능

  • 예: 회계 서비스에서 신용카드 승인/취소 요청 순서 관리

  • 주문취소 사가랑 생성 사가 동시실행시

  • 주문생성 사가가 소비자 신용카드 승인전 주문 취소사가가 해당 신용카드 승인을 취소하는 말도 안되는 상황 벌어 질 수 있음.

  • 순서가 안맞는 요청을 회계서비스가 받아 처리하려면, 작업이 도착하면 기록해두었다 정확한 순서대로 실행하면 됨.

  • 회계서비스는 일단 승인 취소요청 기록하고, 나중에 승인 요청 도착하면 이미 승인 취소 요청이 접수 상태이니 승인 작업을 생략해도 되겠구나 인지.

f) 값에 의한 (By Value):

  • 비즈니스 위험성에 따라 동시성 메커니즘 선택
  • 위험도 낮음: 사가 사용, 위험도 높음: 분산 트랜잭션 사용
  • 장점: 유연한 전략 선택 가능

정리

  • 사가 패턴을 사용할 때 발생할 수 있는 비격리 문제를 해결하기 위한 다양한 전략을 제시
  • 각 대책은 특정 상황에 적합한 해결책을 제공하며, 개발자는 이를 상황에 맞게 선택하거나 조합하여 사용할 수 있습니다

주문 서비스 및 주문 생성 사가 설계

주문 서비스 구조

  • OrderService: 비즈니스 로직이 포함된 클래스
  • Order: 주문 도메인 객체
  • CreateOrderSaga: 주문 생성 사가를 오케스트레이션하는 클래스
  • OrderCommandHandlers: 사가의 커맨드 메시지를 처리하는 어댑터 클래스
  • 사가 오케스트레이터인 주문 서비스는 그 자신이 사가 참여자이기도 한 서비스

OrderService 클래스

@Transactional ← 서비스 메서드에 트랜잭션을 적용
public class OrderService {

  @Autowired
  private SagaManager<CreateOrderSagaState> createOrderSagaManager;

  @Autowired
  private OrderRepository orderRepository;

  @Autowired
  private DomainEventPublisher eventPublisher;

  public Order createOrder(OrderDetails orderDetails) {
    ...
    ResultWithEvents<Order> orderAndEvents = Order.createOrder(...); ← Order 생성
    Order order = orderAndEvents.result;
    OrderRepository.save(order);  ← DB에 Order 저장

    eventPublisher.publish(Order.class,  ← 도메인 이벤트 발행
                          Long.toString(order.getId()),
                          orderAndEvents.events);

    CreateOrderSagaState data =
        new CreateOrderSagaState(order.getId(), orderDetails);  ← CreateOrdersaga 생성
    CreateOrderSagaManager.create(data, Order.class, order.getId());

    return order;
  }

  ...
}
  • 주문 생성/관리를 담당하는 도메인 서비스

  • Order 생성/수정, OrderRepository를 통한 저장, SagaManager를 이용한 사가 생성

  • createOrder() 메서드:

  • a) Order 생성 및 저장

  • b) 도메인 이벤트 발행

  • c) CreateOrderSaga 생성

  • CreateOrderSaga: 사가의 상태 기계를 정의한 싱글턴 클래스(singleton class) Create OrderSagaState로 커맨드 메시지를 생성하고, 사가 참여자 프록시 클래스(예: Kitchen ServiceProxy)가 지정한 메시지 채널을 통해 참여자에게 메시지를 전달합니다.

  • CreateOrderSagaState: 사가의 저장 상태. 커맨드 메시지를 생성합니다.

  • 사가 참여자 프록시 클래스: 프록시 클래스마다 커맨드 채널, 커맨드 메시지 타입, 반환형으로 구성된 사가 참여자의 메시징 API를 정의합니다.

    CreateOrderSaga 클래스

  • 사가의 상태 기계를 정의한 싱글턴 클래스

  • SimpleSaga 인터페이스 구현

  • 사가 데피니션 정의:

  • a) 각 단계별 참여자 호출 (invokeParticipant)

  • b) 응답 처리 (onReply)

  • c) 보상 트랜잭션 정의 (withCompensation)

public class CreateOrderSaga implements SimpleSaga<CreateOrderSagaState> {

  private SagaDefinition<CreateOrderSagaState> sagaDefinition;

  public CreateOrderSaga(OrderServiceProxy orderService,
                         ConsumerServiceProxy consumerService,
                         KitchenServiceProxy kitchenService,
                         AccountingServiceProxy accountingService) {

    this.sagaDefinition =
            step()
            .withCompensation(orderService.reject,
                    CreateOrderSagaState::makeRejectOrderCommand)

            .step()
              .invokeParticipant(consumerService.validateOrder,
                    CreateOrderSagaState::makeValidateOrderByConsumerCommand)
            .step()
              .invokeParticipant(kitchenService.create,
                    CreateOrderSagaState::makeCreateTicketCommand)
            .onReply(CreateTicketReply.class,
               CreateOrderSagaState::handleCreateTicketReply)
            .withCompensation(kitchenService.cancel,
                    CreateOrderSagaState::makeCancelCreateTicketCommand)
            .step()
              .invokeParticipant(accountingService.authorize,
                    CreateOrderSagaState::makeAuthorizeCommand)
            .step()
              .invokeParticipant(kitchenService.confirmCreate,
                    CreateOrderSagaState::makeConfirmCreateTicketCommand)
            .step()
              .invokeParticipant(orderService.approve,
                    CreateOrderSagaState::makeApproveOrderCommand)
            .build();
  }

  @Override
  public SagaDefinition<CreateOrderSagaState> getSagaDefinition() {
    return sagaDefinition;
  }
}
blic class CreateOrderSaga ...

  public CreateOrderSaga(..., KitchenServiceProxy kitchenService,
            ...) {
    ...
    .step()
      .invokeParticipant(kitchenService.create,  ← 포워드 트랜잭션 정의
                CreateOrderSagaState::makeCreateTicketCommand)
      .onReply(CreateTicketReply.class,
                CreateOrderSagaState::handleCreateTicketReply) ← 성공 응답을 수신하면 handleCreateTicketReply( ) 호출
      .withCompensation(kitchenService.cancel, ← 보상 트랜잭션 정의
                CreateOrderSagaState::makeCancelCreateTicketCommand)
    ...
    ;
    

CreateOrderSagaState 클래스

public class CreateOrderSagaState {

  private Long orderId;
  private OrderDetails orderDetails;
  private long ticketId;

  public Long getOrderId() {
    return orderId;
  }

  private CreateOrderSagaState() {
  }

  public CreateOrderSagaState(Long orderId, OrderDetails orderDetails) { ← Orderservice가 호출하여 CreateOrdersagastate 인스턴스를 생성 
    this.orderId = orderId;
    this.orderDetails = orderDetails;
  }

  CreateTicket makeCreateTicketCommand() {  ← CreateTicket 커맨드 메시지 생성
    return new CreateTicket(getOrderDetails().getRestaurantId(),
                  getOrderId(), makeTicketDetails(getOrderDetails()));
  }

  void handleCreateTicketReply(CreateTicketReply reply) {  ← 새로 만든 티켓 ID 저장
    logger.debug("getTicketId {}", reply.getTicketId());
    setTicketId(reply.getTicketId());
  }

  CancelCreateTicket makeCancelCreateTicketCommand() {  ← CancelCreateTicket 커맨드 메시지 생성
    return new CancelCreateTicket(getOrderId());
  }

  ...
  
  • 사가 인스턴스의 상태를 나타내는 클래스
  • 주요 역할: 사가 참여자에게 보낼 메시지 생성
  • 각 단계별 커맨드 메시지 생성 메서드 포함

KitchenServiceProxy 클래스

public class KitchenServiceProxy {

  public final CommandEndpoint<CreateTicket> create =
      CommandEndpointBuilder
          .forCommand(CreateTicket.class)
          .withChannel(
              KitchenServiceChannels.kitchenServiceChannel)
          .withReply(CreateTicketReply.class)
          .build();

  public final CommandEndpoint<ConfirmCreateTicket> confirmCreate =
      CommandEndpointBuilder
          .forCommand(ConfirmCreateTicket.class)
          .withChannel(
              KitchenServiceChannels.kitchenServiceChannel)
          .withReply(Success.class)
          .build();

  public final CommandEndpoint<CancelCreateTicket> cancel =
      CommandEndpointBuilder
          .forCommand(CancelCreateTicket.class)
          .withChannel(
              KitchenServiceChannels.kitchenServiceChannel)
          .withReply(Success.class)
          .build();
}
  • 주방 서비스의 커맨드 메시지 끝점 정의
  • 각 CommandEndpoint에 커맨드 타입, 목적지 채널, 예상 응답 타입 지정

이벤추에이트 트램 사가 프레임워크


  • sagas.orchestration 패키지:
  • a) SimpleSaga 인터페이스
  • b) SagaManager 클래스 (사가 인스턴스 생성/관리)
    사가 생성 및 실행 과정:
  • a) OrderService가 CreateOrderSagaState 생성
  • b) SagaManager가 사가 인스턴스 생성
  • c) 첫 번째 단계 실행 및 커맨드 메시지 전송
  • d) 사가 인스턴스 DB 저장
  • 응답 처리 과정:
  • a) 응답 수신 및 사가 인스턴스 조회
  • b) 다음 단계 실행 및 커맨드 메시지 전송
  • c) 업데이트된 사가 인스턴스 DB 저장

OrderCommandHandlers 클래스

public class OrderCommandHandlers {

  @Autowired
  private OrderService orderService;

  public CommandHandlers commandHandlers() { ← 커맨드 메시지를 각각 적절한 핸들러 메서드로 라우팅
    return SagaCommandHandlersBuilder
      .fromChannel("orderService")
      .onMessage(ApproveOrderCommand.class, this::approveOrder)
      .onMessage(RejectOrderCommand.class, this::rejectOrder)
      ...
      .build();
  }

  public Message approveOrder(CommandMessage<ApproveOrderCommand> cm) {
    long orderId = cm.getCommand().getOrderId(); 
    OrderService.approveOrder(orderId);  ← Order를 승인 상태로 변경
    return withSuccess();  ← 제네릭 성공 메시지 반환
  }

  public Message rejectOrder(CommandMessage<RejectOrderCommand> cm) {
    long orderId = cm.getCommand().getOrderId();
    OrderService.rejectOrder(orderId); ← Order를 거부 상태로 변경
    return withSuccess();
  }
  • 사가가 전송한 커맨드 메시지 처리
  • commandHandlers() 메서드로 메시지 타입별 핸들러 매핑
  • 각 핸들러 메서드는 OrderService 호출 후 응답 메시지 반환

OrderServiceConfiguration 클래스

@Configuration
public class OrderServiceConfiguration {

  @Bean
  public OrderService orderService(RestaurantRepository restaurantRepository,
    ...
    SagaManager<CreateOrderSagaState> createOrderSagaManager,
  ...) {
    return new OrderService(restaurantRepository,
      ...
      CreateOrderSagaManager
      ...);
  }

  @Bean
  public SagaManager<CreateOrderSagaState> createOrderSagaManager(
    CreateOrderSaga saga) {
    return new SagaManagerImpl<>(saga);
  }

  @Bean
  public CreateOrderSaga createOrderSaga(OrderServiceProxy orderService,
    ConsumerServiceProxy consumerService, ...) {
    return new CreateOrderSaga(orderService, consumerService, ...);
  }

  @Bean
  public OrderCommandHandlers orderCommandHandlers() {
    return new OrderCommandHandlers();
  }

  @Bean
  public SagaCommandDispatcher orderCommandHandlersDispatcher(
    OrderCommandHandlers orderCommandHandlers) {
    return new SagaCommandDispatcher("orderService",
      orderCommandHandlers.commandHandlers());
  }

  @Bean
  public KitchenServiceProxy kitchenServiceProxy() {
    return new KitchenServiceProxy();
  }

  @Bean
  public OrderServiceProxy orderServiceProxy() {
    return new OrderServiceProxy();
  }

  ...
}
  • 스프링 빈 구성 클래스
  • 다양한 컴포넌트의 빈 정의 (OrderService, CreateOrderSaga, OrderCommandHandlers 등)
  • 핵심 메시지는 마이크로서비스 아키텍처에서 사가 패턴을 사용한 트랜잭션 관리 방법과 그 구현 details
profile
어제의 나보다 한걸음 더

0개의 댓글