이 연재글은 Spring Domain Event 발행 실습의 1번째 글입니다.

이번 장에서는 SpringBoot framework 내에서 이벤트를 발행하고 사용하는 방법에 대해서 살펴보겠습니다.

SpringBoot Events

이벤트는 스프링 프레임워크에서 간과되는 기능 중 하나이지만 유용한 기능입니다. 이벤트 발행(event publishing)은 ApplicationContext가 제공하는 기능 중 하나로 이미 표준화된 방법을 제공합니다.

  • Event는 ApplicationEvent를 확장해서 사용합니다.
  • 이벤트 게시자(Event Publisher)는 ApplicationEventPublisher에 이벤트를 발행합니다.
  • 이벤트를 소비하는 리스너(Event Listener)는 ApplicationListener 인터페이스를 구현하여 이벤트를 받습니다.

Project 생성

https://start.spring.io/에서 프로젝트를 하나 생성합니다.

build.gradle

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
plugins {
id 'org.springframework.boot' version '2.4.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.daddyprogrammer'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
plugins { id 'org.springframework.boot' version '2.4.3' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'com.daddyprogrammer' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } test { useJUnitPlatform() }
plugins {
	id 'org.springframework.boot' version '2.4.3'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.daddyprogrammer'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
	useJUnitPlatform()
}

Event 생성

ApplicationEvent를 확장한 이벤트를 아래와 같이 하나 생성합니다.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
package com.daddyprogrammer.springevents.event;
import org.springframework.context.ApplicationEvent;
public class CustomEvent extends ApplicationEvent {
private String message;
public CustomEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
}
package com.daddyprogrammer.springevents.event; import org.springframework.context.ApplicationEvent; public class CustomEvent extends ApplicationEvent { private String message; public CustomEvent(Object source, String message) { super(source); this.message = message; } public String getMessage() { return message; } }
package com.daddyprogrammer.springevents.event;

import org.springframework.context.ApplicationEvent;

public class CustomEvent extends ApplicationEvent {
    private String message;

    public CustomEvent(Object source, String message) {
        super(source);
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

Event Publisher 생성

위에서 작성한 이벤트를 발행할 수 있는 게시자를 아래와 같이 생성합니다. publish 메서드에서 ApplicationEventPublisher로 이벤트를 발행합니다.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
package com.daddyprogrammer.springevents.event;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@Component
public class CustomEventPublisher {
private final ApplicationEventPublisher applicationEventPublisher;
public void publish(final String message) {
System.out.println("Publishing custom event. ");
CustomEvent customSpringEvent = new CustomEvent(this, message);
applicationEventPublisher.publishEvent(customSpringEvent);
}
}
package com.daddyprogrammer.springevents.event; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; @RequiredArgsConstructor @Component public class CustomEventPublisher { private final ApplicationEventPublisher applicationEventPublisher; public void publish(final String message) { System.out.println("Publishing custom event. "); CustomEvent customSpringEvent = new CustomEvent(this, message); applicationEventPublisher.publishEvent(customSpringEvent); } }
package com.daddyprogrammer.springevents.event;

import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class CustomEventPublisher {
    private final ApplicationEventPublisher applicationEventPublisher;

    public void publish(final String message) {
        System.out.println("Publishing custom event. ");
        CustomEvent customSpringEvent = new CustomEvent(this, message);
        applicationEventPublisher.publishEvent(customSpringEvent);
    }
}

Event Listener 생성

ApplicationEventPublisher에 발행된 이벤트는 ApplicationListener를 확장하여 구현할 수 있습니다.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
package com.daddyprogrammer.springevents.event;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class CustomEventListener implements ApplicationListener<CustomEvent> {
@Override
public void onApplicationEvent(CustomEvent event) {
System.out.println("Received spring custom event - " + event.getMessage());
}
}
package com.daddyprogrammer.springevents.event; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; @Component public class CustomEventListener implements ApplicationListener<CustomEvent> { @Override public void onApplicationEvent(CustomEvent event) { System.out.println("Received spring custom event - " + event.getMessage()); } }
package com.daddyprogrammer.springevents.event;

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class CustomEventListener implements ApplicationListener<CustomEvent> {
    @Override
    public void onApplicationEvent(CustomEvent event) {
        System.out.println("Received spring custom event - " + event.getMessage());
    }
}

이벤트 발행 및 소비 테스트

아래와 같이 Controller를 하나 만들어 message가 발행되고 소비되는지 확인해볼 수 있습니다.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
package com.daddyprogrammer.springevents.controller;
import com.daddyprogrammer.springevents.event.CustomEventPublisher;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
@RestController
public class EventController {
private final CustomEventPublisher customEventPublisher;
@GetMapping("/event")
public String event(@RequestParam String message) {
customEventPublisher.publish(message);
return "finished";
}
}
package com.daddyprogrammer.springevents.controller; import com.daddyprogrammer.springevents.event.CustomEventPublisher; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @RestController public class EventController { private final CustomEventPublisher customEventPublisher; @GetMapping("/event") public String event(@RequestParam String message) { customEventPublisher.publish(message); return "finished"; } }
package com.daddyprogrammer.springevents.controller;

import com.daddyprogrammer.springevents.event.CustomEventPublisher;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class EventController {

    private final CustomEventPublisher customEventPublisher;

    @GetMapping("/event")
    public String event(@RequestParam String message) {
        customEventPublisher.publish(message);
        return "finished";
    }
}

웹서버를 실행하고 브라우저에서 메시지를 발송하면 다음과 같이 발행된 메시지가 소비되는 것을 확인할 수 있습니다.

localhost:8080/event?message=hello

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Publishing custom event.
Received spring custom event - hello
Publishing custom event. Received spring custom event - hello
Publishing custom event. 
Received spring custom event - hello

비동기로 이벤트 실행하기

만약 이벤트를 처리하는데 오래 걸리면 응답이 지연되기 때문에 사용자가 오래 기다려야 하며 이런 상황에서는 이벤트를 비동기로 처리하는 것이 좋습니다.
예) 회원가입 후 가입 완료 메일 발송을 비동기 이벤트로 처리하면 회원이 메일 발송이 완료될 때까지 응답을 기다리지 않아도 됩니다.

테스트를 위해 다음과 같이 이벤트 처리 시 지연을 발생시킵니다.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@Component
public class CustomEventListener implements ApplicationListener<CustomEvent> {
@Override
public void onApplicationEvent(CustomEvent event) {
try {
Thread.sleep(3000);
System.out.println("Received spring custom event - " + event.getMessage());
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Component public class CustomEventListener implements ApplicationListener<CustomEvent> { @Override public void onApplicationEvent(CustomEvent event) { try { Thread.sleep(3000); System.out.println("Received spring custom event - " + event.getMessage()); } catch (Exception e) { e.printStackTrace(); } } }
@Component
public class CustomEventListener implements ApplicationListener<CustomEvent> {
    @Override
    public void onApplicationEvent(CustomEvent event) {
        try {
            Thread.sleep(3000);
            System.out.println("Received spring custom event - " + event.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

다시 서버를 실행하고 요청을 보내면 3초 후에 응답이 오는 것을 확인할 수 있습니다.

Async 설정 추가

다음과 같이 이벤트가 비동기로 실행될 수 있도록 설정을 추가합니다. 설정 추가 후에 다시 요청을 보내면 응답은 바로 오지만 이벤트는 백그라운드에서 따로 처리되는 것을 확인할 수 있습니다.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@Configuration
public class AsyncEventConfig {
@Bean(name = "applicationEventMulticaster")
public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
SimpleApplicationEventMulticaster eventMulticaster =
new SimpleApplicationEventMulticaster();
eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
return eventMulticaster;
}
}
@Configuration public class AsyncEventConfig { @Bean(name = "applicationEventMulticaster") public ApplicationEventMulticaster simpleApplicationEventMulticaster() { SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster(); eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor()); return eventMulticaster; } }
@Configuration
public class AsyncEventConfig {
    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
        SimpleApplicationEventMulticaster eventMulticaster =
                new SimpleApplicationEventMulticaster();

        eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return eventMulticaster;
    }
}

Annotation 기반 EventListener

리스너 구현을 위해 일일이 ApplicationListener를 구현할 필요 없이 @EventListener 어노테이션을 통해 관리 빈의 모든 공용 메서드에 리스너를 등록할 수 있습니다. 위에서 작성한 CustomEventListener는 다음과 같이 대체할 수 있습니다.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
package com.daddyprogrammer.springevents.event;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class AnnotaionListener {
@EventListener
public void handleEvent(CustomEvent event) {
System.out.println("Received spring custom event by annotation listener - " + event.getMessage());
}
}
package com.daddyprogrammer.springevents.event; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @Component public class AnnotaionListener { @EventListener public void handleEvent(CustomEvent event) { System.out.println("Received spring custom event by annotation listener - " + event.getMessage()); } }
package com.daddyprogrammer.springevents.event;

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class AnnotaionListener {
    @EventListener
    public void handleEvent(CustomEvent event) {
       System.out.println("Received spring custom event by annotation listener - " + event.getMessage());
    }
}

Annotation 기반 비동기 처리

위에서 설정한 전역 비동기 처리도 AsyncEventConfig를 등록하지 않고 @Async를 이용하여 처리할 수 있습니다. @Async를 활성화하려면 Application에 @EnableAsync를 추가하면 됩니다.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@EnableAsync
@SpringBootApplication
public class SpringEventsApplication {
public static void main(String[] args) {
SpringApplication.run(SpringEventsApplication.class, args);
}
}
@EnableAsync @SpringBootApplication public class SpringEventsApplication { public static void main(String[] args) { SpringApplication.run(SpringEventsApplication.class, args); } }
@EnableAsync
@SpringBootApplication
public class SpringEventsApplication {

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

}

이제 EventListener에 @Async를 적용하면 비동기로 이벤트가 처리됩니다.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@Component
public class AnnotaionListener {
@Async
@EventListener
public void handleEvent(CustomEvent event) {
try {
Thread.sleep(3000);
System.out.println("Received spring custom event by annotation listener - " + event.getMessage());
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Component public class AnnotaionListener { @Async @EventListener public void handleEvent(CustomEvent event) { try { Thread.sleep(3000); System.out.println("Received spring custom event by annotation listener - " + event.getMessage()); } catch (Exception e) { e.printStackTrace(); } } }
@Component
public class AnnotaionListener {
    @Async
    @EventListener
    public void handleEvent(CustomEvent event) {
        try {
            Thread.sleep(3000);
            System.out.println("Received spring custom event by annotation listener - " + event.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Generic Event

이벤트 형태를 Generic으로 선언하여 다양한 형태의 이벤트를 처리할 수 있습니다. 또한 Spring Expression Language (SpEL)을 사용하여 특정한 상태의 이벤트만 처리할 수도 있습니다.

GenericEvent 생성

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
package com.daddyprogrammer.springevents.event;
import lombok.Getter;
@Getter
public class GenericEvent <T> {
private T result;
protected boolean success;
public GenericEvent(T result, boolean success) {
this.result = result;
this.success = success;
}
}
package com.daddyprogrammer.springevents.event; import lombok.Getter; @Getter public class GenericEvent <T> { private T result; protected boolean success; public GenericEvent(T result, boolean success) { this.result = result; this.success = success; } }
package com.daddyprogrammer.springevents.event;

import lombok.Getter;

@Getter
public class GenericEvent <T> {
    private T result;
    protected boolean success;

    public GenericEvent(T result, boolean success) {
        this.result = result;
        this.success = success;
    }
}

GenericEventPublisher 생성

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
package com.daddyprogrammer.springevents.event;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@Component
public class GenericEventPublisher<T> {
private final ApplicationEventPublisher applicationEventPublisher;
public void publish(final T message, final boolean success) {
System.out.println("Publishing generic event. ");
GenericEvent<T> genericEvent = new GenericEvent<>(message, success);
applicationEventPublisher.publishEvent(genericEvent);
}
}
package com.daddyprogrammer.springevents.event; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; @RequiredArgsConstructor @Component public class GenericEventPublisher<T> { private final ApplicationEventPublisher applicationEventPublisher; public void publish(final T message, final boolean success) { System.out.println("Publishing generic event. "); GenericEvent<T> genericEvent = new GenericEvent<>(message, success); applicationEventPublisher.publishEvent(genericEvent); } }
package com.daddyprogrammer.springevents.event;

import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class GenericEventPublisher<T> {
    private final ApplicationEventPublisher applicationEventPublisher;

    public void publish(final T message, final boolean success) {
        System.out.println("Publishing generic event. ");
        GenericEvent<T> genericEvent = new GenericEvent<>(message, success);
        applicationEventPublisher.publishEvent(genericEvent);
    }
}

GenericEventListener 생성

listener에 조건(condition)을 설정하여 event.success == true일 때만 처리할 수 있습니다.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
package com.daddyprogrammer.springevents.event;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class GenericEventListener {
@EventListener(condition = "#event.success")
public void handleEvent(GenericEvent event) {
System.out.println("Received spring generic event by annotation listener - " + event.getResult());
}
}
package com.daddyprogrammer.springevents.event; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @Component public class GenericEventListener { @EventListener(condition = "#event.success") public void handleEvent(GenericEvent event) { System.out.println("Received spring generic event by annotation listener - " + event.getResult()); } }
package com.daddyprogrammer.springevents.event;

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class GenericEventListener {
    @EventListener(condition = "#event.success")
    public void handleEvent(GenericEvent event) {
        System.out.println("Received spring generic event by annotation listener - " + event.getResult());
    }
}

EventController에 generic event 테스트를 위한 endpoint 추가

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@RequiredArgsConstructor
@RestController
public class EventController {
private final CustomEventPublisher customEventPublisher;
private final GenericEventPublisher<String> genericEventPublisher;
@GetMapping("/event")
public String event(@RequestParam String message) {
customEventPublisher.publish(message);
return "finished";
}
@GetMapping("/event/generic")
public String event(@RequestParam String message, @RequestParam boolean success) {
genericEventPublisher.publish(message, success);
return "finished";
}
}
@RequiredArgsConstructor @RestController public class EventController { private final CustomEventPublisher customEventPublisher; private final GenericEventPublisher<String> genericEventPublisher; @GetMapping("/event") public String event(@RequestParam String message) { customEventPublisher.publish(message); return "finished"; } @GetMapping("/event/generic") public String event(@RequestParam String message, @RequestParam boolean success) { genericEventPublisher.publish(message, success); return "finished"; } }
@RequiredArgsConstructor
@RestController
public class EventController {

    private final CustomEventPublisher customEventPublisher;
    private final GenericEventPublisher<String> genericEventPublisher;

    @GetMapping("/event")
    public String event(@RequestParam String message) {
        customEventPublisher.publish(message);
        return "finished";
    }

    @GetMapping("/event/generic")
    public String event(@RequestParam String message, @RequestParam boolean success) {
        genericEventPublisher.publish(message, success);
        return "finished";
    }
}

서버를 실행하고 요청을 보내면 success 변수의 값이 따라 이벤트가 처리되거나 처리되지 않는 것을 확인할 수 있습니다.

http://localhost:8080/event/generic?message=hello&success=true

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Publishing generic event.
Received spring generic event by annotation listener - hello
Publishing generic event. Received spring generic event by annotation listener - hello
Publishing generic event. 
Received spring generic event by annotation listener - hello


http://localhost:8080/event/generic?message=hello&success=false

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Publishing generic event.
Publishing generic event.
Publishing generic event. 

Transaction 바운더리에서의 이벤트 처리

@EventListener의 확장인 @TransactionalEventListener를 사용하면 트랜잭션 상태에 따른 이벤트 처리가 가능합니다. phase를 설정하지 않으면 트랜잭션이 성공적으로 완료되었을 때 이벤트가 실행됩니다.

  • AFTER_COMMIT (default) – 트랜잭션이 성공했을때 실행
  • AFTER_ROLLBACK – 트랜잭션이 롤백되었을때 실행
  • AFTER_COMPLETION – 트랜잭션이 완료되었을때 실행 (AFTER_COMMIT 및 AFTER_ROLLBACK이 완료되었을때)
  • BEFORE_COMMIT – 트랜잭션이 commit 되기 전에 실행

최신 소스는 GitHub 사이트를 참고해 주세요.

https://github.com/codej99/spring-event


연재글 이동
[다음글] SpringBoot @DomainEvent, AbstractAggregateRoot 이용한 Domain Event 발행