[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 위반에 대해서는 아래 포스팅에서 자세하게 다룬다.
🛠️ 문제 해결
주문 승인과 취소에 대한 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가 하나의 명확한 작업을 수행할 수 있도록 해야 한다.