반응형

spring에서 제공하는 application publisher, listener를 쓰기 좋을때가 있을것 같아서 기능에 대해 소개해보려고 한다.

 

요구사항

먼저 임의의 요구사항을 만들어보자.

우리 서비스에 메일 전송 기능을 만들고자 합니다.
메일 전송과 관련해서 필요한 기능은 다음과 같습니다.

1. 유저가 메일을 전송합니다. 
2. 메일 전송시 유저에게 1달러가 과금돼야 합니다. (말이 안되지만 생각나는 기능이 없어서 넣음)
3. 메일 전송 기능 사용 횟수가 집계돼야 합니다.

 

 

위 요구사항에 대해 다음과 같이 코드를 구현해보았다.

@Service
public class MailService {

    @Autowired
    private UserActionService userActionService;

    @Autowired
    private PaymentService paymentService;

    public void sendMail(String message) {
        String userId = "1234";
        System.out.println(userId + "유저가 메일을 전송했습니다.");
        System.out.println(String.format("메세지 : %s", message));

        userActionService.countingSendMail(userId);
        paymentService.payingSendMail(userId);
    }
}

@Service
public class UserActionService {

    private Map<String, Integer> userMailCount = new ConcurrentHashMap<>();

    public void countingSendMail(String userId) {
        userMailCount.put(userId, userMailCount.getOrDefault(userId, 0) + 1);
        System.out.println(String.format("%s 유저가 메일을 보낸 횟수 : %d", userId, userMailCount.get(userId)));
    }
}

@Service
public class PaymentService {
    private final static int SEND_MAIL_PRICE = 1;

    public void payingSendMail(String userId) {
        System.out.println(userId + String.format("에게 메일 전송 비용 %d$가 지불되었습니다.", SEND_MAIL_PRICE));
    }
}

api 호출시 로그

 

서비스의 의존 관계는 다음과 같이 생성된다.

 

코드에서 이렇게 직접적인 의존 관계가 생겨버리면 메일을 보내는 코드 내에서 메일을 전송하는 순수한 기능만 있는게 아니라서 추가적인 기능이 붙다보면 의존 관계가 되는 서비스가 많아지고 그러다보면 메일 서비스와 다른 서비스간에 결합도가 너무 높아져 버릴 수 있다.

 

Application Event, Publisher, listener

이제 로직을 수정해보자면 메일 전송 이벤트를 생성하고 이벤트를 기반으로 과금, 유저 행동에 대한 기능을 분리해보려고 한다.

이를 스프링에서 제공하는 이벤트 기능들을 이용해서 구현해보자.

 

먼저 MailService를 수정해보자.

@Service
public class MailService {

    @Autowired
    private ApplicationEventPublisher publisher;

    public void sendMail(String message) {
        String userId = "1234";
        System.out.println(userId + "유저가 메일을 전송했습니다.");
        System.out.println(String.format("메세지 : %s", message));

        publisher.publishEvent(new SendMailEvent(userId, message));
    }
}

@Getter
@Setter
public class SendMailEvent {
    public SendMailEvent(String userId, String message) {
        this.userId = userId;
        this.message = message;
    }

    private String message;
    private String userId;
}

이전 코드와 다르게 event publisher에게 메일 전송 이벤트를 생성해 넘겨주고 다른 서비스의 의존성은 끊겼다.

생성할 이벤트는 pojo 형식으로 클래스를 생성하면 된다. 참고로 spring 4.2 이전 버전에서는 생성할 이벤트는 ApplicationEvent 클래스를 상속받았어야 했었다.

 

다음으로 이벤트 리스너를 생성해보자.

@Component
public class MailEventListener {

    @Autowired
    private UserActionService userActionService;

    @Autowired
    private PaymentService paymentService;

    @EventListener
    public void invokeUserActionLogging(SendMailEvent sendMailEvent) {
        userActionService.countingSendMail(sendMailEvent.getUserId());
    }

    @EventListener
    public void invokePayment(SendMailEvent sendMailEvent) {
        paymentService.payingSendMail(sendMailEvent.getUserId());
    }

}

 

메일 전송 이벤트를 받는 리스너 Bean을 생성하고 @EventListener 어노테이션을 지정하고 생성한 이벤트를 파라미터로 받는 메소드를 만들면 간단하게 이벤트를 받아와 로직을 수행할 수 있다. 여기서는 편의상 이벤트 리스너 빈을 하나의 클래스로 생성했지만, 여러 개의 리스너를 만들어 같은 이벤트를 받아들이도록 분리할 수 있다.

 

이제 다시 로직을 실행해 보았다.

 

Order

여기서 별다른 처리를 하지 않았다면 이벤트 발행 후 listener들은 이벤트를 발행한 스레드에서 동기적으로 순서대로 호출된다.

이벤트 처리 순서는 userAction -> payment 순서로 실행되었는데 실행 순서가 중요한 경우 @Order 어노테이션으로 이벤트 리스너 처리 순서를 정할수도 있다.

    @Order(2)
    @EventListener
    public void invokeUserActionLogging(SendMailEvent sendMailEvent) {
        userActionService.countingSendMail(sendMailEvent.getUserId());
    }

    @Order(1)
    @EventListener
    public void invokePayment(SendMailEvent sendMailEvent) {
        paymentService.payingSendMail(sendMailEvent.getUserId());
    }

코드를 수정하고 이렇게 결제를 먼저 실행하고 유저 행동 카운팅을 수행하게 바뀌었다.

 

코드 구조는 MailService에 여러 개의 의존 서비스들이 붙는 형태에서 다음과 같이 변경되었다.

이렇게 구조가 변경되면 메일 전송 기능에 각종 부가 기능들이 추가된다 하더라도 메일 전송 이벤트를 구독하는 이벤트 리스너와 서비스들을 붙여나가면 되고 메일 전송 서비스에서는 메일 전송 기능에만 집중하면 돼서 서비스간 결합도를 낮추고 응집도가 높힐 수 있다.

 

여기서 조금 더 상세한 구현을 해보자.

메일 전송 비용이 지불된 다음에 유저에게 지불 알람이 가야 한다면 어떻게 해야 할까?

기존 코드에서는 메일 전송 비용 결제가 메일 전송 이벤트를 받아 처리되었는데 동기적으로 수행하게 한다면 다음과 같이 붙여볼 수 있다.

    @Autowired
    private UserNotificationService userNotificationService;

    @Order(1)
    @EventListener
    public PaymentEvent invokePayment(SendMailEvent sendMailEvent) {
        String userId = sendMailEvent.getUserId();
        return new PaymentEvent(userId, paymentService.payingSendMail(userId));
    }

    @EventListener
    public void invokePushAlert(PaymentEvent paymentEvent) {
        userNotificationService.pushAlert(paymentEvent.getUserId(), paymentEvent.getPrice());
    }

결제를 처리하고 결제 이벤트를 리턴하게 하고 해당 이벤트를 받는 리스너를 생성해 알람 서비스 로직을 수행하게 하면 된다.

 

비동기 실행 @Async

마지막으로 이벤트를 발행하고 각 이벤트 처리를 별도의 스레드에서 실행하게 하려면?

@Async 어노테이션을 쓰면 된다.

    @Async
    @EventListener
    public void invokePayment(SendMailEvent sendMailEvent) {
        String userId = sendMailEvent.getUserId();
        paymentService.payingSendMail(userId);
    }

@Async 어노테이션을 사용해 성능을 높일 수 있지만 리턴값을 사용한 이벤트 추가 발행이 불가능하기 때문에 application publisher를 가지고 직접 이벤트를 발행해야 하는 단점이 있다. 또한 이런식으로 비동기 처리시 기존 스레드의 context가 새로운 스레드에 넘어가 유지되지 않기 때문에 별도의 처리 과정이 필요한 점도 고려해야 한다.

 

이외에도 이벤트 발행도 메일 서비스에 포함시키지 않고 AOP로 분리한다던가, 트랜잭션으로 전체 로직 처리의 일관성을 유지하는 부분들은 요구사항에 따라 적절하게 구현하면 될 것 같다.

 

참고 자료

반응형