Spring 이벤트 시스템을 이용한 실시간 데이터 변경 감지 및 처리
애플리케이션 내에서 데이터의 변경을 실시간으로 감지 및 처리를 위해서 Spring 프레임워크의 이벤트 시스템을 활용할 수 있다.
Spring 이벤트 시스템은 옵저버 패턴에 대한 추상화를 제공하여 결합도를 낮추면서도 강력한 이벤트 기반 프로그래밍 모델을 제공한다.
이를 통해 데이터 변경 이벤트를 실시간으로 감지하고 처리하는데 매우 유용하다.
이번 포스팅에서는 Spring 이벤트 시스템을 이용하여 실시간 데이터 변경을 감지하고 처리하는 방법에 대해서 정리하고자 한다.
Spring 이벤트 시스템 개요
Spring 프레임워크는 강력한 이벤트 시스템을 제공하여 애플리케이션 내에서 발생하는 다양한 이벤트를 처리할 수 있도록 한다. Spring 이벤트 시스템의 주요 구성요소는 다음과 같다.
- Event: 발생한 이벤트를 나타내는 객체
- Event Publisher: 이벤트를 발행하는 컴포넌트
- Event Listener: 발행된 이벤트를 처리하는 컴포넌트
List 객체의 변화를 감지하여 이벤트를 발행하고 처리하는 예제 코드를 작성해 보았다.
예제 코드는 Spring 4.2 이후 버전에서 동작하는 코드다. Spring 4.2 버전에서 @EventListener 어노테이션이 도입되어, 이를 사용하여 이벤트 리스너를 간단하게 정의할 수 있다.
Spring 4.2 이전 버전에서는 ApplicationListerner 인터페이스를 구현하여 이벤트 리스너를 만들 수 있다.
이벤트 클래스 작성
이벤트 리스너에 전달될 이벤트를 나타내는 클래스를 정의한다.
@Getter
@ToString
public class ListChangeEvent {
String data;
String message;
public ListChangeEvent(String data, String message) {
this.data = data;
this.message = message;
}
}
ListChangeEvent 클래스는 List 인스턴스에 데이터가 추가/삭제되었을 때 전달되는 이벤트 역할을 할 것이다.
데이터 변경 감지 및 이벤트 발행 클래스 작성
List 객체의 변화를 감지하여 이벤트를 발행하는 클래스를 정의한다.
@Component
public class ListData {
private final List<String> sharedListData = new CopyOnWriteArrayList<>();
private final ApplicationEventPublisher applicationEventPublisher;
public ListData( ApplicationEventPublisher applicationEventPublisher ) {
this.applicationEventPublisher = applicationEventPublisher;
}
public void addData( String data ) {
sharedListData.add( data );
applicationEventPublisher.publishEvent( new ListChangeEvent( data, "added" ) );
}
public void removeData( String data ) {
if ( sharedListData.remove( data ) ) {
applicationEventPublisher.publishEvent( new ListChangeEvent( data, "removed" ) );
}
}
}
ListData 클래스에서는 sharedListData 의 변화를 감지하여 ApplicationEventPublisher 인스턴스를 통해서 이벤트를 발행시키는 역할을 한다.
이벤트 리스너 클래스 작성
@Component
public class ListChangedEventListener {
@EventListener
public void listEventHandler(ListChangeEvent listChangeEvent) {
System.out.println(listChangeEvent);
//이벤트를 처리한다.
}
}
@EventListener 어노테이션으로 지정된 listEventHandler 메서드로 발행된 ListChangeEvent 인스턴스가 전달된다.
@EventListener 어노테이션이 지정된 핸들러 메서드가 여러 개 있는 경우 어떤 이벤트 핸들러 메서드가 호출되는지는 파라미터로 전달되는 이벤트 인스턴스의 타입에 따라 자동으로 결정된다.
데이터 변경 코드
@SpringBootApplication
@RequiredArgsConstructor
public class SpringEventExampleApplication implements CommandLineRunner {
private final ListData listData;
public static void main( String[] args ) {
SpringApplication.run( SpringEventExampleApplication.class, args );
}
@Override
public void run( String... args ) throws Exception {
listData.addData( "spring" );
listData.addData( "event" );
listData.addData( "처리" );
listData.removeData( "처리" );
listData.removeData( "event" );
listData.removeData( "spring" );
}
}
실행 결과
ListChangeEvent(data=spring, message=added)
ListChangeEvent(data=event, message=added)
ListChangeEvent(data=처리, message=added)
ListChangeEvent(data=처리, message=removed)
ListChangeEvent(data=event, message=removed)
ListChangeEvent(data=spring, message=removed)
이벤트 리스너 체이닝
위 예제 코드에서 ListChangeEventListener 클래스의 listEventHandler 핸들러 메서드의 리턴타입은 void다. 일반적으로 리턴타입을 void로 지정하지만 다른 객체를 반환할 수도 있다. 이때 반환된 객체는 또 다른 이벤트로 간주되어 다시 이벤트 시스템으로 전파될 수 있다.
ListChangeEventListener 클래스를 다음과 같이 변경 후 실행해 보자.
@Component
public class ListChangedEventListener {
@EventListener
public EndEvent listEventHandler( ListChangeEvent listChangeEvent) {
System.out.println(listChangeEvent);
//이벤트를 처리한다.
return new EndEvent( "end event handler, data: " + listChangeEvent.getData() + ", action: " + listChangeEvent.getMessage());
}
@EventListener
public void endEventHandler(EndEvent endEvent) {
System.out.println(endEvent.getMessage());
}
@Getter
public static class EndEvent {
private final String message;
EndEvent(String message) {
this.message = message;
}
}
}
listEventHandler 에서는 이벤트를 처리 후에 EndEvent 객체를 리턴하였다.
이때 EndEvent 객체는 또 다른 이벤트로 간주되어 endEventHandler 메서드로 전달이 된다.
실행 결과
ListChangeEvent(data=spring, message=added)
end event handler, data: spring, action: added
ListChangeEvent(data=event, message=added)
end event handler, data: event, action: added
ListChangeEvent(data=처리, message=added)
end event handler, data: 처리, action: added
ListChangeEvent(data=처리, message=removed)
end event handler, data: 처리, action: removed
ListChangeEvent(data=event, message=removed)
end event handler, data: event, action: removed
ListChangeEvent(data=spring, message=removed)
end event handler, data: spring, action: removed
@EventListener 속성
@EventListener 어노테이션은 Spring의 이벤트 처리 메커니즘을 사용할 때 이벤트 리스너로 등록하는 데 사용된다. 이 어노테이션에는 다양한 속성을 설정할 수 있는데 각 속성의 역할과 사용법은 다음과 같다.
classes와 value
이 속성은 핸들러 메서드가 처리할 이벤트 타입을 명시적으로 지정한다. 메서드 파라미터 타입을 유추하는 대신, 이 속성을 사용하여 이벤트 타입을 명시적으로 지정할 수 있다.
classes 속성과 value 속성은 동일한 역할을 한다. 따라서 classes 속성과 value 속성 둘 중 하나를 사용하면 되겠다.
이 속성에 지정되는 클래스는 상속관계가 허용이 된다. 즉, 지정한 클래스 타입의 이벤트뿐 아니라 그 하위 클래스 타입의 이벤트도 처리할 수 있다.
예제 코드를 통해서 알아보자.
부모 Event 클래스 정의
@Getter
@ToString
public class BaseEvent {
private final String message;
public BaseEvent(String message) {
this.message = message;
}
}
자식 Event 클래스 정의
@Getter
@ToString
public class ListChangeEvent extends BaseEvent {
private final String data;
public ListChangeEvent(String data, String message) {
super(message);
this.data = data;
}
}
데이터 변경 감지 클래스 정의
@Component
public class ListData {
private final List<String> sharedListData = new CopyOnWriteArrayList<>();
private final ApplicationEventPublisher applicationEventPublisher;
public ListData( ApplicationEventPublisher applicationEventPublisher ) {
this.applicationEventPublisher = applicationEventPublisher;
}
public void addData( String data ) {
sharedListData.add( data );
applicationEventPublisher.publishEvent( new ListChangeEvent( data,"added" ) );
}
public void removeData( String data ) {
if ( sharedListData.remove( data ) ) {
applicationEventPublisher.publishEvent( new ListChangeEvent( data, "removed" ) );
}
}
public void clearData() {
sharedListData.clear();
applicationEventPublisher.publishEvent( new BaseEvent( "cleared" ) );
}
}
이벤트 리스너 클래스 정의
@Component
public class ListChangedEventListener {
//classes 속성은 이벤트 인스턴스의 타입을 명시적으로 알려주는 역할을 하기 때문에
//속성을 지정하지 않아도 동일하게 동작한다.
@EventListener(classes = BaseEvent.class)
public void baseEventHandler(BaseEvent baseEvent) {
System.out.println("baseEventHandler called, " + baseEvent);
}
//classes 속성은 이벤트 인스턴스의 타입을 명시적으로 알려주는 역할을 하기 때문에
//속성을 지정하지 않아도 동일하게 동작한다.
@EventListener(classes = ListChangeEvent.class)
public void listEventHandler(ListChangeEvent listChangeEvent) {
System.out.println("listEventHandler called, " + listChangeEvent);
}
}
데이터 변경 코드
private final ListData listData;
public static void main( String[] args ) {
SpringApplication.run( SpringEventExampleApplication.class, args );
}
@Override
public void run( String... args ) {
listData.addData( "spring" );
listData.clearData();
}
실행 결과
baseEventHandler called, ListChangeEvent(data=spring)
listEventHandler called, ListChangeEvent(data=spring)
baseEventHandler called, BaseEvent(message=cleared)
addData() 메서드 호출로 인해 발행된 ListChangedEvent는 BaseEvent를 상속받은 이벤트로써 이벤트 리스너 클래스의 baseEventHandler(), listEventHandler() 모두 호출이 된다.
반면, clearData() 메서드 호출로 인해 발행된 BaseChangedEvent는 baseEventHandler() 핸들러 메서드만 호출되는 것일 확인할 수 있다.
classes 속성에 이벤트 객체의 여러 클래스 타입을 선언한 경우에는 다음과 같이 처리할 수 있다.
이벤트 리스너 클래스 정의
@Component
public class ListChangedEventListener {
@EventListener(classes = {FirstEvent.class, SecondEvent.class})
public void arrayClassesEvent(Object object) {
if ( object instanceof FirstEvent event ) {
System.out.println(event.getMessage());
}
else if ( object instanceof SecondEvent event ) {
System.out.println(event.getMessage());
}
}
}
이벤트 발행
@SpringBootApplication
@RequiredArgsConstructor
public class SpringEventExampleApplication implements CommandLineRunner {
private final ApplicationEventPublisher applicationEventPublisher;
public static void main( String[] args ) {
SpringApplication.run( SpringEventExampleApplication.class, args );
}
@Override
public void run( String... args ) {
applicationEventPublisher.publishEvent( new ListChangedEventListener.FirstEvent("first event") );
applicationEventPublisher.publishEvent( new ListChangedEventListener.SecondEvent("second event") );
}
}
실행 결과
first event
second event
이벤트 핸들러 메서드에서 Object 인스턴스 타입을 체크해 줘야 한다.
condition
스프링 표현 언어(SpEL)를 사용하여 이벤트를 처리할 조건을 지정한다. 조건이 참인 경우에만 메서드가 호출되어 조건적으로 이벤트 핸들러를 수행할 수 있다.
아래 코드는 listChangedEvent의 data 필드의 값이 'spring'인 경우에만 이벤트 핸들러 메서드가 호출된다.
@Component
public class ListChangedEventListener {
@EventListener(condition = "#listChangeEvent.data == 'spring'")
public void listEventHandler(ListChangeEvent listChangeEvent) {
System.out.println("listEventHandler called, " + listChangeEvent);
}
}
@Override
public void run( String... args ) {
listData.addData( "spring" );
listData.addData( "event" );
listData.addData( "처리" );
listData.removeData( "처리" );
listData.removeData( "event" );
listData.removeData( "spring" );
}
실행 결과
listEventHandler called, ListChangeEvent(data=spring, message=added)
listEventHandler called, ListChangeEvent(data=spring, message=removed)
Spring 이벤트 시스템을 이용하여 실시간 데이터 변경을 감지하고 처리하는 방법에 대해서 정리해 보았다.
이벤트 기반 아키텍쳐는 모듈 간의 결합도를 낮추고 확장성을 높여 복잡한 비즈니스 로직을 효과적으로 관리할 수 있도록 도와준다.
특히 Spring의 이벤트 시스템을 사용하면 애플리케이션 내에서 발생하는 다양한 이벤트를 손쉽게 처리할 수 있다.
끝.