HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🍗
[New] 조규현팀
/
🏗️
Tech Store
/
🎣
Spring Event
🎣

Spring Event

담당자들
카테고리
Skill
주제
Spring Event
나의 블로그
완료율%
프로젝트
최종 프로젝트
상태
완료
[목표]
7/13 (수) 까지 완성하는게 목표입니다.
[목표 달성률]
현재 목표 달성률은 100%입니다.
주제SpringBoot Event 🤔 여기서 생기는 문제점은 무엇일까요?🐥 해결 방법은 ? Event ! 🫒 이벤트의 실행 단계🪐 이벤트 의존성 추가🐢 강한 결합 해결하기🐳 ApplicationEventPublisher🍉 결과⛄️ 성능 해결을 위한 비동기 처리🐧 결과🍁 트랜잭션 문제 해결 트랜잭션 문제해결 실습해보기 실행결과

주제

  • Spring Boot에서 이벤트를 어떻게 사용하는지 알아봅시다

SpringBoot Event

 
@Service public class RegisterService { public void register(String name) { // 회원가입 처리 로직 System.out.println("회원 추가 완료"); // 가입 축하 메세지 전송 System.out.println(name + "님에게 가입 축하 메세지를 전송했습니다."); // 가입 축하 쿠폰 발급 System.out.println(name + "님에게 쿠폰을 전송했습니다."); } }
  • 위와 같이 회원 가입, 메세지 전송, 쿠폰 발급의 로직을 타게 됩니다.

🤔 여기서 생기는 문제점은 무엇일까요?

  • 강한 결합
    • 현재 회원 가입 서비스에 회원 가입 로직 뿐만 아니라 가입 축하 메세지를 전송하는 로직, 가입 축하 쿠폰을 발급하는 로직이 모두 섞여있습니다.
    • 이렇게 강한 결합으로 묶여 있으면 후에 유지 보수가 어려울 뿐만 아니라 코드의 구조가 복잡해집니다.
  • 트랜잭션
    • 가입 축하 메세지를 전송하다가 예외가 발생하면 회원 가입을 한 이력까지 모두 롤백을 하는 것은 절대 좋은 방법이 아닙니다.
    • 회원 가입 처리를 해주고, 축하 메세지와 쿠폰 발급을 따로 관리하는게 옳은 방법입니다.
  • 성능
    • 만약 가입 축하 메세지를 전송하는데 3분, 쿠폰 발급에 3분이 걸린다면 회원 가입은 총 6분이 걸립니다.
    • 여기서 메인 이벤트는 회원 가입 처리 로직으로 서브 이벤트를 기다릴 필요가 없습니다.
    • 즉, 회원 가입 처리 → 가입 축하 메세지 전송 → 쿠폰 발급 → 회원 가입 처리 완료가 아닌
    • 회원 가입 처리 → 회원 가입 처리 완료 → 가입 축하 메세지 전송, 쿠폰발급 의 순서로 실행하면 됩니다.

🐥 해결 방법은 ? Event !

  • 이를 해결 하는 방법이 이벤트 ! 이벤트는 생성 주체의 상태가 변경되면 이벤트를 발생 시켜 원하는 기능을 실행해서 후처리를 도와줍니다.
 

🫒 이벤트의 실행 단계

  1. 생성 주체에서 이벤트를 발생하면 이벤트 디스패처에 전달합니다.
  1. 이벤트 디스패처가 이벤트 핸들러를 연결해줍니다.
  1. 이벤트 핸들러에서 이벤트에 담긴 데이터를 통해 원하는 기능을 실행합니다.
 

🪐 이벤트 의존성 추가

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

🐢 강한 결합 해결하기

  1. 이벤트 클래스 만들기
public class RegisteredEvent { private String name; public RegisteredEvent(String name) { this.name = name; } public String getName() { return name; } }
public class RegisteredEvent extends ApplicationEvent { private String name; public RegisteredEvent(Object source, String name) { super(source); this.name = name; } public String getName() { return name; } }
  • 이벤트는 상태가 바뀐 후에 발생하기 때문에 과거시제로 네이밍을 합니다.
  • 이벤트 클래스는 이벤트를 처리하는 데이터를 포함합니다.
 
  1. 서비스 만들기
@Service public class RegisterService { private final ApplicationEventPublisher publisher; public RegisterService(ApplicationEventPublisher publisher) { this.publisher = publisher; } public void signUp(String name) { //회원 가입 처리 로직 System.out.println("회원 추가 완료"); publisher.publishEvent(new RegisteredEvent(name)); // publisher.publishEvent(new RegisteredEvent(this, name)); } }
  • 이벤트를 보내는 기능을 사용하기 위해서 ApplicationEventPublisher를 주입해줍니다.
  • 회원 가입 처리를 완료하고 나면, publishEvent()를 사용해 이벤트를 전달해줍니다.
 

🐳 ApplicationEventPublisher

@FunctionalInterface public interface ApplicationEventPublisher { default void publishEvent(ApplicationEvent event) { publishEvent((Object) event); } void publishEvent(Object event); }
 
  1. 이벤트가 발생하면 핸들링 할 이벤트 핸들러 만들기
@Component public class SmsEventHandler { @EventListener public void sendSms(RegisteredEvent event) { System.out.println(event.getName() + "님에게 가입 축하 메세지를 전송했습니다."); } @EventListener public void makeCoupon(RegisteredEvent event) { System.out.println(event.getName() + "님에게 쿠폰을 전송했습니다."); } }
  • 어노테이션 기반으로 사용하기
@Component public class SmsEventListener implements ApplicationListener<RegisteredEvent> { @Override public void onApplicationEvent(RegisteredEvent event) { System.out.println("이건 뭐야?"); } }
  • EventListener를 사용하면 이벤트 리스너로 등록이 되고, 매개 변수에 이벤트 클래스를 정의하면 해당 이벤트가 발생했을 때 수신해서 처리를 할 수 있다.
 
  1. test controller
@RestController public class TestController { private final OrderService service; public TestController(OrderService service) { this.service = service; } @GetMapping("/register/{name}") public void register(@PathVariable String name) { service.signUp(name); System.out.println("회원가입을 완료했어요"); } }

🍉 결과

 
notion image
  • 결과는 잘 나오지만 중간의 서브 이벤트 때문에 회원 가입 완료 출력이 마지막에 나옵니다.

⛄️ 성능 해결을 위한 비동기 처리

  1. @EnableAsync 추가
@EnableAsync @SpringBootApplication public class SpringEventApplication { public static void main(String[] args) { SpringApplication.run(SpringEventApplication.class, args); } }
  • 비동기 처리를 위해 Application에 @EnableAsync를 추가합니다.
@Configuration public class AsyncEventConfig { @Bean(name = "applicationEventMulticaster") public ApplicationEventMulticaster simpleApplicationEventMulticaster() { SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster(); eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor()); return eventMulticaster; } }
  • config 파일에 추가해도 무방합니다.
 
  1. @Async 추가
@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() + "님에게 쿠폰을 전송했습니다ㅏ."); } }

🐧 결과

notion image
  • 실행하면 바로 결과가 나옵니다.
notion image
  • 이후 시간에 맞춰 해당 결과를볼 수 있습니다.

🍁 트랜잭션 문제 해결

  • @TransactionalEventListener 옵션
  • @TransactionalEventListener을 이용하면 트랜잭션의 어떤 타이밍에 이벤트를 발생시킬 지 정할 수 있습니다. 옵션을 사용하는 방법은 phase = TransactionPhase.AFTER_COMMIT.BEFORE_COMMIT 을 이용하는 것이며 아래와 같은 옵션을 사용할 수 있습니다.
  • 옵션
    • AFTER_COMMIT (기본값) - 트랜잭션이 성공적으로 마무리(commit)됐을 때 이벤트 실행
    • AFTER_ROLLBACK – 트랜잭션이 rollback 됬을 때 이벤트 실행
    • AFTER_COMPLETION – 트랜잭션이 마무리 됬을 때(commit or rollback) 이벤트 실행
    • BEFORE_COMMIT - 트랜잭션의 커밋 전에 이벤트 실행

트랜잭션 문제해결 실습해보기

1. Handler 코드
@Component public class SmsEventHandler { @Async @TransactionalEventListener public void sendSms(RegisteredEvent event) { System.out.println(event.getName() + "님에게 가입 축하 메세지를 전송했습니다."); } @Async @EventListener public void makeCoupon(RegisteredEvent event) { System.out.println(event.getName() + "님에게 쿠폰을 전송했습니다."); } }
  • 우리가 예상하는결과 ⇒ sendSms는 실행되지 않고, makeCoupon은 실행됩니다.
  1. service 코드
@Service public class OrderService { private final ApplicationEventPublisher publisher; public OrderService(ApplicationEventPublisher publisher) { this.publisher = publisher; } @Transactional public void signUp(String name) { //회원 가입 처리 로직 System.out.println("회원 추가 완료"); publisher.publishEvent(new RegisteredEvent(this, name)); if (!name.isBlank()) { throw new RuntimeException(); } } }
  • @Transactional넣어주고 임의로 예외를 터뜨렸습니다.

실행결과

 
notion image
  • 우리가 원하는대로 결과가 나왔습니다. !
  • @TransactionalEventListener 이 걸린 sendSms는 실행되지 않았습니다.