일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 알고리즘
- 자료구조
- 기술면접
- 카카오코테
- 코틀린
- groupby
- 자바
- 스터디
- java
- 혼공단
- Kotlin
- 티스토리챌린지
- 정보처리기사
- 안드로이드스튜디오
- MySQL
- 혼공챌린지
- CS
- Android
- SQL
- 인프런
- 코테
- 프로그래머스
- 오블완
- 혼공파
- Til
- join
- 정처기
- 안드로이드
- select
- doitandroid
- Today
- Total
Welcome! Everything is fine.
[Spring] enum abstract method로 중복을 해결해보자 본문
이번 심화 프로젝트를 진행하다가 튜터님께 피드백 받은 내용이 있다.
사실 이 부분은 정신 놓고 코드 짠거 같아서 부끄럽지만..같은 실수를 하지 않기 위해 정리해본다.
🛠️ 변경 전 코드
우선 OwnerOrderController는 가게 사장님이 들어온 주문을 수락 및 거절하는 API를 가지고 있다. 수락이 되었다면 음식조리중, 배달중, 배달완료 순으로 흐를 수 있도록 해야한다. 다음 코드를 보면, 각각의 상태에 따라 API가 다 따로 만들어져있다. 처음에는 상태가 되돌아 갈 수 없도록 하는 것에만 집중해서 예외 처리만 하고 끝냈다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class OwnerOrderController {
private final OwnerOrderService ownerOrderService;
@Order
@PatchMapping("/v1/orders/{orderId}/accept")
public ResponseEntity<OrderStatusResponseDto> acceptOrder(
@Auth AuthUser owner,
@PathVariable Long orderId
) {
return ResponseEntity.ok(ownerOrderService.acceptOrder(owner.getId(), orderId));
}
@Order
@PatchMapping("/v1/orders/{orderId}/reject")
public ResponseEntity<OrderStatusResponseDto> rejectOrder(
@Auth AuthUser owner,
@PathVariable Long orderId
) {
return ResponseEntity.ok(ownerOrderService.rejectOrder(owner.getId(), orderId));
}
@Order
@PatchMapping("/v1/orders/{orderId}/cook")
public ResponseEntity<OrderStatusResponseDto> startCooking(
@Auth AuthUser owner,
@PathVariable Long orderId
) {
return ResponseEntity.ok(ownerOrderService.startCooking(owner.getId(), orderId));
}
@Order
@PatchMapping("/v1/orders/{orderId}/deliver")
public ResponseEntity<OrderStatusResponseDto> startDelivery(
@Auth AuthUser owner,
@PathVariable Long orderId
) {
return ResponseEntity.ok(ownerOrderService.startDelivery(owner.getId(), orderId));
}
@Order
@PatchMapping("/v1/orders/{orderId}/complete")
public ResponseEntity<OrderStatusResponseDto> completeDelivery(
@Auth AuthUser owner,
@PathVariable Long orderId
) {
return ResponseEntity.ok(ownerOrderService.completeDelivery(owner.getId(), orderId));
}
}
이런 경우는 문제점이 있다.
만약, API가 점점 늘어나서 몇 백개, 몇 만개가 된다면?
물론 주문하고 배달하기까지의 과정이 그렇게까지 많은 상태로 쪼개지긴 힘들겠지만, 어떤 상태가 추가된다면 또 새로운 API를 추가해야한다. 그리고 다음과 같이 상태 하나하나 따로 호출하게 된다.
PATCH api/v1/orders/1/reject
PATCH api/v1/orders/1/cook
PATCH api/v1/orders/1/deliver
...이렇게 API URL만 적다가 끝나버릴수도...
그리고 OwnerOrderService를 보면 한 눈에 봐도 중복되는 코드가 아주 많다는 것을 볼 수 있다. 중복되는 코드가 눈에 보이면 빠르게 하나로 합칠 수 있다.
@Service
@RequiredArgsConstructor
public class OwnerOrderService {
private final UserOrderService userOrderService;
@Transactional
public OrderStatusResponseDto acceptOrder(Long ownerId, Long orderId) {
UserOrderEntity order = userOrderService.getOrderById(orderId);
if (!Objects.equals(ownerId, order.getStore().getUser().getId())) {
throw new InvalidRequestException(ErrorOrderMessage.NOT_OWNER_ORDER.getMessage());
}
if (!OrderStatus.PENDING.equals(order.getOrderStatus())) {
throw new InvalidRequestException(ErrorOrderMessage.CANNOT_CANCEL_ORDER.getMessage());
}
order.updateOrderStatus(OrderStatus.ACCEPTED);
return OrderStatusResponseDto.from(order);
}
@Transactional
public OrderStatusResponseDto rejectOrder(Long ownerId, Long orderId) {
UserOrderEntity order = userOrderService.getOrderById(orderId);
if (!Objects.equals(ownerId, order.getStore().getUser().getId())) {
throw new InvalidRequestException(ErrorOrderMessage.NOT_OWNER_ORDER.getMessage());
}
if (!OrderStatus.PENDING.equals(order.getOrderStatus())) {
throw new InvalidRequestException(ErrorOrderMessage.CANNOT_CANCEL_ORDER.getMessage());
}
order.updateOrderStatus(OrderStatus.REJECTED);
return OrderStatusResponseDto.from(order);
}
@Transactional
public OrderStatusResponseDto startCooking(Long ownerId, Long orderId) {
UserOrderEntity order = userOrderService.getOrderById(orderId);
if (!Objects.equals(ownerId, order.getStore().getUser().getId())) {
throw new InvalidRequestException(ErrorOrderMessage.NOT_OWNER_ORDER.getMessage());
}
if (OrderStatus.DELIVERING.equals(order.getOrderStatus())
|| OrderStatus.COMPLETED.equals(order.getOrderStatus())) {
throw new InvalidRequestException(ErrorOrderMessage.ALREADY_DELIVERING_OR_COMPLETED.getMessage());
}
if (!OrderStatus.ACCEPTED.equals(order.getOrderStatus())) {
throw new InvalidRequestException(ErrorOrderMessage.MUST_BE_ACCEPTED_BEFORE_COOKING.getMessage());
}
order.updateOrderStatus(OrderStatus.COOKING);
return OrderStatusResponseDto.from(order);
}
@Transactional
public OrderStatusResponseDto startDelivery(Long ownerId, Long orderId) {
UserOrderEntity order = userOrderService.getOrderById(orderId);
if (!ObjectUtils.nullSafeEquals(ownerId, order.getStore().getUser().getId())) {
throw new InvalidRequestException(ErrorOrderMessage.NOT_OWNER_ORDER.getMessage());
}
if (OrderStatus.DELIVERING.equals(order.getOrderStatus())
|| OrderStatus.COMPLETED.equals(order.getOrderStatus())) {
throw new InvalidRequestException(ErrorOrderMessage.ALREADY_DELIVERING_OR_COMPLETED.getMessage());
}
if (!OrderStatus.COOKING.equals(order.getOrderStatus())) {
throw new InvalidRequestException(ErrorOrderMessage.MUST_BE_COOKING_BEFORE_DELIVERY.getMessage());
}
order.updateOrderStatus(OrderStatus.DELIVERING);
return OrderStatusResponseDto.from(order);
}
@Transactional
public OrderStatusResponseDto completeDelivery(Long ownerId, Long orderId) {
UserOrderEntity order = userOrderService.getOrderById(orderId);
if (!ObjectUtils.nullSafeEquals(ownerId, order.getStore().getUser().getId())) {
throw new InvalidRequestException(ErrorOrderMessage.NOT_OWNER_ORDER.getMessage());
}
if (OrderStatus.COMPLETED.equals(order.getOrderStatus())) {
throw new InvalidRequestException(ErrorOrderMessage.ALREADY_COMPLETED.getMessage());
}
if (!OrderStatus.DELIVERING.equals(order.getOrderStatus())) {
throw new InvalidRequestException(ErrorOrderMessage.MUST_BE_DELIVERING_BEFORE_COMPLETE.getMessage());
}
order.updateOrderStatus(OrderStatus.COMPLETED);
return OrderStatusResponseDto.from(order);
}
}
다시 봐도 생각없이 짠 코드같아서 스스로도 어이가 없다. 대략 요약하자면 다음과 같은 부분들이 반복된다.
▪️유저 아이디를 검증하는 코드
▪️ 현재 들어온 상태 변경을 하기에 적절한 상태인지 확인하는 코드
▪️order.updateOrderStatus()로 주문 상태를 변경하는 코드
▪️ OrderStatusResponseDto을 반환하는 코드
변경 전에는 다음과 같이 enum 클래스에서 상태명만 가져와 사용하고 있다.
@Getter
@RequiredArgsConstructor
public enum OrderStatus {
PENDING("주문 확인전"),
CANCELED("주문 취소"),
ACCEPTED("주문 접수"),
REJECTED("주문 거절"),
COOKING("조리중"),
DELIVERING("배달중"),
COMPLETED("배달 완료");
private final String description;
}
이제 리팩토링을 해보자!
✨ 리팩토링한 코드
튜터님의 피드백을 정리해보면 다음과 같다.
✔️ 중복되는 코드가 너무 많다.
✔️ 상태가 추가되면 API를 계속 추가해야하고, 관리가 어려워진다는 문제가 생긴다. 주문 상태를 파라미터마다 받는 것은 어떨까?
✔️ 각 상태마다 switch문으로 분기할 수 있을 것이다.
✔️ 각 상태별 예외처리를 메서드로 분리해 주문 상태에 따른 처리 로직만 남기면 코드를 간소화할 수 있다.
✔️ 현재 enum 클래스로 상태를 관리하고 있으므로, enum 내에 추상 메서드를 이용해 그 안에서 예외처리를 해볼 수도 있다.
먼저, 주문상태를 @RequestParam으로 받도록 변경했다. 다음과 같이 단 하나의 API만 남기고 모두 지웠다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class OwnerOrderController {
private final OwnerOrderService ownerOrderService;
@Order
@PatchMapping("/v1/orders/{orderId}")
public ResponseEntity<OrderStatusResponseDto> order(
@Auth AuthUser owner,
@PathVariable Long orderId,
@RequestParam String status
) {
return ResponseEntity.ok(ownerOrderService.order(owner.getId(), orderId, status));
}
}
그러면 다음과 같이 호출할 수 있다. 주문 상태에 따라 status 값만 바꿔주면 된다.
PATCH /api/v1/orders/1?status=ACCEPTED
그 다음 OwnerOrderService로 가보면 이전과 달리 아주 심플해진 코드를 볼 수 있다.
✔️ 사용자를 검증하는 코드를 private 메서드로 분리해 order() 메서드 내에서는 validateOwner() 한 줄로 적었다.
✔️ OrderStatus enum 클래스 내부에 추상 메서드 update()를 선언해 역시 OrderStatus.valueOf(status).update(order); 한 줄로 적었다.
@Service
@RequiredArgsConstructor
public class OwnerOrderService {
private final UserOrderService userOrderService;
@Transactional
public OrderStatusResponseDto order(Long ownerId, Long orderId, String status) {
UserOrderEntity order = userOrderService.getOrderById(orderId);
validateOwner(ownerId, order);
OrderStatus.valueOf(status).update(order);
return OrderStatusResponseDto.from(order);
}
private void validateOwner(Long ownerId, UserOrderEntity order) {
if (!Objects.equals(ownerId, order.getStore().getUser().getId())) {
throw new InvalidRequestException(ErrorOrderMessage.NOT_OWNER_ORDER.getMessage());
}
}
}
그러면 이제 변경사항이 생겨도 사실상 Controller와 Service 쪽은 건드릴 필요가 없다. OrderStatus 클래스에 각 상태에 따른 예외 처리를 넣어놨기 때문이다. 중복이 되는 부분은 따로 빼서 메서드로 만들고, order를 받아 조건이 맞으면 해당 order의 상태를 수정하는 방식이다.
@Getter
@RequiredArgsConstructor
public enum OrderStatus {
PENDING("주문 확인전") {
@Override
public void update(UserOrderEntity order) {
}
},
CANCELED("주문 취소") {
@Override
public void update(UserOrderEntity order) {
validateOrderCancelable(order);
}
},
ACCEPTED("주문 접수") {
@Override
public void update(UserOrderEntity order) {
validateOrderCancelable(order);
order.updateOrderStatus(OrderStatus.ACCEPTED);
}
},
REJECTED("주문 거절") {
@Override
public void update(UserOrderEntity order) {
validateOrderCancelable(order);
order.updateOrderStatus(OrderStatus.REJECTED);
}
},
COOKING("조리중") {
@Override
public void update(UserOrderEntity order) {
validateNotDeliveringOrCompleted(order);
if (!OrderStatus.ACCEPTED.equals(order.getOrderStatus())) {
throw new InvalidRequestException(ErrorOrderMessage.MUST_BE_ACCEPTED_BEFORE_COOKING.getMessage());
}
order.updateOrderStatus(OrderStatus.COOKING);
}
},
DELIVERING("배달중") {
@Override
public void update(UserOrderEntity order) {
validateNotDeliveringOrCompleted(order);
if (!OrderStatus.COOKING.equals(order.getOrderStatus())) {
throw new InvalidRequestException(ErrorOrderMessage.MUST_BE_COOKING_BEFORE_DELIVERY.getMessage());
}
order.updateOrderStatus(OrderStatus.DELIVERING);
}
},
COMPLETED("배달 완료") {
@Override
public void update(UserOrderEntity order) {
if (!OrderStatus.DELIVERING.equals(order.getOrderStatus())) {
throw new InvalidRequestException(ErrorOrderMessage.MUST_BE_DELIVERING_BEFORE_COMPLETE.getMessage());
}
order.updateOrderStatus(OrderStatus.COMPLETED);
}
};
private final String description;
abstract public void update(UserOrderEntity order);
private static void validateOrderCancelable(UserOrderEntity order) {
if (!OrderStatus.PENDING.equals(order.getOrderStatus())) {
throw new InvalidRequestException(ErrorOrderMessage.CANNOT_CANCEL_ORDER.getMessage());
}
}
private static void validateNotDeliveringOrCompleted(UserOrderEntity order) {
if (OrderStatus.DELIVERING.equals(order.getOrderStatus())
|| OrderStatus.COMPLETED.equals(order.getOrderStatus())) {
throw new InvalidRequestException(ErrorOrderMessage.ALREADY_DELIVERING_OR_COMPLETED.getMessage());
}
}
}
이렇게 리팩토링을 진행하고, 튜터님께 다시 피드백을 받았는데 잘 수정했다고 하셨다. 이 과정을 통해 중복 코드에 대한 경각심을 다시 한 번 일깨웠고, enum 클래스에서 abstract 메서드를 사용하는 방법에 대해 학습해봤다.
'TIL' 카테고리의 다른 글
[Spring] application.yml에 시크릿 키 넣지 말아주세요...(.env 파일 만들고 설정하기) (0) | 2025.03.10 |
---|---|
프로젝트를 시작할 때 생각해볼 것들 💭 (0) | 2025.03.08 |
[Spring] JWT 시크릿 키 설정 및 문제 해결 / -hex 64 vs -base64 32 차이 (0) | 2025.02.27 |
[Spring] @EntityGraph로 N+1문제 해결하기 (0) | 2025.02.26 |
[Spring] Spring에서 세션을 다루는 3가지 방법, 뭐가 제일 좋을까? (0) | 2025.02.22 |