대용량 트래픽 & 데이터 처리 3

anvel·2025년 8월 31일

항해 플러스

목록 보기
37/39

Saga패턴,Outbox

대용량 트래픽 & 데이터 처리

이번 주차는 저번주에 작성했던 Redis를 이용한 실시간 집계처리에 대하여 트랜젝션이 완료된 이후에 정합성이 보장되도록 집계하는 것과 외부 Mock API를 동시에 호출하여 주요 로직과 분리를 보장하는 내용을 실제로 구현하는 것이었습니다.

또한, 분산 트렌잭션을 적용해야하는 상황을 위한 이벤트 기반 Saga 패턴을 설계해보는 과정이었습니다.

부가 로직의 관심사 분리

목적

주문 생성 시 핵심 트랜잭션과 부가기능(집계, 알림 등)을 분리하여 안정성과 가독성 확보

설계 방향

  • 주문 생성은 OrderFacade 내에서 트랜잭션 단위로 처리
  • 주문 생성 완료 시점에 주문 완료 이벤트(OrderCompletedEvent) 발행
  • 부가 로직은 별도 이벤트 핸들러에서 수행
  • 동일 이벤트에 대해 내부 집계(TopProduct) 와 외부 집계(Mock External) 가 동시에 동작하도록 구현

이벤트 흐름

  • OrderFacade.placeOrder()
    • 사용자 검증, 상품 재고 차감, 쿠폰 사용, 잔액 차감, 주문 생성 처리
    • 완료 후 OrderCompletedEvent 발행
  • TopProductEventHandler
    • OrderCompletedEvent 수신
    • 주문 상품 정보를 기반으로 랭킹 집계 수행
  • OrderExternalEventHandler(Mock External)
    • 동일하게 OrderCompletedEvent 수신
    • 외부 시스템에 전달되는 시뮬레이션을 Redis 저장소에 기록
    • 학습 목적으로 외부 API 대신 로그 기록 및 Redis 저장소(OrderExternalRedisRepository)를 사용

기능 구현

이벤트 관련 클래스 구현

  • OrderCompletedEvent.java

    public record OrderCompletedEvent(Long orderId, Long userId, List<OrderLineSummary> lines) {
    }
    • 주문 생성 성공 이벤트
  • TopProductEventHandler.java

    @Component
    @RequiredArgsConstructor
    public class TopProductEventHandler {
    
        private final TopProductService topProductService;
    
        @Async
        @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
        public void on(OrderCompletedEvent event) {
            topProductService.recordOrdersAsync(event.orderId(), event.lines());
        }
    }
    • AFTER_COMMIT 에 의해 트랜젝션이 커밋 되고 나서 이벤트 동작으로 구현
    • 주문 생성 함수의 OrderCompletedEvent를 수집하여 TopProductService에 집계 요청
  • TopProductService.java

    @Service
    @RequiredArgsConstructor
    public class TopProductService {
        /* ... */
        @Async
        public void recordOrdersAsync(Long orderId, List<OrderLineSummary> lines) {
            if (redisRepository.isAlreadyIssued(orderId)) {
                return;
            }
            try {
                redisRepository.recordOrders(lines.stream().map(TopProductMapper::toRecord).toList());
                redisRepository.markIssued(orderId);
            } catch (Exception ignored) {
            }
        }
        /* ... */
    }
    • 기록된 주문인지 확인
    • 상품 번호 및 수량 배열로 RedisRepository에 기록
    • 주문 번호 기록
  • TopProductRedisRepository.java

    @Repository
    @RequiredArgsConstructor
    public class TopProductRedisRepository {
        /* ... */
      
        private static final int TTL_DAYS = 4;
        private static final String PRODUCT_RANKING_PREFIX = "RANKING:PRODUCT:";
        private static final String ISSUED_ORDER_SET = PRODUCT_RANKING_PREFIX + "ISSUED";
    
        private String getDailyKey(LocalDate date) {
            return PRODUCT_RANKING_PREFIX + date.format(FORMATTER);
        }
    
        public boolean isAlreadyIssued(Long orderId) {
            return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(ISSUED_ORDER_SET, orderId.toString()));
        }
    
        public void markIssued(Long orderId) {
            redisTemplate.opsForSet().add(ISSUED_ORDER_SET, orderId.toString());
            redisTemplate.expire(ISSUED_ORDER_SET, Duration.ofDays(TTL_DAYS));
        }
    
        public void recordOrders(List<TopProductRecord> items) {
            String key = getDailyKey(LocalDate.now());
    
            for (TopProductRecord item : items) {
                redisTemplate.opsForZSet().incrementScore(key, item.productId(), item.soldQty());
            }
    
            redisTemplate.expire(key, Duration.ofDays(TTL_DAYS));
        }
        
        /* ... */
    }
    • isAlreadyIssued: SET 자료구조의 중복 발급 확인 함수
    • markIssued: 상품별 수량 증가 후 완료된 주문 기록, TTL은 집계함수와 동일하게 설정
    • recordOrders: 상품 배열 기반으로 날짜별 판매량 을 ZSET에 기록

집계 및 외부 송신 테스트

외부 전송 Mock 핸들러 구현

  • OrderExternalEventHandler.java

    • OrderExternalEventHandler 외부 Mock API 핸들러: 호출 결과를 Redis에 기록

      @Slf4j
      @Component
      @RequiredArgsConstructor
      public class OrderExternalEventHandler {
      
          private final OrderExternalRedisRepository redisRepository;
      
          @Async
          @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
          public void on(OrderCompletedEvent event) {
              log.info("[MOCK-EXTERNAL] 주문 완료 이벤트 전송: orderId={}, items={}",
                      event.orderId(), event.lines().size());
      
              mockSendToExternalSystem(event);
          }
      
          private void mockSendToExternalSystem(OrderCompletedEvent event) {
              log.debug("[MOCK-EXTERNAL] 외부 MOCK 이벤트 송신 - orderId={}, userId={}",
                      event.orderId(), event.userId());
      
              redisRepository.recordSent(event.orderId(), event.userId());
          }
      }
  • OrderExternalRedisRepository.java

    @Repository
    @RequiredArgsConstructor
    public class OrderExternalRedisRepository {
    
        private final StringRedisTemplate redisTemplate;
        private static final String KEY_PREFIX = "EXTERNAL:ORDER:";
    
        public void recordSent(Long orderId, Long userId) {
            String key = KEY_PREFIX + orderId;
            String value = "sentAt=" + LocalDateTime.now() + ", userId=" + userId;
            redisTemplate.opsForList().rightPush(key, value);
        }
    
        public Long countRecords(Long orderId) {
            return redisTemplate.opsForList().size(KEY_PREFIX + orderId);
        }
    
        public void clear(Long orderId) {
            redisTemplate.delete(KEY_PREFIX + orderId);
        }
    }

학습 내용

Saga Pattern

  • Orchestration: Orchestrator가 모든 흐름을 관리

    • 순차적으로 각 도메인을 호출하고, 실패 시 보상 트랜잭션을 지시
    • 흐름이 명확하고 제어가 용이하지만, Orchestrator가 과도한 책임을 가지며, 확장성/유연성이 떨어질 수 있음
  • Choreography: 각 도메인이 이벤트를 발행/구독하며 Saga를 진행

    • 도메인간 결합성이 줄어들고 확장성이 뛰어남
    • 흐름이 분산되어있어 추적/디버깅이 어려움

도메인 별 트랜젝션이 분리

목적

분산 환경을 고려한 도메인별 책임의 트랙젠션 로직 설계
현재 비즈니스 로직 중 다수의 도메인이 모두 연계되어 처리되는 상태를 단일 트랜젝션에서 관리한다면 일관성의 보장은 쉽지만, 확장성이 떨어진다.
분산 트

발생 가능한 문제

  • 부분 실패
    • Order는 생성되었지만 Product에서 재고 차감 실패
    • Coupon 사용 처리 후 Balance 차감 실패 → 쿠폰만 소모된 상태
  • 이벤트 순서 문제
    • StockReservedEvent보다 CouponUsedEvent가 먼저 도착하거나, 중복 전달되는 경우
  • 중복 처리
    • 동일 이벤트가 두 번 소비되면 잔액이 이중 차감될 위험
  • 최종 일관성 지연
    • 쓰기(Command)는 즉시 반영되었으나, 조회(Query)는 이벤트 동기화 지연으로 잠시 다른 상태 보일 수 있음
  • 보상 트랜잭션 필요성
    • Balance 차감 실패 시 → 이미 차감된 재고/쿠폰을 원복해야 함

설계 방향

1. 주문 시작

  • OrderCommandService가 사용자 요청을 받아 Order 엔티티를 OrderStatus.DRAFT로 생성
  • 생성 직후 OrderSagaRepository에 초기 상태를 기록한 뒤 OrderRequestedEvent 발행
    • 커밋 이후에 DRAFT 상태로 주문을 커밋/저장하고, 이를 자체 OrderDraftedEvent 감지하여 Saga 저장소에 저장
      • 초기 상태를 OrderSagaRepository에 저장하는 타이밍이 어려웠습니다.
      • PENDING 앞에 상태를 하나 더 둬서 다른 도메인이 호출할 Saga 저장소가 확실히 존재하도록 단계를 나눴습니다.
  • 주문 상태를 PENDING으로 다른 도메인에 전이

2. 이벤트 흐름 및 도메인 역할

sequenceDiagram
    participant Order
    participant Saga
    participant Product
    participant Coupon
    participant Balance

    Order->>Saga: OrderDraftedEvent
    Saga->>Order: OrderRequestedEvent
    Saga->>Product: OrderRequestedEvent
    Saga->>Coupon: OrderRequestedEvent
    Product-->>Saga: StockReservedEvent
    Coupon-->>Saga: CouponUsedEvent
    Saga->>Balance: OrderCalculatedEvent
    Balance-->>Saga: BalanceDeductedEvent
    Saga->>Order: OrderCompletedEvent
  • OrderRequestedEvent 발행

    • 각 도메인이 이를 구독하여 각각의 트렌젝션에서 로직 실행
    • Product 도메인: 재고 확인 및 차감 시도
      • 성공 시 StockReservedEvent, PriceQuotedEvent 발행
      • 실패 시 StockReserveFailedEvent 발행
    • Coupon 도메인: 쿠폰이 있으면 유효성 확인 및 사용 처리
      • 사용 시 CouponUsedEvent 발행
      • 실패 시 CouponUseFailedEvent 발행
      • 미사용 시에는 CouponSkippedEvent 발행 (혹은 0원으로 성공 처리)
    • Balance 도메인: 총액 및 차감액을 바탕으로 잔액 차감
      • 성공 이벤트(PriceQuotedEvent, CouponUsedEvent/CouponSkippedEvent)를 기반으로 최종 결제 금액 계산
      • OrderSagaHandler가 발행한 OrderCalculatedEvent에 의해 잔액 차감 수행
      • 결제 가능 여부 확인 후 차감
      • 성공 시 BalanceDeductedEvent 발행
      • 실패 시 BalanceDeductionFailedEvent 발행
  • OrderSagaHandler: 위 이벤트를 구독하여 Saga 상태를 갱신

    • 모든 Product, Coupon 도메인의 성공 이벤트 수집
      • ProductStockReservedEventPriceQuotedEvent 수집
      • CouponCouponUsedEvent 또는 CouponSkippedEvent 수집
      • BalanceBalanceDeductedEvent 수집
    • [추가] OrderCalculatedEvent 발행 (StockReservedEvent, CouponUsedEvent 수집 시)
      • BalanceBalanceDeductedEvent 수집
    • 최종적으로 OrderCompletedEvent 발행
      • OrderEventHandlerOrderStatus.PAID 상태 변경
      • 집계, 외부 전송 핸들러가 이를 수집

3. 보상 트랜잭션 설계 - Compensation

stateDiagram-v2
    [*] --> Draft
    Draft --> Pending: OrderRequested
    Pending --> Paid: BalanceDeducted
    Pending --> Failed: StockReserveFailed / CouponUseFailed / BalanceDeductionFailed
    Failed --> Restored: CompensationHandlers
    Restored --> Cancelled
  • 각 도메인에서 실패 시 원복 이벤트를 발행
    • Product: StockReserveFailedEvent → 이미 차감된 재고 복구(트랜젝션에 의한 롤백), 쿠폰 사용 원복
    • Coupon: CouponUseFailedEvent → 쿠폰 사용 상태 되돌림(트랜젝션에 의한 롤백), 상품 재고 원복
    • Balance: BalanceDeductionFailedEvent → 차감된 금액 환불(트랜젝션에 의한 롤백), 쿠폰 사용 원복, 상품 재고 원복
  • 각 도메인은 다른 도메인의 실패 여부를 수집하고, 실행여부를 확인하여 원복 로직 수행
  • 다른 도메인의 실패 이벤트로 인한 원복 쿼리와 원래 트랜젝션 쿼리의 충돌이 발생할 수도 있고,
  • 실패 이벤트가 먼저 수집된 뒤에 정상 이벤트를 수집하게 되면 실패가 무시되어 실행될 수 있음
  • 각 보상 핸들러는 orderIdSagaState를 조회해 해당 도메인이 실제로 SUCCESS였던 경우에만 복구
  • Saga 저장소에서 도메인별 성공/실패/취소됨/복구됨 상태를 관리

4. 동시성 제어 설계

  • 단일 자원 충돌을 막기 위해 Redisson 기반 @DistributedLock 적용
  • 주요 기준 키
    • Product: productId 기준 → reserveStock / restoreStock
    • Coupon: couponId 기준 → useCoupon / restoreCoupon
    • Balance: userId 기준 → deductBalance / restoreBalance
  • Order 단계에서는 멀티락을 사용하지 않음
    • 주문은 초안(DRAFT)만 생성하고, 실제 차감은 도메인별로 병렬적으로 수행되므로 각 도메인에서 개별 락을 적용
    • userId 기준으로 중복 요청방지를 위한 락 적용
  • @Version 기반 낙관적 락, 조건부 업데이트 쿼리 병행 → DB 일관성과 Redis 분산 환경 모두에서 안전성 확보

5. CQRS 설계

  • 각 도메인 별 Command, Query 모델을 분리하여 수정과 조회 로직을 분산
  • 롤백이 필요한 수정 함수만 트랜젝션 내에서 처리 가능
  • 다만, 지금 모든 도메인이 각자 상태값을 바꾸는 설계로 구현하여 현재 주문 생성 중에는 각 도메인의 Command 부분만 사용

6. Saga 상태 저장소

  • 발급된 Saga의 현재 상태를 추적하기 위한 저장소, JPA 혹은 Redis에 구현 가능

  • 모든 도메인이 이벤트 기반으로 움직이기 때문에, 영속성을 강하게 가져가기 위해 JPA 기반으로 구현

  • OrderSagaState: 필요 상태 정보

    • 주문 메타 정보
      • orderId: 최초 DRAFT의 상태의 주문 정보 PK
      • userId: 사용자 ID
      • items: 사용자가 요청한 item 배열
        • productId: 상품 번호
        • quantity: 상품 수량
      • couponId: 쿠폰 사용 시 쿠폰 번호
    • 각 도메인 별 트랜젝션 수행 정보: SUCCESS, FAILED, CANCELED, RESTORED
      • productReserved: 상품 재고 차감 수행, 총액 반환 여부
      • couponApplied: 쿠폰 사용 여부
      • balanceCharged: 잔액 차감 여부
    • 잔액 계산을 위한 결과값 정보 / 이벤트에 담아서도 전송하지만, 소실되어 이벤트를 재발행하거나, 원복해야할 내용 참조를 위해
      • subTotalAmount: 상품 총액 원가
      • discountAmount: 쿠폰 할인액
      • totalAmount: 최종 금액

기능 구현을 위한 필요 클래스

상태 저장소 구성

  • OrderSagaRepository: Saga 상태 저장소

  • OrderSagaEventStatus (enum: PENDING, SUCCESS, FAILED, CANCELED, RESTORED)

  • OrderSagaState: 주문 Saga 상태값

    주요 코드
    @Entity
    @Table(name = "order_saga_state")
    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @AllArgsConstructor
    @Builder
    public class OrderSagaState {
    
        @Id
        private Long orderId;   // 주문 ID (Order 엔티티 PK와 동일)
    
        private Long userId;
    
        @ElementCollection
        @CollectionTable(name = "order_saga_items", joinColumns = @JoinColumn(name = "order_id"))
        private List<OrderSagaItem> items;
    
        private Long couponId;
    
        // 각 도메인 상태
        @Enumerated(EnumType.STRING)
        private OrderSagaEventStatus productReserved;
    
        @Enumerated(EnumType.STRING)
        private OrderSagaEventStatus couponApplied;
    
        @Enumerated(EnumType.STRING)
        private OrderSagaEventStatus balanceCharged;
    
        // 금액 정보
        private Integer subTotalAmount;
        private Integer discountAmount;
        private Integer totalAmount;
    
        public void markProductReservedSuccess(int subTotalAmount) {
            this.productReserved = OrderSagaEventStatus.SUCCESS;
            this.subTotalAmount = subTotalAmount;
        }
    
        public void markCouponAppliedSuccess(int discountAmount) {
            this.couponApplied = OrderSagaEventStatus.SUCCESS;
            this.discountAmount = discountAmount;
        }
    
        public void markBalanceChargedSuccess(int totalAmount) {
            this.balanceCharged = OrderSagaEventStatus.SUCCESS;
            this.totalAmount = totalAmount;
        }
    
        public void markFailedDomain(String domain) {
            switch (domain) {
                case "PRODUCT" -> this.productReserved = OrderSagaEventStatus.FAILED;
                case "COUPON" -> this.couponApplied = OrderSagaEventStatus.FAILED;
                case "BALANCE" -> this.balanceCharged = OrderSagaEventStatus.FAILED;
            }
        }
    
        public boolean isReadyForCalculation() {
            return productReserved == OrderSagaEventStatus.SUCCESS
                    && couponApplied == OrderSagaEventStatus.SUCCESS
                    && subTotalAmount != null
                    && discountAmount != null;
        }
    }

도메인 이벤트

  • Order
    • OrderDraftedEvent: 초안 생성됨 이벤트, 자기 참조용
    • OrderRequestedEvent: 주문 요청됨 이벤트, 각 도메인 참조용
    • OrderCalculatedEvent: [추가] 상품 총액과 쿠폰 차감액이 성공적으로 반환되었을 때, OrderSagaHandler.tryTriggerOrderCalculated에 의해 발행
    • OrderCompletedEvent: 주문 완료 이벤트
    • OrderFailedEvent: 주문 실패 이벤트
  • Product
    • StockReservedEvent: 재고 차감 완료 이벤트
    • PriceQuotedEvent: 주문 총액 이벤트(재고 차감 이벤트와 통합 가능) - 제외
    • StockReserveFailedEvent: 재고 차감 실패 이벤트
  • Coupon
    • CouponUsedEvent: 쿠폰 사용 이벤트 + 할인액 반환
    • CouponSkippedEvent: 쿠폰 미사용 이벤트(쿠폰 0원 사용 이벤트로 도 가능 할 것으로 보임) - 제외
    • CouponUseFailedEvent: 쿠폰 사용 실패 이벤트
  • Balance
    • BalanceDeductedEvent: 잔액 차감 이벤트
    • BalanceDeductionFailedEvent: 잔액 차감 실패 이벤트

Saga 상태 머신 핸들러

  • OrderSagaHandler: Saga의 상태 관리 메타 데이터 및 부가 정보 관리

    주요 코드
    @Slf4j
    @Component
    @RequiredArgsConstructor
    public class OrderSagaHandler {
    
        private final ApplicationEventPublisher publisher;
        private final OrderSagaRepository sagaRepository;
    
        // 주문 조안 생성 시 감지 후 저장
        @Async
        @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
        public void on(OrderDraftedEvent event) {
            // 초안 저장 시 초기 Saga 상태 저장
            OrderSagaState sagaState = OrderSagaState.builder()
                    .orderId(event.orderId())
                    .userId(event.userId())
                    .items(event.items())
                    .couponId(event.couponId())
                    .productReserved(OrderSagaEventStatus.PENDING)
                    .couponApplied(OrderSagaEventStatus.PENDING)
                    .balanceCharged(OrderSagaEventStatus.PENDING)
                    .build();
    
            sagaRepository.save(sagaState);
            log.info("[SAGA] Drafted order={}, user={} saga initialized", event.orderId(), event.userId());
    
            // Saga 저장소 생성 후 각 도메인별 주문 요청 이벤트 발행
            publisher.publishEvent(
                    new OrderRequestedEvent(event.orderId(), event.userId(), event.items(), event.couponId())
            );
        }
    
        private void tryTriggerOrderCalculated(OrderSagaState saga) {
            if (saga.isReadyForCalculation()) {
                int total = saga.getSubTotalAmount() - saga.getDiscountAmount();
                publisher.publishEvent(new OrderCalculatedEvent(
                        saga.getOrderId(),
                        saga.getUserId(),
                        total,
                        saga.getItems(),
                        saga.getCouponId()
                ));
                log.info("[SAGA] order={} calculation prepared → total={}", saga.getOrderId(), total);
            }
        }
    
        // 재고 차감 상태 업데이트
        @Async
        @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
        public void on(StockReservedEvent event) {
            sagaRepository.findById(event.orderId()).ifPresent(saga -> {
                saga.markProductReservedSuccess(event.subTotalAmount());
                sagaRepository.save(saga);
                log.info("[SAGA] order={} product reserved success, subtotal={}", event.orderId(), event.subTotalAmount());
                tryTriggerOrderCalculated(saga);
            });
        }
    
        // 쿠폰 사용 상태 업데이트
        @Async
        @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
        public void on(CouponUsedEvent event) {
            sagaRepository.findById(event.orderId()).ifPresent(saga -> {
                saga.markCouponAppliedSuccess(event.discountAmount());
                sagaRepository.save(saga);
                log.info("[SAGA] order={} coupon used success, discount={}", event.orderId(), event.discountAmount());
                tryTriggerOrderCalculated(saga);
            });
        }
    
        // 잔액 차감 완료 상태 업데이트
        @Async
        @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
        public void on(BalanceDeductedEvent event) {
            sagaRepository.findById(event.orderId()).ifPresent(saga -> {
                saga.markBalanceChargedSuccess(event.totalAmount());
                sagaRepository.save(saga);
                log.info("[SAGA] order={} balance deducted success, total={}", event.orderId(), event.totalAmount());
            });
        }
    
        // 상품 재고 차감 실패 상태 업데이트
        @Async
        @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
        public void on(StockReserveFailedEvent event) {
            sagaRepository.findById(event.orderId()).ifPresent(saga -> {
                saga.markFailedDomain("PRODUCT");
                sagaRepository.save(saga);
                log.warn("[SAGA] order={} product reserve failed, reason={}", event.orderId(), event.reason());
            });
        }
    
        // 쿠폰 사용 실패 상태 업데이트
        @Async
        @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
        public void on(CouponUseFailedEvent event) {
            sagaRepository.findById(event.orderId()).ifPresent(saga -> {
                saga.markFailedDomain("COUPON");
                sagaRepository.save(saga);
                log.warn("[SAGA] order={} coupon use failed, reason={}", event.orderId(), event.reason());
            });
        }
    
        // 잔액 차감 실패 상태 업데이트
        @Async
        @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
        public void on(BalanceDeductionFailedEvent event) {
            sagaRepository.findById(event.orderId()).ifPresent(saga -> {
                saga.markFailedDomain("BALANCE");
                sagaRepository.save(saga);
                log.warn("[SAGA] order={} balance deduction failed, reason={}", event.orderId(), event.reason());
            });
        }
    }

기능 구현

flowchart TD
    A[OrderCommandService] -->|OrderDraftedEvent| B[OrderSagaHandler]
    B -->|OrderRequestedEvent| C[ProductCommandService]
    B -->|OrderRequestedEvent| D[CouponCommandService]
    C -->|StockReservedEvent| B
    D -->|CouponUsedEvent| B
    B -->|OrderCalculatedEvent| E[BalanceCommandService]
    E -->|BalanceDeductedEvent| B
    B -->|OrderCompletedEvent| F[외부 핸들러]

피드백

  • Saga 핸들러의 역할이 오케스트레이터의 역할로 보이며, 다른 도메인과 같은 레이어를 갖기보단 saga 디렉토리를 root로 하는 flow 전체를 관리하기 용이한 아키텍쳐로 이동해도 좋을 것 같다.

마치며

  • 이번주차 과제는 사실 설계까지만 이었으나, 한번 도전해보고 싶어서 일부 기능을 구현했습니다. 완성은 하지 못했지만, 나쁘지 않은 방향으로 설계를 한것 같아서 차주까지 한번 완성해보는 것을 목표로 하고 있습니다.

0개의 댓글