본문 바로가기

ToyProject

[ToyProject] 상품 구매 서비스 리팩토링

 

@Service
@RequiredArgsConstructor
public class PaymentAppService {

    private final IamportClient iamportClient;

    private final ItemRepository itemRepository;

    private final OrderService orderService;

    @Transactional
    public OrderResponse paymentValidate(PaymentRequest request) {
        Payment payment = null;
        
        try {
            payment = iamportClient.paymentByImpUid(request.getImpUid()).getResponse();
        } catch (IamportResponseException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        Optional<Item> item = itemRepository.findByName(request.getItemName());

        validation(payment, item);

        return orderService.makeOrder(item.get(), request.getMemberId());
    }

    private static void validation(Payment payment, Optional<Item> item) {
        itemExistValidate(item.isPresent());
        itemNameValidate(item.get().getName().equals(payment.getName()));
        itemPriceValidate(item.get().getPrice() == payment.getAmount().intValue());
    }
}

기존 상품 구매 관련 비즈니스 로직이다.

 

언뜻 보기에는 문제가 보이지 않을 수 있지만 곰곰히 생각해본 끝에 해당 클래스에서 외부 서비스를 의존하면서 동시에 비즈니스 로직을 처리하는건 문제가 생길 수 있다고 판단했다.

 

좋은 코드는 한 클래스에서 하나의 역할만 가져야한다고 한다. 

 

현재 코드에서는 IamPortClient에 의존하면서 결제에 대한 검증을 해주고, 같은 클래스 내에서 주문을 생성한다.

 

이 상황에서 만약 결제모듈이 변경된다면 어떻게될까? IamPort라는 결제모듈을 의존하고 있는 클래스들에서 해당 로직들을 전부 변경해줘야한다.

 

유지보수적인 면에서도 많은 문제가 생기고, 강한 결합 등의 부가적인 문제점들이 있다.

 

이런 문제점들 때문에 코드를 분리하기로 결정했고, 여러가지 방법을 생각해봤는데,

 

결제모듈 관련 클래스를 인터페이스화 해서 추상화에 의존하고 탬플릿 콜백 패턴을 활용해 비즈니스 로직의 변경을 최소화시키기로 했다.

 

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final ItemRepository itemRepository;

    private final OrderService orderService;

    private final PaymentTemplate paymentTemplate;

    @Transactional
    public GetOrderDto purchase(PaymentRequest request) {
        Item item = itemRepository.findByName(request.getItemName()).orElseThrow(PaymentService::getExceptionMessage);

        itemPriceValidate(item.getQuantity() > request.getQuantity());

        return paymentTemplate.execute(
                new ValidatePayment(item, request),
                () -> orderService.makeOrder(item, request.getMemberId(), request.getQuantity()));
    }

    private static ItemStatusException getExceptionMessage() {
        return new ItemStatusException("존재하지 않는 상품입니다.");
    }
}

변경된 코드이다.

 

PaymentService로 클래스명을 변경했고, 이제 이 클래스는 더이상 외부 서비스에 의존하지 않는다.

 

다만 PaymentTemplate 이라는 인터페이스를 의존하게 해서, 만약 결제모듈이 변경되더라도 해당 서비스의 코드는 변경될 일이 없을것이다!

 

왜냐하면 PaymentTemplate을 구현하는 클래스에서 결제모듈 로직이 수행되기 때문이다.

 

public interface PaymentTemplate {
    <T> T execute(ValidatePayment validatePayment, IamPortCallBack<T> T);
}
@Component
@RequiredArgsConstructor
@Transactional
public class IamPortTemplate implements PaymentTemplate{

    private final IamportClient iamportClient;

    @Override
    public <T> T execute(ValidatePayment validatePayment, IamPortCallBack<T> T) {
        try {
            Payment payment = iamportClient.paymentByImpUid(validatePayment.request().getImpUid()).getResponse();

            itemNameValidate(validatePayment.getItemName().equals(payment.getName()));
            itemPriceValidate(validatePayment.request().getAmount() == payment.getAmount().intValue());

            return T.call();
        } catch (IamportResponseException e) {
            throw new IamPortRunTimeException(e);
        } catch (IOException e) {
            throw new IamPortRunTimeIoException(e);
        }
    }
}

 

구체 클래스가 아닌 인터페이스에 의존하게 함으로써 DIP를 지켰을 뿐만 아니라,

 

탬플릿 콜백 패턴을 활용함으로써 기존의 비즈니스 로직이 IamPort라는 라이브러리의 예외에 의존하게 되었던 것도 전부다 분리시킬 수 있었다.

 

기존 api의 응답방식도 변경했다!

 

기존에는 이렇게 메세지의 계층 구분없이 응답해줬었는데,

 

status를 번호로 구분, data의 계층을 나눠서 return 해주었다.

 

@Data
public class OrderResponse {

    private Long orderId;

    private GetOrderResponseMemberDto member;

    private GetOrderResponseItemDto item;

    private OrderResponse(GetOrderDto dto) {
        this.orderId = dto.getOrderId();
        this.member = new GetOrderResponseMemberDto(dto.getMemberId(), dto.getMemberName());
        this.item = new GetOrderResponseItemDto(dto.getItemId(), dto.getItemName());
    }

    public static OrderResponse create(GetOrderDto dto) {
        return new OrderResponse(dto);
    }


    @Data
    @AllArgsConstructor
    class GetOrderResponseMemberDto {
        private Long memberId;

        private String memberName;
    }

    @Data
    @AllArgsConstructor
    class GetOrderResponseItemDto {
        private Long itemId;

        private String itemName;
    }
}

내부 클래스를 활용해 응답 메시지를 해당 클래스에서 변경해준 후 응답하게끔 했다.