Spring Boot EventHandler

김파란·2024년 12월 13일

SpringAdv

목록 보기
7/8

참고) https://velog.io/@penrose_15/Spring-Boot-EventHandler
https://gudwnsgur.tistory.com/19
https://wildeveloperetrain.tistory.com/246

이벤트를 사용하는 이유

  • 결합도를 끊어내어 관심사를 분리할 수 있다
  • Spring에서 Bean과 Bean사이의 데이터를 전달하는 방법 중 하나이다
  • Bean A에서 이벤트를 ApplicationContext로 넘겨주고 이를 Listener에서 받아서 처리한다

이벤트 구성 요소 및 동작

  • Event는 크게 Event Class와 이벤트를 발생시키는 Event Publisher 그리고 이벤트를 받아들이는 Event Listener 3가지 요소로 볼 수 있다
    • 생성 주체에서 이벤트를 발생하면 이벤트 디스패처에게 전달한다
    • 이벤트 디스패처에서 이벤트 핸들러를 연결해준다
    • 이벤트 핸들러에서 이벤트에 담긴 데이터를 통해 원하는 기능을 실행한다
  • 내부적으로 Observer패턴이 구현되어 있다
  • 스프링 이벤트는 기본적으로 동기 방식으로 동작한다
    • 이때문에 이벤트를 처리하는데 오랜 시간이 걸리면 전체 프로세스가 대기하게 된다

(1). Event Class

  • 4.2까지는 ApplicationEVent를 상속받아서 사용했으나 현재는 Object로만 사용하면 된다
public class OrderedEvent {

    private String productName;

    public OrderedEvent(String productName) {
        this.productName = productName;
    }

    public String getProductName() {
        return productName;
    }
}

(2). Event Publisher

  • ApplicationEventPublisher 빈을 주입하여 publishEvent() 메서드를 통해 생성된 이벤트 객체를 넣어준다
  • ApplicationContext가 ApplicationEventPublisher를 상속하고 있기 때문에 Publisher와 Listener는 스프링 빈으로 등록되어야 한다
  • Spring이 초기화될 때 @EventListener, @TransactionalEventListener를 ApplicationContext에 등록을 해두고 publishEvent()가 실행되면 등록된 Listener에 Event를 뿌려준다
public class OrderService {

    ApplicationEventPublisher publisher;

    public OrderService(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }

    public void order(String productName) {
   
        publisher.publishEvent(new OrderedEvent(productName));
   
    }
}

(3). Event Listener

  • 4.2이하는 ApplicationListener<CustomEvent>를 상속받아서 썻으나 현재는 애노테이션으로 이벤트를 캐치할 수 있다
  • 이벤트가 발생하면 @EventListenerCustomEvent 클래스를 파라미터로 받는 메서드를 모두 실행하기 때문에 이벤트를 받을 파라미터의 중복에 주의 해야 한다
	@EventListener
    public void sendPush(OrderedEvent event) throws InterruptedException {
        log.info(String.format("푸시 메세지 발송 [상품명 : %s]", event.getProductName()));
    }

주의점

동일 트랜잭션

  • EventListener는 같은 트랜잭션에 묶여 있어서 만약 주문은 성공적으로 끝났는데 알림서비스에서 문제가 발생긴다면 전부 롤백시켜야 하는 경우가 발생한다
  • 이를 위해서 주문 트랜잭션이 성공적으로 끝난 이후에 알림 생성이 이루어져야 한다

동기적 방식

  • 주문과 알림이 동기적으로 일어날 이유가 없다
  • 동기적으로 움직이면 오히려 주문 생성의 시간만 길어질 뿐이다

트랜잭션 문제 해결

  • 동일 트랜잭션 문제를 하기 위해서 TransactionalEventListener을 사용하면 된다
  • 기본 설정은 After_Commit으로 트랜잭션이 성공적으로 커밋된 후 Listener로직을 수행하라는 뜻이다
  • 4가지 옵션이 있다

옵션

  • Before_Commit : 커밋되기 전에 실행한다. 이럴 경우 TransactionalEventListener을 쓰는 이유가 없어진다
  • After_Commit : 기본 값으로, 커밋이 성공적으로 완료된 후에만 실행되므로, 안정적인 후속 처리가 필요할 때 유용하다
  • After_Rollback: 트랜잭션이 실패하고 롤백된 후에 이벤트를 처리하거나 롤백 작업을 할 때 유용하다
  • After_Completed: After_Rollback + After_Commit
    • 트랜잭션이 완료된 후(커밋 또는 롤백 후)에 실행된다
	@RequiredArgsConstructor
	@Component
	public class CustomEventListener {
    	private final NotificationService notificationService;

    // 이벤트 발행된 곳의 트랜잭션 완료 후 실행
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handler(Notice notice) {
        notificationService.createNotification(notice);
    }
}

주의점

  • After_Commit을 사용시 이전의 이벤트를 publish 하는 코드에서 트랜잭션이 이미 커밋되었기 때문에 After_Commit이후 새로운 트랜잭션 수행 시 트랜잭션을 커밋하지 않는다
  • @TransactionalEventListener의 경우 event publisher의 트랜잭션 안에서 동작하며, 커밋이 된 이후 추가 커밋을 허용하지 않는다
  • 즉 After_Commit을 사용하면 트랜잭션은 적용되지 않는다
  • 그래서 Propagation = Require_new를 통해 새로운 트랜잭션을 열어 수행해줘야 한다

TransactionSynchronization

  • 애노테이션을 사용하지 않고 프로그래밍 방식으로 작성하는 방법도 있다
  • 선언적 트랜잭션 리스너와 똑같이 4가지가 있고 상황에 맞게 사용하면 된다
  • 트랜잭션과 이벤트 처리에 세밀하고 복잡한 제어가 가능하다
		// 트랜잭션 커밋 후 이벤트 발행
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                eventPublisher.publishEvent(new ItemRegistrationEvent(this, item.getName(), item.getPrice()));
            }
        });

비동기처리

  • 비동기처리를 위한 애노테이션을 넣어주면 된다
  • @AsyncEnableAsync만 선언해주면 끝난다
  • Thread관리를 하고 싶다면 Config 설정을 해줘야 한다
  @Async // 추가
  @EventListener
  public void sendSms(RegisteredEvent event) throws InterruptedException {
    Thread.sleep(2000);
    System.out.println(event.getName() + "님에게 가입 축하 메세지를 전송했습니다.");
  }
@EnableAsync
@Configuration
public class AsyncConfig {
    @Bean(name = "threadPoolTaskExecutor")
    public Executor  threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(3); // 기본 스레드 수
        threadPoolTaskExecutor.setMaxPoolSize(30); // 최대 스레드 수
        threadPoolTaskExecutor.setQueueCapacity(10); // Queue 수
        threadPoolTaskExecutor.setThreadNamePrefix("Executor-");
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }
}

0개의 댓글