이 연재글은 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

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를 확장한 이벤트를 아래와 같이 하나 생성합니다.

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로 이벤트를 발행합니다.

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를 확장하여 구현할 수 있습니다.

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가 발행되고 소비되는지 확인해볼 수 있습니다.

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

Publishing custom event. 
Received spring custom event - hello

비동기로 이벤트 실행하기

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

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

@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 설정 추가

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

@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는 다음과 같이 대체할 수 있습니다.

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를 추가하면 됩니다.

@EnableAsync
@SpringBootApplication
public class SpringEventsApplication {

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

}

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

@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 생성

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 생성

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일 때만 처리할 수 있습니다.

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 추가

@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

Publishing generic event. 
Received spring generic event by annotation listener - hello


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

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 발행