[목표]
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 !
- 이를 해결 하는 방법이 이벤트 ! 이벤트는 생성 주체의 상태가 변경되면 이벤트를 발생 시켜 원하는 기능을 실행해서 후처리를 도와줍니다.
🫒 이벤트의 실행 단계
- 생성 주체에서 이벤트를 발생하면 이벤트 디스패처에 전달합니다.
- 이벤트 디스패처가 이벤트 핸들러를 연결해줍니다.
- 이벤트 핸들러에서 이벤트에 담긴 데이터를 통해 원하는 기능을 실행합니다.
🪐 이벤트 의존성 추가
- 의존성 추가 : spring-boot-starter-web
🐢 강한 결합 해결하기
- 이벤트 클래스 만들기
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; } }
- 이벤트는 상태가 바뀐 후에 발생하기 때문에 과거시제로 네이밍을 합니다.
- 이벤트 클래스는 이벤트를 처리하는 데이터를 포함합니다.
- 서비스 만들기
@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); }
- 이벤트가 발생하면 핸들링 할 이벤트 핸들러 만들기
@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
를 사용하면 이벤트 리스너로 등록이 되고, 매개 변수에 이벤트 클래스를 정의하면 해당 이벤트가 발생했을 때 수신해서 처리를 할 수 있다.
- 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("회원가입을 완료했어요"); } }
🍉 결과

- 결과는 잘 나오지만 중간의 서브 이벤트 때문에 회원 가입 완료 출력이 마지막에 나옵니다.
⛄️ 성능 해결을 위한 비동기 처리
@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 파일에 추가해도 무방합니다.
@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() + "님에게 쿠폰을 전송했습니다ㅏ."); } }
🐧 결과

- 실행하면 바로 결과가 나옵니다.

- 이후 시간에 맞춰 해당 결과를볼 수 있습니다.
🍁 트랜잭션 문제 해결
@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은 실행됩니다.
- 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
넣어주고 임의로 예외를 터뜨렸습니다.
실행결과

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