Spring IoC 컨테이너 ApplicationEventPublisher
업데이트:
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"))
;
}
}
테스트코드와 결과는 위와 같다.
위에서 볼 수 있듯이 작성한 로그도 정상적으로 출력됨을 확인할 수 있다.
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"))
;
}
}
테스트코드와 결과는 위와 같다.
위에서 볼 수 있듯이 작성한 로그도 정상적으로 출력됨을 확인할 수 있다.
@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
를 위와 같이 만들었다.
위의 package org.springframework.core
의 Ordered
인터페이스를 보면,
- 가장 높은 우선순위에 대한 상수 =
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만큼 낮은 우선순위로 두었다.
그리고 아까 실행했던 "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)
로 바꿔, 최우선순위로 두었다.
그리고 아까 실행했던 "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) 테스트"
를 실행시켜보자.
위의 로그 중에 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());
}
}
MyEventHandlerNewA
와 MyEventHandlerNewB
의 메서드에,
위와 같이 @Async
애노테이션을 붙이면 이벤트를 받는 부분들이 비동기적으로 실행된다.
하지만 @Async
애노테이션만 붙인다고 바로 비동기 실행이 되는 것은 아니다.
이 상태로 실행하면, 위처럼 이전과 같이 모두 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) 테스트
를 실행시켜보자.
위처럼
- 이벤트가 발생되는 스레드 (
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 이전 버전
요청을 해보면,
위와 같이 200응답과 ResponseBody 응답이 잘 나온다.
Spring Boot Application 로그도 위와 같이 잘 출력됨을 알 수 있다.
Spring 4.2 부터의 버전
###Spring 4.2 부터의 버전
요청을 해보면,
위와 같이 200응답과 ResponseBody 응답이 잘 나온다.
Spring Boot Application 로그도 위와 같이 잘 출력됨을 알 수 있다.
댓글남기기