ApplicationEventPublisher
- Spring의 ApplicationContext가 상속하는 인터페이스 중 하나
- Design Pattern 중 하나인 옵저버 패턴의 구현체
이벤트를 발생하는 Publisher
와 이를 감시하는 Observer(or Subscriber)
사이의 결합도를 낮추면서 이벤트를 Observer
에게 전달하고 싶을 때 사용하게 된다.
사용
파일 업로드를 시도한다고 가정할 때, 성공과 에러를 구분해서 이벤트를 감지하고 결과를 간단한 RESTful API를 사용하여 내려주도록 해보자.
Event
import lombok.Builder;
import lombok.Getter;
import java.util.Map;
import java.util.UUID;
@Getter
@Builder
public class FileEvent {
private String eventId;
private String type;
private Map<String, Object> data;
public static FileEvent toCompleteEvent(Map data) {
return FileEvent.builder()
.eventId(UUID.randomUUID().toString())
.type(EventType.COMPLETED.name())
.data(data)
.build();
}
public static FileEvent toErrorEvent(Map data) {
return FileEvent.builder()
.eventId(UUID.randomUUID().toString())
.type(EventType.ERROR.name())
.data(data)
.build();
}
}
Spring 4.2 이후에는 Event에서 AppplicationEvent
를 상속받을 필요가 없어졌다. 4.2 이후의 이벤트 처리는 POJO Java 가 된다. 이러한 구현 방식은 소스 코드에 spring 코드가 들어가지 않도록 하는 비침투성 철학과 일치한다.
결과적으로 개발자는 의존성 같은 고려없이 더 편하게 테스트 할 수 있게하고 코드를 유지보수하기 쉽게 한다.
Publisher
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class FileEventPublisher {
private final ApplicationEventPublisher applicationEventPublisher;
public void notifyComplete(FileEvent fileEvent) {
applicationEventPublisher.publishEvent(fileEvent);
}
public void notifyError(FileEvent fileEvent) {
applicationEventPublisher.publishEvent(fileEvent);
}
}
ApplicationEventPublisher
의 publishEvent()
메소드를 호출해서 이벤트를 발생시킬 수 있다. publishEvent()
는 자바의 최상위 클래스인 Object를 받을 수 있으므로 파라미터로 전달하는 타입을 신경쓰지 않아도 된다.
Listener
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class FileEventListener {
@EventListener
public void onFileEventHandler(FileEvent fileEvent) {
log.info("fileEvent receive type : {} data : {}", fileEvent.getType(), fileEvent.getData());
}
}
FileEventListener
는 스프링이 누구에게 이벤트를 전달할지 알아야하기 때문에 빈으로 등록되어야 하고 @Component를 사용해서 등록한다.
@EventListener 어노테이션을 통해 Handler를 구현한다. 정해진 이벤트가 발생하게 되면 이 어노테이션이 달린 메소드가 실행되면서 로그가 찍히게 된다.
FileService
@Slf4j
@Service
@RequiredArgsConstructor
public class FileService {
private final FileEventPublisher fileEventPublisher;
public void fileUpload(Map<String, Object> data) {
try {
log.info("파일 복사 완료");
log.info("DB 파일 메타 정보 저장 완료");
FileEvent completeEvent = FileEvent.toCompleteEvent(data);
fileEventPublisher.notifyComplete(completeEvent);
} catch (Exception e) {
log.error("file upload fail", e);
FileEvent errorEvent = FileEvent.toErrorEvent(data);
fileEventPublisher.notifyError(errorEvent);
}
}
}
Controller
@RestController
@RequiredArgsConstructor
public class FileController {
private final FileService fileService;
@GetMapping("/upload/image")
public ResponseEntity fileUpload() {
Map<String, Object> data = new HashMap<>();
data.put("userId", "홍길동");
data.put("type", "webp");
data.put("fileSize", 5);
fileService.fileUpload(data);
return ResponseEntity.ok("success");
}
}
로그 확인
스프링을 실행시키고 @GetMapping 으로 명시해준 url 로 접속해보자.
이벤트가 정상적으로 발생하며 의도대로 동작하는 것을 알 수 있다.