※ 이 글은 개인의 주관이 많이 섞여 있습니다.

장바구니 웹 어플리케이션을 구현하면서 다음과 같은 요구사항이 존재했다.

  1. 사용자의 장바구니의 모든 상품을 주문한다
  2. 장바구니의 상품을 주문하면 사용자의 장바구니의 상품을 모두 삭제한다

따라서 다음과 같이 주문을 하는 OrderService의 save 메서드를 구현했다.

public Order save(final Long memberId) {  
	final List<CartItem> cartItems = cartItemService.findAllByMemberId(memberId);  
  
	final Order order = Order.createFromCartItems(cartItems, memberId);  
  
	cartItemService.deleteByMemberId(memberId);  
	return orderRepository.save(order);  
}

첫 줄의 사용자에 해당하는 장바구니 아이템을 불러오는 코드는 괜찮지만

cartItemService.deleteByMemberId는 cartItem의 데이터를 직접 변경시키기 때문에 OrderService의 책임을 벗어난다고 볼 수 있다.

OrderService는 주문을 하는 데에만 관심이 있고, 주문을 함으로써 일어나는 side-effect까지 OrderService가 책임지는 것은 당장은 괜찮아 보이지만  side-effect가 많아지면 이를 관리하기 어려울 수 있다.

예를 들어서, 주문을 했을 때 사용자의 장바구니에 담긴 상품도 삭제하고, 점주에게 알림 메시지를 보내고, 가게에 수수료를 부과한다고 가정해보자.

그렇다면 코드가 다음과 같이 변경될 것이다.

public Order save(final Long memberId) {  
	final List<CartItem> cartItems = cartItemService.findAllByMemberId(memberId);  
  
	final Order order = Order.createFromCartItems(cartItems, memberId);  
  
	cartItemService.deleteByMemberId(memberId);  
	shopService.sendOrderMessage(order); // 가게에 주문 메시지를 보냄
	commissionService.charge(order);	// 가게에 수수료를 부과함
	return orderRepository.save(order);  
}

이렇게 주문을 함으로써 일어나는 side-effect들이 절차지향적으로 쌓이고, 주문 로직이 변경되지 않더라도 OrderService가 변경되어야 할 것이다. (서비스간의 결합도가 높다는 문제도 있다.)

이는, OrderService의 변경의 원인이 하나가 아니게 되어 SRP를 위반하고,

주문 외 기능이 확장되었을 때 OrderService에 변경이 생긴다.

이 문제를 해결하기 위해 스프링의 이벤트를 사용할 수 있다.

이벤트란?

스프링에서 디자인 패턴의 옵저버 패턴을 구현한 하나의 방법.

이벤트를 발행하면(notify), 이를 구독하고 있는 Observer들이 이벤트를 전달받고, 각자의 로직을 수행한다.

스프링의 이벤트는 다음과 같이 3가지 요소로 이루어져 있다.

1. 이벤트를 발행하는 Publisher

2. 이벤트 발행시 전달되는 이벤트 객체

3. 이벤트를 전달받는 EventListener

이벤트 객체는 스프링 4.2 버전 이전에는 ApplicationEvent를 상속한 객체만 가능했지만 그 이후에는 모든 객체를 이벤트 객체로 사용할 수 있다.

하지만, 도메인 객체를 직접 이벤트 객체로 사용하기 보다 이벤트가 발생한 시점의 도메인 객체의 스냅샷을 이벤트 객체로 사용하는 것이 좋다. 다음 코드에서는 `OrderedEvent.from()`에서 Order의 스냅샷을 저장하여 이벤트를 발행하도록 했다. 

public Order save(final Long memberId) {  
	final List<CartItem> cartItems = cartItemService.findAllByMemberId(memberId);  
  
	final Order order = Order.createFromCartItems(cartItems, memberId);  
  
	applicationEventPublisher.publishEvent(OrderedEvent.from(order));	// 이벤트 발행
	return orderRepository.save(order);  
}

OrderedEvent는 스냅샷을 저장하고 있는 DTO라고 볼 수 있다.

OrderService는 주문을 생성하여 저장하고, 주문을 완료했다는 이벤트만 발행하고 있다.

이 이벤트를 Listener를 구현하여 이벤트가 발행되면 그 이후의 연관되는 작업을 수행할 수 있다.

@Component
public class OrderedEventHandler {

    private final CartItemService cartItemService;

    public OrderEventHandler(CartItemService cartItemService) {
        this.cartItemService = cartItemService;
    }

    @EventListener
    public void deleteCartItems(OrderedEvent event) {
        cartItemService.deleteByMemberId(event.getMemberId());
    }

    // 가게에 메시지를 보내는 리스너
    // 가게에 수수료를 부과하는 리스너
}

주문이 발생했을 때 수행되는 추가적인 작업들을 이 Listener에 모두 구현하면 된다.

OrderService는 주문과 이벤트를 발행만 함으로써 주문에 따른 side-effect에 대한 책임을 eventListener에게 넘겨 SRP를 만족 하고, 서비스간의 결합도를 줄일 수 있었다.

 

참고 자료

- https://docs.spring.io/spring-framework/reference/core/beans/context-introduction.html#context-functionality-events 

- https://www.baeldung.com/spring-events

-https://ko.wikipedia.org/wiki/%EC%98%B5%EC%84%9C%EB%B2%84_%ED%8C%A8%ED%84%B4

+ Recent posts