Event Driven Development (EDD)
Event Driven Development 는 특정 이벤트(Event)가 발생했을 때 동작이 수행되는 개발 방식이다.
주로 비동기 프로세스가 필요한 시스템이나 실시간 데이터 처리가 중점인 시스템에서 적합하다.
Kafka 같은 Message Queue를 통해 어렵지 않게 구현할 수 있지만,
여기서는 단순히 Spring + Java + ApplicationEventPublisher 로만 간단하게 구현하였다.
(일단 개념만 이해하는 게 목표긴 함ㅋ)
위 그림에서 Producer2 없이 Producer1이 Consumer 1,2,3에게 모든 이벤트를 발생시키게 만들 것이다.
1. ApplicationEventPublisher
ApplicationContext의 기능 중 하나이다.
Configure 작업을 통해 빈으로 만들고 할 필요도 없이, 스프링이 뜨면 알아서 빈으로 등록한다.
interface이고 별도로 구현해서 공통로직이나 추가로직을 구현할 수도 있다.
(Log 붙이거나, 공통 에러핸들링 로직 같은 것 붙이면 될 듯)
2. Observer Pattern과의 비교
무언가가 발생했을 때 특정 로직이 실행되는 방식인 것은 비슷하다.
Observer Pattern | 객체의 상태 변화 -> 옵저버에게 통지 |
Event Driven Development | 이벤트 발생 -> 리스너가 감지 |
또한, 단순하게 발생->처리 구조라 이벤트를 발생시키는 쪽이 처리가 어떻게 흘러가는지 몰라도 되는 것도 공통점이라면 공통점이다.
그러면 뭐가 다르냐,
Observer Pattern | Event Driven Development |
설계 패턴 | 아키텍처 패턴 |
동기 / 비동기 | 비동기 |
객체에 옵저버가 등록됨, 강한 결합 | 발생하는 쪽과 처리하는 쪽이 멀찍이(?) 연결됨, 약한 결합 |
예시) 버튼 클릭 이벤트를 버튼에 붙임 | 예시) 주문이 생성되면 결재, 재고, 알림 이벤트가 독립적으로 비즈니스를 처리 |
3. 만들어보자
대충 시나리오는 어떠한 API가 있고 해당 API로 요청을 받으면 메일과 푸시를 요청한다는 그런 시나리오다.
3-1 Dto들
public record CommentDto(
String comment,
String noticeId,
String registerUserId
) {}
public class CommentEvent extends ApplicationEvent {
public CommentEvent(Object source) {
super(source);
}
public Object getSource() { return super.getSource(); }
}
public class CommentInsertEvent extends ApplicationEvent {
public CommentInsertEvent(Object source) {
super(source);
}
public Object getSource() { return super.getSource(); }
}
public class SendMailEvent extends ApplicationEvent {
public SendMailEvent(Object source) {
super(source);
}
public Object getSource() { return super.getSource(); }
}
public class SendPushEvent extends ApplicationEvent {
public SendPushEvent(Object source) {
super(source);
}
public Object getSource() { return super.getSource(); }
}
3-2 Consumer 들
@Component
public class CommentInsertConsumer {
@Async // 비동기 실행
@EventListener
public void consumeEvent(CommentInsertEvent event) throws Exception {
Thread.sleep(7000);
System.out.println("DB에 넣었음 : " + event.getSource().toString());
}
}
@Component
public class SendMailConsumer {
@Async // 비동기 실행
@EventListener
public void consumeEvent(SendMailEvent event) throws Exception {
Thread.sleep(5000);
System.out.println("메일 보냈음 : " + event.getSource().toString());
}
}
@Component
public class SendPushConsumer {
@Async // 비동기 실행
@EventListener
public void consumeEvent(SendPushEvent event) throws Exception {
Thread.sleep(6000);
System.out.println("푸시 보냈음 : " + event.getSource().toString());
}
}
컨슈머들 전체가 비동기로 돈다고 가정해서 @Async를 붙였다.
@Async는 Applicaiotn.java에 @EnableAsync 달아줘야 작동하니 까먹지 말자.
나는 자주 까먹음 사실
3-3. EventRouter
@Component
@RequiredArgsConstructor
public class EventRouter {
private final ApplicationEventPublisher applicationEventPublisher;
private final ObjectMapper objectMapper;
@Async // 비동기 실행
@EventListener
public void handleEvent(ApplicationEvent event) throws Exception {
if (event instanceof CommentEvent commentInsertEvent) {
CommentDto commentDto = objectMapper.readValue(commentInsertEvent.getSource().toString(), CommentDto.class);
applicationEventPublisher.publishEvent(new SendMailEvent(commentDto.registerUserId()));
applicationEventPublisher.publishEvent(new SendPushEvent(commentDto.registerUserId()));
applicationEventPublisher.publishEvent(new CommentInsertEvent(commentDto));
} //...
}
}
여기도 @Async 붙여서 비동기로 돈다.
handleEvent 메소드를 보면 알겠지만
단순하게 if 문에다 else if 문 추가해서 다른 이벤트에 대한 것들을 추가로 확장하는게 쉽다.
3-4. ProducerController
@RestController
@RequiredArgsConstructor
public class ProducerController {
private final ApplicationEventPublisher eventPublisher;
private final ObjectMapper objectMapper;
@PostMapping("/event")
public ResponseEntity<String> createComment(@RequestBody CommentDto commentDto) throws Exception {
CommentEvent event = new CommentEvent(objectMapper.writeValueAsString(commentDto));
eventPublisher.publishEvent(event);
return ResponseEntity.ok().build();
}
}
필드 주입말고 생성자 주입을 생활화 하자
comment create에 필요한 데이터클래스를 json으로 만들어 commentEvent를 발생 시킨다.
비동기이기 때문에 당연히 리턴값은 상태값만 ㅇㅇ
4. 작동해보자
요로코롬 만들어서 날려주면
비동기라서 101밀리초만에 응답은 왔다. 그러나
컨슈머에서 Thread.sleep 넣은대로 5, 6, 7초만에 해당 메세지가 잘 출력된다.