본문 바로가기

Spring/Spring

Spring ApplicationEvent 비동기로 처리될 것만 같지?

반응형

ApplicationEventListener는 비동기가 아니다

스프링(Spring) 프레임워크를 공부하면서 왠지 모르게 가끔씩 이벤트 드리븐(Event Driven)이라는 단어도 듣게 됐다.

뿐만 아니라 스프링 5 이후로는 WebFlux가 등장하면서 논블록킹(NonBlocking), 리액티브(Reactive)라는 단어도 듣게 됐다.

그 둘이 무슨 연관성이냐 할 수 있지만, 개념에 대해서 정리가 안되어서 그런지 나는 이벤트 = 비동기 같은 생각을 은연중에 하게되었다.

이런 상황이다 보니 ApplicationContext를 이용하여 ApplicationEventPublish하고 Listen하는 기능을 알았을 때, '아 이렇게 하는 거구나' 하고 넘겼고, 아무 생각 없이 "비동기"로 처리되겠거니 했다.

그러나 실제로 테스트해보니까 그렇지 않았다.

이벤트를 발행하는 스레드나 이벤트를 리슨하고 받아 동작하는 스레드나 같은 스레드였다.

같은 스레드로 동작하는지 테스트

테스트용 컨트롤러를 만들어서 테스트해본다.

@RestController
@RequiredArgsConstructor
@Slf4j
public class VacationController {
    private final ApplicationEventPublisher applicationEventPublisher;
    private final VacationService vacationService;

    @GetMapping(value = "/vacationCreatedEvent")
    public ResponseEntity<String> test1() {
        log.info("Controller Thread ID : {}", Thread.currentThread().getId());
        applicationEventPublisher.publishEvent(new VacationCreateEvent(applicationEventPublisher, 2L, "vacationCreated"));
        log.info("after publishEvent!! : {}", Thread.currentThread().getId());
        return ResponseEntity.ok("vacationCreatedEvent");
    }
}

localhost:8080/vacationCreatedEvent 로 Get요청을 하면 컨트롤러에서 로그로 스레드 아이디를 찍고, applicationEventPublisher로 이벤트를 발생시키고 난 후에 또 로그로 스레드 아이디를 찍는 로직이다.

이벤트를 받아 처리하는 부분도 테스트로 작성해본다.

@Component
@Slf4j
public class TempComponent {
    @EventListener
    public void eventListen(VacationCreateEvent vacationCreateEvent) {
        log.info("TempComponent Thread Id : {}, eventId : {}", Thread.currentThread().getId(), vacationCreateEvent.getId());
    }
}

위의 컴포넌트 말고도 하나 더 아래와 같이 만들었다.

@Service
@Slf4j
@RequiredArgsConstructor
public class VacationServiceImpl implements VacationService {
        private final VacationRepository vacationRepository;

        @EventListener(VacationCreateEvent.class)
    public void createdVacation(VacationCreateEvent vacationCreateEvent) {
        log.info("VacationServiceImpl Thread Id : {}, eventId : {}", Thread.currentThread().getId(), vacationCreateEvent.getId());
    }
}

만약, 비동기로 동작한다면 VacationServiceImpl 클래스와 TempComponent 클래스에서의 스레드 아이디가 각각 다를 것이다.

뿐만 아니라 로그가 찍히는 순서도 다를 것이다.

결과는 아래와 같다.

같은 스레드가 동기로 이벤트를 처리하는 것을 확인할 수 있다.

ApplicationEventListener를 아무리 많이 만들어도 하나의 스레드가 그 이벤트 리스너에 정의한 동작을 수행할 것이고, 그게 다 끝난 후에야 남은 작업을 할 것이라는 것 즉, 동기로 동작할 것을 알 수 있다.


비동기로 동작시켜보자

특별히 설정할 것은 없다.

@EnableAsync 애노테이션을 설정하고, 비동기로 동작하고자 하는 메서드 위에 @Async 애노테이션을 설정하면 된다.

(@Async 애노테이션을 달면 무조건 비동기로 동작하지는 않는다 프록시와 관련되어 있으니 무슨 말 하는지 이해할 수 없다면 다음 링크를 학습하면 좋다.)

@SpringBootApplication
@EnableAsync
public class JeongproApplication {
    public static void main(String[] args) {
        SpringApplication.run(JeongproApplication.class, args);
    }
}

메인 메서드를 실행하는 곳에다가 @EnableAsync 애노테이션을 설정했다. (굳이 여기다가 하지 않고 @Configuration 애노테이션이 설정된 클래스에서 해도 된다.)

그다음 아까 @EventListener 가 설정된 메서드에 아래와 같이 @Async 애노테이션을 붙여준다.

@Service
@Slf4j
@RequiredArgsConstructor
public class VacationServiceImpl implements VacationService {
    private final VacationRepository vacationRepository;

    @EventListener(VacationCreateEvent.class)
    @Async // 여기
    public void createdVacation(VacationCreateEvent vacationCreateEvent) {
        log.info("VacationServiceImpl Thread Id : {}, eventId : {}", Thread.currentThread().getId(), vacationCreateEvent.getId());
    }
}
@Component
@Slf4j
public class TempComponent {
    @EventListener
    @Async // 여기
    public void eventListen(VacationCreateEvent vacationCreateEvent) {
        log.info("TempComponent Thread Id : {}, eventId : {}", Thread.currentThread().getId(), vacationCreateEvent.getId());
    }
}

결과는 아래와 같다.

스레드 ID 뿐만 아니라 로그에 찍히는 Name?도 다르다. 그리고 순서도 VacationController에서 찍은 로그가 먼저 따닥 찍혔다.

GET READY FOR THE NEXT BATTLE

그냥 '스프링의 이벤트 리스너가 동기로 동작하는구나' + '비동기로 동작하게 하려면 @Async 를 붙이면 되는구나' 하고 끝나면 안 된다.

대부분의 애플리케이션을 개발할 때는 보통 트랜잭션(Transaction)이 연관되어 있다.

그렇기 때문에 이벤트 드리븐으로 애플리케이션 로직을 개발할 때 비동기와 트랜잭션이 엮였을 때 어떻게 동작하게 되는지도 알아야 할 필요가 있다.

그다음 주제와 연관된 키워드는@TransactionalEventListener 애노테이션이다.

다음 포스트에서 트랜잭션과 비동기로 동작하는 이벤트 리스너가 어떻게 동작하는지 살펴본다.

ps. 한 번에 트랜잭션까지 정리하면 좋지만 하나의 메서드가 하나의 동작만 하듯 주제가 비동기가 아님을 확인하는 주제였으므로 다음 포스트로 넘긴다. (라고 핑계를 댄다.)

반응형