본문 바로가기

Backend/SpringBoot

Spring Boot에서 이벤트 사용하기

안녕하세요! 이번 포스팅에서는 Spring Boot에서 이벤트를 적용하는 방법에 대해 알아보겠습니다.

전체 코드는 Github에서 확인이 가능합니다. ✍️


📚 개념 정리 & 사전준비

1. 이벤트

Spring Boot에서 이벤트를 적용하는 방법에 대해 들어가기 전에, 이벤트를 왜 써야하는지, 사용하면 좋은 상황에 대해 먼저 소개하겠습니다. 회원가입을 하고 나면 가입 축하 메세지를 전송하는 동시에 쿠폰을 전송하는 서비스가 있다고 가정해보겠습니다.

@Service
public class RegisterService {

  @Autowired
  ApplicationEventPublisher publisher;

  public void register(String name) {
    // 회원가입 처리 로직
    System.out.println("회원 추가 완료");
    
    // 가입 축하 메세지 전송
    System.out.println(name + "님에게 가입 축하 메세지를 전송했습니다.");
    
    // 가입 축하 쿠폰 발급
    System.out.println(name + "님에게 쿠폰을 전송했습니다.");
  }
}

위와 같이 회원 가입 - 메세지 전송 - 쿠폰 발급의 로직을 타게 됩니다. 하지만 위 코드에는 문제점이 몇가지 존재합니다.

 

1) 강한 결합

현재 회원가입 서비스에 회원 가입 로직뿐만 아니라 가입 축하 메세지를 전송하는 로직, 가입 축하 쿠폰을 발급하는 로직이 모두 섞여있습니다. 이렇게 서로 강한 결합으로 묶여있으면 후에 유지보수가 어려울뿐만 아니라 코드의 구조가 복잡해지게 됩니다.

 

2) 트랜잭션

만약 가입 축하 메세지를 전송하다가 예외가 발생하면 회원가입을 한 이력까지 모두 롤백을 하는 것은 절대 좋은 방법이 아닙니다. 차라리 회원 가입 처리를 해주고, 축하 메세지와 쿠폰 발급은 따로 관리하는게 옳은 방법입니다.

 

3) 성능

가입 축하 메세지를 전송하는데 3분, 쿠폰을 발급하는데 3분이 걸린다고 하면 총 회원가입은 6분이 걸리게 됩니다. 메인 이벤트인 회원가입 처리 로직을 끝내면 서브 이벤트인 가입 축하 메세지 전송이나 가입 축하 쿠폰은 전송완료까지 기다릴 필요가 없습니다.

즉, 회원가입 처리 → 가입 축하 메세지 전송 → 쿠폰 발급 → 회원가입 처리 완료가 아닌, 회원가입 처리 → 회원가입 처리 완료 (가입축하 메세지, 쿠폰 발급은 가입 처리 되면 따로 실행 시작)의 순서로 실행을 하면 됩니다. 바로 위와 같은 상황을 해결하는 방법 중에 이벤트를 적용해서 해결하는 방법이 있습니다. 이벤트는 생성 주체의 상태가 변경되면 이벤트를 발생시켜 원하는 기능을 실행해서 후처리를 도와줍니다.

 

2. 이벤트의 실행 단계

  1. 생성 주체(주로 도메인 객체)에서 이벤트를 발생하면 이벤트 디스패처에게 전달한다.
  2. 이벤트 디스패처이벤트 핸들러를 연결해준다.
  3. 이벤트 핸들러에서 이벤트에 담긴 데이터를 통해 원하는 기능을 실행한다.

 

3. 사전 준비

  • 의존성 추가
    • spring-boot-starter-web

💻 구현 - 강한결합 해결

1. 이벤트 클래스 만들기

event/RegisteredEvent.java

public class RegisteredEvent {

  private String name;

  public RegisteredEvent(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }
}

이벤트는 상태가 바뀐 후에 발생하기 때문에 이벤트 클래스의 이름은 과거시제로 지어야합니다. 또한, 이벤트 클래스는 이벤트를 처리하는 데이터를 포함합니다.

 

2. 서비스 만들기

service/RegisterService.java

@Service
public class RegisterService {

  @Autowired
  ApplicationEventPublisher publisher; // 1

  public void hello(String name) {
    // 회원가입 처리 로직
    System.out.println("회원 추가 완료");
    publisher.publishEvent(new RegisteredEvent(name)); // 2
  }
}

(1) 이벤트를 보내는 기능을 사용하기 위해 ApplicationEventPublisher를 주입해줍니다.

(2) 회원가입 처리를 완료하고 나면, publishEvent()를 사용해 이벤트를 전달해줍니다.

이제 이벤트가 발생하면 핸들링을 할 이벤트 핸들러를 만들어보겠습니다.

 

3. 이벤트 핸들러 만들기

event/handler/SmsEventHandler.java

@Component
public class SmsEventHandler {

  @EventListener // 1
  public void sendSms(RegisteredEvent event) {
    System.out.println(event.getName() + "님에게 가입 축하 메세지를 전송했습니다.");
  }

  @EventListener
  public void makeCoupon(RegisteredEvent event) {
    System.out.println(event.getName() + "님에게 쿠폰을 전송했습니다.");
  }
}

(1) @EventListener를 사용하면 이벤트 리스너로 등록이 되고, 매개변수에 이벤트 클래스를 정의하면 해당 이벤트가 발생했을 때 수신해서 처리를 할 수 있습니다.

 

4. 컨트롤러 만들기

controller/TestController.java

@RestController
public class TestController {

  @Autowired
  RegisterService service;

  @GetMapping("/register/{name}")
  public void register(@PathVariable String name) {
    service.hello(name);
    System.out.println("회원가입을 완료했어요");
  }

}

테스트용 컨트롤러를 만들어줍니다. 이름을 가져와서 서비스를 호출하는 간단한 코드이기 때문에 부연 설명은 추가하지 않겠습니다.

 

5. 테스트

이제 구현이 끝났으니 테스트를 해보겠습니다.

http://localhost:8080/register/칩앤데일로 접속한 후에 결과를 확인해보면

제대로 나오는 것을 확인할 수 있습니다! 서비스 코드도 로직이 섞이지 않고 결합도도 약해진 것을 확인할 수 있습니다.하지만 만약 축하 메세지와 쿠폰을 전송하는 시간이 오래 걸린다면 어떻게 될까요? 서비스 코드를 아래와 같이 수정해보겠습니다.

 

event/handler/SmsEventHandler.java

@Component
public class SmsEventHandler {

  @EventListener
  public void sendSms(RegisteredEvent event) throws InterruptedException {
    Thread.sleep(2000); // 2초
    System.out.println(event.getName() + "님에게 가입 축하 메세지를 전송했습니다.");
  }

  @EventListener
  public void makeCoupon(RegisteredEvent event) throws InterruptedException {
    Thread.sleep(1000); // 1초
    System.out.println(event.getName() + "님에게 쿠폰을 전송했습니다.");
  }
}

아까와 같이 순서대로 이벤트가 생성되긴 하지만, 회원 추가 완료 —2초 후→ 가입 축하 메세지 전송 —1초 후→ 쿠폰 전송 → 회원가입 완료의 순서대로 작업이 진행됩니다. 이 상황을 해결하기 위해 회원 추가가 완료되면 이벤트들은 비동기로 처리하는 추가 작업을 진행하도록 하겠습니다.

 

💻 구현 - 비동기 처리(성능 해결)

1. @EnableAsync 추가

SpringEventApplication.java

@EnableAsync // 추가
@SpringBootApplication
public class SpringEventApplication {

  public static void main(String[] args) {
    SpringApplication.run(SpringEventApplication.class, args);
  }

}

비동기 처리를 위해 Application에 @EnableAsync를 추가해줍니다. (Application이 아닌 config 파일에 추가해도 무방합니다.)

 

2. @Async 추가

event/handler/SmsEventHandler.java

@Component
public class SmsEventHandler {

  @Async // 추가
  @EventListener
  public void sendSms(RegisteredEvent event) throws InterruptedException {
    Thread.sleep(2000);
    System.out.println(event.getName() + "님에게 가입 축하 메세지를 전송했습니다.");
  }

  @Async // 추가
  @EventListener
  public void makeCoupon(RegisteredEvent event) throws InterruptedException {
    Thread.sleep(1000);
    System.out.println(event.getName() + "님에게 쿠폰을 전송했습니다.");
  }
}

비동기로 처리할 메소드에 @Async를 추가해줍니다.

 

3. 테스트

이번에는 회원 추가가 완료되면 바로 회원가입을 완료했다는 메세지가 뜨고, 그 이후에 순차적으로 쿠폰을 전송하고 축하 메세지를 전송하는 것을 확인할 수 있습니다. 이런식으로 정말 간단하게 비동기를 추가할 수 있고, 아까 문제점에 나왔던 성능 문제 역시 해결할 수 있습니다!


📕 구현 - 트랜잭션 해결

그렇다면 쿠폰을 전송하다 실패하면 평생 발급하지 못하는 것일까요? 그렇지 않습니다! 위 예제는 간단하게 @Async을 추가하는 것 이외에 처리하지 않았지만, 비동기 이벤트 처리를 하는 방법은 아래와 같습니다.

  • 로컬 핸들러를 비동기로 실행하기
  • 메세지 큐를 사용하기(RabbitMQ, Kafka ...)
  • 이벤트 저장소와 이벤트 포워더 사용하기
  • 이벤트 저장소와 이벤트 제공 API 사용하기

위 네가지 방법에 대한 자세한 설명이나 구현 방법은 DDD START!라는 책에 설명이 되어있으니 관심이 있으신 분들은 한 번씩 읽으면 좋을 것 같습니다 :)


✨ 정리

책을 읽다가 정말 좋은 내용인 것 같아서 정리해보았습니다! 혹시 글을 읽으면서 잘못된 내용이 있으면 댓글로 알려주시면 감사하겠습니다. 읽어주셔서 감사합니다! 😊


👏 참고