업데이트:


Spring의 IoC 컨테이너인 ApplicationContext가 상속받고 있는 ApplicationEventPublisher에 대해서 알아보자.

코드는 여기에 있다.

이 내용은 백기선 님의 인프런 스프링 프레임워크 핵심 기술 강좌의 IoC 컨테이너 8부: ApplicationEventPublisher 를 참고했다.

이 인터페이스는 옵저버 패턴의 구현체이다.

이벤트 기반의 프로그래밍을 할 때 유용한 인터페이스다.


Spring 4.2 이전

MyEventOld - 이벤트 클래스

@Getter
public class MyEventOld extends ApplicationEvent {

    private final String data;

    public MyEventOld(Object source, String data) {
        super(source);
        this.data = data;
    }
}

Spring 4.2 이전 버전에서는 위와같이 ApplicationEvent 추상 클래스를 상속받아야 했다.

이 이벤트 클래스는 Bean으로 등록하는게 아니다.

이 이벤트 클래스는 개발자가 원하는 데이터를 담아서 전송하는 용도의 클래스이다.

클래스를 목적에 맞게 구현하면 된다.


ApplicationEventPublisher - 이벤트 퍼플리셔 (컨트롤러 사용)

@Slf4j
@RequiredArgsConstructor
@ResponseBody
@Controller
public class EventController {

    private final ApplicationEventPublisher applicationEventPublisher;

    @GetMapping("/old/api/publish-event")
    public String publishMyEventOld(String data) {
        MyEventOld myEventOld = new MyEventOld(this, data);
        log.info("MyEventOld 이벤트 발생시킴. data = {}", data);
        applicationEventPublisher.publishEvent(myEventOld);
        return data;
    }
}

Event를 발생시키기 위해 Controller를 사용했다.

위와 같은 방식으로 MyEventOld라는 이벤트 객체를 publish(발생시키다)할 수 있다.

publish 직전에 위와 같이 이벤트 발생에 대한 로그를 남기도록 했다.


MyEventHandlerOld - 이벤트 핸들러

@Slf4j
@Component
public class MyEventHandlerOld implements ApplicationListener<MyEventOld> {

    @Override
    public void onApplicationEvent(MyEventOld myEventOld) {
        log.info("MyEventOld 이벤트 전달받음. data = {}", myEventOld.getData());
    }
}

이벤트 핸들러는 Bean으로 등록해야 한다.

Spring 4.2 이전 버전에서는 위와같이 ApplicationListener<E extends ApplicationEvent> 인터페이스를 구현해야 한다.

그리고 E에는 구독할 Event 클래스(위의 MyEventOld)를 넣어줘야 한다.

MyEventOld 이벤트가 발생되면 해당 객체를 가져와 안의 data 값을 로그로 찍도록 했다.


테스트

@SpringBootTest
@AutoConfigureMockMvc
@DisplayName("ApplicationEventPublisher 테스트")
class EventControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @DisplayName("Spring 4.2 버전 이전방식(MyEventOld) 테스트")
    @Test
    void publishMyEventOld() throws Exception {
        // given
        // when
        // then
        mockMvc.perform(get("/old/api/publish-event?data=oldData"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$").value("oldData"))
        ;
    }
}

image

테스트코드와 결과는 위와 같다.

위에서 볼 수 있듯이 작성한 로그도 정상적으로 출력됨을 확인할 수 있다.


Spring 4.2 부터

MyEventNew - 이벤트 클래스

@Getter
public class MyEventNew {

    private final Object source;
    private final String data;

    public MyEventNew(Object source, String data) {
        this.source = source;
        this.data = data;
    }
}

Spring 4.2 버전 부터는 위와같이 더이상 ApplicationEvent 추상 클래스를 상속받지 않아도 된다.

이 이벤트 클래스는 Bean으로 등록하는게 아니다.

이 이벤트 클래스는 개발자가 원하는 데이터를 담아서 전송하는 용도의 클래스이다.

클래스를 목적에 맞게 구현하면 된다.


ApplicationEventPublisher - 이벤트 퍼플리셔 (컨트롤러 사용)


@Slf4j
@RequiredArgsConstructor
@ResponseBody
@Controller
public class EventController {

    private final ApplicationEventPublisher applicationEventPublisher;

    @GetMapping("/new/api/publish-event")
    public String publishMyEventNew(String data) {
        MyEventNew myEventNew = new MyEventNew(this, data);
        log.info("myEventNew 이벤트 발생시킴. data = {}", data);
        applicationEventPublisher.publishEvent(myEventNew);
        return data;
    }
}

위와 같은 방식으로 myEventNew라는 이벤트 객체를 publish 하는 Controller 메서드를 작성했다.

publish 직전에 위와 같이 이벤트 발생에 대한 로그를 남기도록 했다.


MyEventHandlerNewA - 이벤트 핸들러

@Slf4j
@Component
public class MyEventHandlerNewA {

    @EventListener
    public void handle(MyEventNew myEventNew) {
        log.info("Thread name = {}", Thread.currentThread().getName());
        log.info("MyEventHandlerNewA에서 MyEventNew 이벤트 전달받음. data = {}", myEventNew.getData());
    }
}

이벤트 핸들러는 Bean으로 등록해야 한다.

Spring 4.2 버전부터는 위와같이 더이상

ApplicationListener<E extends ApplicationEvent> 인터페이스를 구현하지 않아도 된다.

구독중인 이벤트가 발생되면 실행시킬 메서드 위에 @EventListener 애노테이션을 달아주면 된다.

MyEventNew 이벤트가 발생되면, 해당 객체를 가져와 안의 data 값을 로그로 찍도록 했다.

Thread name을 로그로 찍는 부분은, 이후 내용을 위해 작성했다.


테스트

@SpringBootTest
@AutoConfigureMockMvc
@DisplayName("ApplicationEventPublisher 테스트")
class EventControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @DisplayName("Spring 4.2 버전 부터의 방식(MyEventOld) 테스트")
    @Test
    void publishMyEventNew() throws Exception {
        // given
        // when
        // then
        mockMvc.perform(get("/new/api/publish-event?data=newData"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$").value("newData"))
        ;
    }
}

image

테스트코드와 결과는 위와 같다.

위에서 볼 수 있듯이 작성한 로그도 정상적으로 출력됨을 확인할 수 있다.


@Order - 순서


같은 Event 클래스를 여러 EventHandler에서 구독하고 있으면 순서는 어떻게 보장할 수 있을까?

@Order 애너테이션으로 순서를 지정할 수 있다.


MyEventHandlerNewB 생성

@Slf4j
@Component
public class MyEventHandlerNewA {

    @EventListener
    public void handle(MyEventNew myEventNew) {
        log.info("Thread name = {}", Thread.currentThread().getName());
        log.info("MyEventHandlerNewA에서 MyEventNew 이벤트 전달받음. data = {}", myEventNew.getData());
    }
}

MyEventHandlerNewA와 동일한 EventHandler인 MyEventHandlerNewB를 위와 같이 만들었다.


스크린샷 2022-02-14 오후 10 59 59

위의 package org.springframework.coreOrdered 인터페이스를 보면,

  • 가장 높은 우선순위에 대한 상수 = HIGHEST_PRECEDENCE = 최소 정수값
  • 가장 낮은 우선순위에 대한 상수 = LOWEST_PRECEDENCE = 최대 정수값

이 있음을 확인할 수 있다.

즉, Ordered의 값이 낮을수록 더 높은 우선순위를 갖게됨을 알 수 있다.


“MyEventHandlerNewA -> MyEventHandlerNewB” 의 순서로 이벤트를 받도록 구성해보자.

@Slf4j
@Component
public class MyEventHandlerNewA {

    @Order(Ordered.HIGHEST_PRECEDENCE) // 이 부분 추가
    @EventListener
    public void handle(MyEventNew myEventNew) {
        log.info("Thread name = {}", Thread.currentThread().getName());
        log.info("MyEventHandlerNewA에서 MyEventNew 이벤트 전달받음. data = {}", myEventNew.getData());
    }
}

MyEventHandlerNewA를 위와같이 작성한다.

@Order(Ordered.HIGHEST_PRECEDENCE)를 추가해 최우선순위로 두었다.


@Slf4j
@Component
public class MyEventHandlerNewB {

    @Order(Ordered.HIGHEST_PRECEDENCE + 1) // 이 부분 추가
    @EventListener
    public void handle(MyEventNew myEventNew) {
        log.info("Thread name = {}", Thread.currentThread().getName());
        log.info("MyEventHandlerNewB에서 MyEventNew 이벤트 전달받음. data = {}", myEventNew.getData());
    }
}

MyEventHandlerNewB를 위와같이 작성한다.

@Order(Ordered.HIGHEST_PRECEDENCE + 1)을 추가해 최우선순위보다 1만큼 낮은 우선순위로 두었다.


image

그리고 아까 실행했던 "Spring 4.2 버전 부터의 방식(MyEventOld) 테스트"를 실행시켰더니,

위와 같이 MyEventHandlerNewA -> MyEventHandlerNewB 순으로 로그가 찍혔다.


MyEventHandlerNewB -> MyEventHandlerNewA의 순서로 이벤트를 받도록 구성해보자.

그럼 역순으로 이벤트를 받도록 구성해보자.

@Order(Ordered.HIGHEST_PRECEDENCE) 부분만 서로 바꿔주면 된다.

@Slf4j
@Component
public class MyEventHandlerNewA {

    @Order(Ordered.HIGHEST_PRECEDENCE + 1) // 이 부분 수정
    @EventListener
    public void handle(MyEventNew myEventNew) {
        log.info("Thread name = {}", Thread.currentThread().getName());
        log.info("MyEventHandlerNewA에서 MyEventNew 이벤트 전달받음. data = {}", myEventNew.getData());
    }
}

MyEventHandlerNewA를 위와같이 수정한다.

@Order(Ordered.HIGHEST_PRECEDENCE + 1)로 바꿔, 최우선순위보다 1만큼 낮은 우선순위로 두었다.


@Slf4j
@Component
public class MyEventHandlerNewB {

    @Order(Ordered.HIGHEST_PRECEDENCE) // 이 부분 수정
    @EventListener
    public void handle(MyEventNew myEventNew) {
        log.info("Thread name = {}", Thread.currentThread().getName());
        log.info("MyEventHandlerNewB에서 MyEventNew 이벤트 전달받음. data = {}", myEventNew.getData());
    }
}

MyEventHandlerNewB를 위와같이 수정한다.

@Order(Ordered.HIGHEST_PRECEDENCE)로 바꿔, 최우선순위로 두었다.


image

그리고 아까 실행했던 "Spring 4.2 버전 부터의 방식(MyEventOld) 테스트"를 실행시켰더니,

이전 테스트와 반대 순서인 MyEventHandlerNewB -> MyEventHandlerNewA 순으로 로그가 찍혔다.


@Async - 비동기

@Slf4j
@RequiredArgsConstructor
@ResponseBody
@Controller
public class EventController {

    private final ApplicationEventPublisher applicationEventPublisher;

    @GetMapping("/old/api/publish-event")
    public String publishMyEventOld(String data) {
        MyEventOld myEventOld = new MyEventOld(this, data);
        log.info("Thread name = {}", Thread.currentThread().getName()); // 이 부분 추가
        log.info("MyEventOld 이벤트 발생시킴. data = {}", data);
        applicationEventPublisher.publishEvent(myEventOld);
        return data;
    }

    @GetMapping("/new/api/publish-event")
    public String publishMyEventNew(String data) {
        MyEventNew myEventNew = new MyEventNew(this, data);
        log.info("Thread name = {}", Thread.currentThread().getName()); // 이 부분 추가
        log.info("myEventNew 이벤트 발생시킴. data = {}", data);
        applicationEventPublisher.publishEvent(myEventNew);
        return data;
    }
}

EventController에 위와 같이 스레드의 이름을 로그로 찍도록 해보자.

"Spring 4.2 버전 부터의 방식(MyEventOld) 테스트"를 실행시켜보자.


image

위의 로그 중에 Thread name = Test worker 부분을 보면 알겠지만,

  • 이벤트가 발생되는 스레드 (EventController)
  • 이벤트를 받는 스레드(MyEventHandlerNewA, MyEventHandlerNewB)

가 모두 Test worker로 같음을 알 수 있다.


@Slf4j
@Component
public class MyEventHandlerNewA {

    @Async // 이 부분 추가
    @Order(Ordered.HIGHEST_PRECEDENCE + 1)
    @EventListener
    public void handle(MyEventNew myEventNew) {
        log.info("Thread name = {}", Thread.currentThread().getName());
        log.info("MyEventHandlerNewA에서 MyEventNew 이벤트 전달받음. data = {}", myEventNew.getData());
    }
}
@Slf4j
@Component
public class MyEventHandlerNewB {

    @Async // 이 부분 추가
    @Order(Ordered.HIGHEST_PRECEDENCE)
    @EventListener
    public void handle(MyEventNew myEventNew) {
        log.info("Thread name = {}", Thread.currentThread().getName());
        log.info("MyEventHandlerNewB에서 MyEventNew 이벤트 전달받음. data = {}", myEventNew.getData());
    }
}

MyEventHandlerNewAMyEventHandlerNewB의 메서드에,

위와 같이 @Async 애노테이션을 붙이면 이벤트를 받는 부분들이 비동기적으로 실행된다.

하지만 @Async 애노테이션만 붙인다고 바로 비동기 실행이 되는 것은 아니다.


image

이 상태로 실행하면, 위처럼 이전과 같이 모두 Test worker 스레드에서 실행된다.


@EnableAsync // 이 부분 추가
@SpringBootApplication
public class ApplicationEventPublisherApplication {

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

}

@Async가 동작하도록 하려면, Spring Boot Application 클래스에 위와 같이 @EnableAsync 애너테이션을 붙여야 한다.

원래는 Thread pool과 관련된 설정을 더 해야하지만, 비동기 여부만 확인하면 되므로 추가적인 설정은 하지 않겠다.

이 상태로 다시 Spring 4.2 버전 부터의 방식(MyEventOld) 테스트를 실행시켜보자.


image

위처럼

  • 이벤트가 발생되는 스레드 (EventController)
  • 이벤트를 받는 스레드(MyEventHandlerNewA, MyEventHandlerNewB)

부분들이 모두 각각 개별 스레드에서 실행되었음을 알 수 있다.


“.http” 파일로 테스트

실제 애플리케이션을 실행시키고, .http 파일로 로그를 확인해보자.

.http 파일과 관련된 내용은 이동욱 님IntelliJ의 .http를 사용해 Postman 대체하기를 참고하자.

### Spring 4.2 이전 버전
GET http://localhost:8080/old/api/publish-event?data=oldData
Accept: application/json

### Spring 4.2 부터의 버전
GET http://localhost:8080/new/api/publish-event?data=newData
Accept: application/json

application-event-publisher.http 파일을 위와 같이 작성한다.


Spring 4.2 이전 버전

그리고 Spring Boot Application을 띄운 뒤에 ### Spring 4.2 이전 버전 요청을 해보면,

스크린샷 2022-02-14 오후 10 43 28

위와 같이 200응답과 ResponseBody 응답이 잘 나온다.

스크린샷 2022-02-14 오후 10 44 44

Spring Boot Application 로그도 위와 같이 잘 출력됨을 알 수 있다.


Spring 4.2 부터의 버전

###Spring 4.2 부터의 버전 요청을 해보면,

스크린샷 2022-02-14 오후 10 47 01

위와 같이 200응답과 ResponseBody 응답이 잘 나온다.

image

Spring Boot Application 로그도 위와 같이 잘 출력됨을 알 수 있다.


태그: ,

업데이트:

댓글남기기