본문 바로가기

Spring

[Spring Boot] 로직 흐름 변화에 따른 API 분리 (판매자 주문 승인/취소)

☝️ 사용 버전

Spring Boot 3.2.5
JDK 17

 

💡문제 상황

 

초기에는 판매자가 주문을 승인하고, 취소하는 기능을 하나의 API로 만들었다.

두 기능 모두 단순히 주문 상태를 승인 및 취소로 수정하는 것이었기 때문에, 논리적 흐름이 동일했다. 따라서 하나의 API로 관리해도 문제가 없었다. 

    @Operation(summary = "(판매자) 주문 승인 또는 취소", description = "(판매자) 주문을 승인 또는 취소한다.")
    @PatchMapping("/{orderId}")
    public ResponseEntity<MessageDTO> cancelOrApproveOrder(@PathVariable Long orderId,
                                                           @RequestBody @Validated OrderSellerRequest orderSellerRequest) {
        MessageDTO messageDTO = orderSellerService.cancelOrApproveOrder(orderId, orderSellerRequest);
        return ResponseEntity.ok(messageDTO);
    }

 

 

그러나 '주문 취소 시 상품 재고 롤백' 등의 기능들이 추가되면서, 주문 승인과 취소의 전체적인 논리 흐름이 달라지게 되었다(아래 코드 참고).

    @Transactional
    public MessageDTO cancelOrApproveOrder(Long orderId, OrderSellerRequest orderSellerRequest) {

        // 주문 유효성 검사
        upperOrderRepository.getOrder(orderId)
                .filter(Order -> Order.getStatus() == Status.WAITING)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "주문이 존재하지 않거나 상태가 'WAITING'이 아니어서 처리할 수 없습니다."));

        return processOrder(orderId, orderSellerRequest);
    }

    // 주문 처리
    @Transactional
    public MessageDTO processOrder(Long orderId, OrderSellerRequest orderSellerRequest) {

        MessageDTO messageDTO = null;

        if (orderSellerRequest.getApprovalStatus() == ApprovalStatus.APPROVE) {
            upperOrderSellerRepository.approveOrder(orderId);
            messageDTO = new MessageDTO(HttpStatus.OK.value(), "주문이 정상적으로 승인 되었습니다.");
        } else if (orderSellerRequest.getApprovalStatus() == ApprovalStatus.CANCEL) {
            upperOrderSellerRepository.cancelOrder(orderId);
            orderDetailService.restoreStockByOrder(orderId);
            messageDTO = new MessageDTO(HttpStatus.OK.value(), "주문이 정상적으로 취소 되었습니다.");
        }

        upperOrderRepository.getOrder(orderId)
                .map(OrderResponse::fromOrder)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "처리 후 주문 정보를 찾을 수 없습니다."));

        return messageDTO;
    }

 

이렇게 논리적 흐름이 다른 두 기능을 하나의 API로 관리하게 되면 생기는 문제점은 다음과 같다.

 

 

1. 두 기능의 논리가 맞지 않고, API를 각 기능에 따라 독립적으로 관리하기가 힘들어 유지보수가 어렵다.

예를 들어 API를 사용하다보면 API 엔드포인트를 기준으로 에러를 모니터링해야하는 상황이 많은데, 승인과 취소가 같은 엔드포인트일 경우, 오류가 났을 때 승인 문제인지 취소 문제인지 명확하지 않아 디버깅부터 모니터링이 힘들 수 있다.

 

2. SRP(단일 책임 원칙) 위반: 주문 승인과 취소 기능은 논리적으로 다른 책임을 가지기 때문에, 메서드가 여러 개의 변경 이유를 가지게 된다.

 

3. OCP 위반: '주문 보류'와 같은 새로운 상태를 추가하려면 기존의 코드를 수정해야 한다. 

 

SRP와 OCP 위반에 대해서는 아래 포스팅에서 자세하게 다룬다.

https://suoop.tistory.com/92

 

[SOLID] 단일 API로 두 기능을 관리했을 때: SRP, OCP 위반 예시

☝️ 사용 버전Spring Boot3.2.5JDK17 💡문제 상황 아래 코드는 주문 승인과 취소, 두 가지 기능을 단일 API로 관리하는 상황이다.   @Operation(summary = "(판매자) 주문 승인 또는 취소", description = "(판매

suoop.tistory.com

 

 

 

🛠️ 문제 해결

주문 승인과 취소에 대한 API를 각각 따로 만들어, 각 기능에 대해 명확하게 관리할 수 있게 하였다.

 

<컨트롤러>

@Tag(name = "OrderSeller", description = "판매자(OrderSeller)의 주문 정보 관리에 대한 API 입니다.")
@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/seller/orders")
public class OrderSellerController {

    private final OrderSellerService orderSellerService;

    @Operation(summary = "(판매자) 주문 승인", description = "(판매자) 주문을 승인한다.")
    @PatchMapping("/{orderId}-approve")
    public ResponseEntity<MessageDTO> approveOrder(@PathVariable Long orderId) {
        MessageDTO messageDTO = orderSellerService.approveOrder(orderId);
        return ResponseEntity.ok(messageDTO);
    }

    @Operation(summary = "(판매자) 주문 취소", description = "(판매자) 주문을 취소한다.")
    @PatchMapping("/{orderId}-cancel")
    public ResponseEntity<MessageDTO> cancelOrder(@PathVariable Long orderId) {
        MessageDTO messageDTO = orderSellerService.cancelOrder(orderId);
        return ResponseEntity.ok(messageDTO);
    }
}

 

<서비스>

@Transactional
    public MessageDTO approveOrder(Long orderId) {
        MessageDTO messageDTO;

        // 주문 유효성 검사
        upperOrderRepository.getOrder(orderId)
                .filter(Order -> Order.getStatus() == Status.WAITING)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "주문이 존재하지 않거나 상태가 'WAITING'이 아니어서 처리할 수 없습니다."));

        upperOrderSellerRepository.approveOrder(orderId);
        messageDTO = new MessageDTO(HttpStatus.OK.value(), "주문이 정상적으로 승인 되었습니다.");

        upperOrderRepository.getOrder(orderId)
                .map(OrderResponse::fromOrder)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "처리 후 주문 정보를 찾을 수 없습니다."));

        return messageDTO;
    }

    @Transactional
    public MessageDTO cancelOrder(Long orderId) {
        MessageDTO messageDTO;

        // 주문 유효성 검사
        upperOrderRepository.getOrder(orderId)
                .filter(Order -> Order.getStatus() == Status.WAITING)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "주문이 존재하지 않거나 상태가 'WAITING'이 아니어서 처리할 수 없습니다."));

        upperOrderSellerRepository.cancelOrder(orderId);
        orderDetailService.restoreStockByOrder(orderId);
        messageDTO = new MessageDTO(HttpStatus.OK.value(), "주문이 정상적으로 취소 되었습니다.");

        upperOrderRepository.getOrder(orderId)
                .map(OrderResponse::fromOrder)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "처리 후 주문 정보를 찾을 수 없습니다."));

        return messageDTO;
    }
}

 

 

📝 결론

논리적 흐름이 달라진 두 기능을 하나의 API로 관리하게 되면,

API의 목적과 의도가 불분명해지고, SRP, OCP 위반 등의 문제가 발생한다.

따라서 API를 따로 분리하여 각 API가 하나의 명확한 작업을 수행할 수 있도록 해야 한다.