사용자 도구

사이트 도구


java:guava:eventbus

Guava Event Bus

왜 필요한가

  • 개발을 하다보면 본질적인 비즈니스 로직(예: 사용자 가입. 사용자정보 검증 및 DB 저장)과 비본질적인 비즈니스에 대한 후처리 로직(예: 가입 축하 Email발송, 통계 서비스에 가입자 통지 등)이 강하게 결합(Tight Coupling)하는 경우가 발생한다.
  • 실제 비즈니스 로직과 그 후처리 로직을 완전히 분리하여 비즈니스 로직을 간결하게 유지하며 코드 유지보수성을 높인다.
  • 비즈니스 로직과 후처리 로직이 섞여 있으면 후처리 로직에서 발생하는 예외 등으로 인해 비즈니스 로직이 영향을 받을 수 있는데 Event Bus를 사용하면 서로간의 영향을 분리할 수 있다.
  • 비즈니스에 직접적인 관련이 없는 코드를 추가할 일이 있을 때마다 비즈니스 코드 자체를 수정하는 것은 개방 폐쇄 원칙에 어긋나고 유지보수성도 떨어진다. 비본질적 코드는 본질적코드에서 알 필요가 없게 분리하는 것이 좋다.
  • 비즈니스 코드의 마지막에서 Event를 발생시키고, 비본질적인 후처리 로직들을 Event Listener로 만들어 처리하도록 한다.

정리

  • EventBus : 동기식 처리.
    • 이벤트를 발생(post)시킨 것과 동일한 쓰레드에서 Subscriber들이 이벤트를 처리한다.
    • 이 경우 이벤트를 발생시킨 쓰레드는 모든 이벤트 Subscriber가 일을 마칠 때까지 Blocking 된다.
  • AsyncEventBus : 비동기 처리
    • 이벤트를 발생시킨 쓰레드가 지정된 쓰레드풀의 별도 쓰레드에서 처리
  • @Subscribe 애노테이션이 붙은 메소드는
    • 첫번째 인자값으로 된 이벤트를 받아서 처리한다. 이 메소드는 무조건 하나의 파라미터만 가지고 있어야 한다.
    • public void methodName(Param onlyOneParam) 여야한다.
  • 하나의 Listener 클래스에는 여러개의 @Subscribe 메소드가 있어도 된다.
  • 간단한 동기 / 비동기 이벤트 예제 : EventBusBasicExample.java
public class EventBusBasicExample {
    private static final Logger log = LoggerFactory.getLogger(EventBusBasicExample.class);
 
    public static class MessageListener {
        @Subscribe
        public void receive(String message) throws InterruptedException {
            log.info("Receiving message... {}", message);
            receiving(3, message);
            log.info("Done");
        }
 
        private static void receiving(int count, String message) throws InterruptedException {
            for (int i = 0; i < count; i++) {
                log.info("... {}", message);
                TimeUnit.SECONDS.sleep(1L);
            }
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        log.info("### Starting Event Bus ###");
        EventBus eventBus = new EventBus("sync");
        ExecutorService executor = Executors.newFixedThreadPool(5);
        AsyncEventBus asyncEventBus = new AsyncEventBus("async", executor);
 
        asyncEventBus.register(new MessageListener());
        eventBus.register(new MessageListener());
 
        asyncEventBus.post("ASYNC event"); // eventBus.post를 먼저 실행하면 블로킹됨.
        eventBus.post("SYNC event");
 
        log.info("### End ###");
 
        if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
            executor.shutdown();
        }
 
    }
}

결과.. EventBusmain 쓰레드에서 AsyncEventBus는 쓰레드 풀(ExecutorService)에서 subscribe 메소드 처리

@AllowConcurrentEvents

  • 기본적으로 @Subscribe 메소드는 멀티 쓰레드에서 동시 호출이 막혀있다하다. (synchronized로 감싸져서 호출됨. 단, EventBus를 통하지 않고 별도의 코드로 직접 호출할 때 제외)
  • @Subscribe 메소드에 @AllowConcurrentEvents 애노테이션을 붙여주면 여러 쓰레드에서 동시에 접근 가능해진다.
  • 멀티 쓰레드 환경(Web 환경 등)에서는 특별한 사유가 없다면 @Subscribe 메소드는 모두 Thread-Safe 하게 작성하고 @AllowConcurrentEvents 어노테이션을 붙여줘야 성능 저하없이 사용가능하다.
    • @AllowConcurrentEvents 이 존재하는 상태로 실행하면 receive메소드가 멀티 쓰레드에서 동시에 실행된다. 따라서 로그가 여러쓰레드에서 뒤죽박죽 남는다.
    • @AllowConcurrentEvents를 주석 처리하면 receive 메소드가 특정 쓰레드에서만 점유되어 순차적으로 실행된다. 로그가 한 쓰레드씩 순차적으로 남는다.

DeadEvent 받기

  • 어떠한 subscriber도 없는 죽은 이벤트 받기
@Subscribe
public void handleDeadEvent(DeadEvent deadEvent) {
    // do something
}

주의할 점

  • Guava EventBus 는 메시지 객체의 Type 검사를 하지 않기 때문에 쉽게 잘못된 객체를 메시지로 던지는 실수를 할 수 있다. Type Safety를 보장할 수 있도록 EventBus를 감싸서 사용하는 방법을 취하는 것이 좋아보인다.
  • 트랜잭션이 연결된 쓰레드에서 이벤트가 발생할 경우, 만약 이벤트 버스에서 오류가 발생하면 해당 트랜잭션 전체가 롤백될 수 있다.
  • Spring 연동시 Listener의 @PostConstruct로 등록하기 보다는 EventBus 객체 생성하는 설정 코드에서 Listener Bean을 생성해 바로 등록하는게 좋을 것 같다. @PostConstruct로 등록시 어떤 것들이 등록돼 있는지 코드를 추적해 확인하기 어렵다.
    • Spring 에서는 그냥 사용하지 말고, TransactionalEventListener 사용하는게 맞다.
    • 따라서 EventBus 선언과 register 코드를 한 군데에 모아서 관리 편의성을 꾀하는게 좋아 보인다.
  • 특별한 이유가 없다면 @Subscribe 메소드는 항상 Thread-Safe 하게 작성하고 @AllowConcurrentEvents를 붙인다.
  • @Subscribe 메소드가 primitive type을 받아서는 안 된다. 항상 객체를 받아야 한다. 또한 파라미터는 항상 한 개여야 한다.
  • subscribe 메소드에서 예외를 발생시키지 말도록 한다. EventBus가 예외를 로깅해주긴 하지만 그것에 의존하지 말 것.
  • EventBus 객체를 Singleton으로 한개만 생성해서 여러 이벤트들이 사용해도 되는가? → 그래도 된다. 이벤트별로 EventBus 객체를 분리하건, 단 한개의 통합 EventBus를 만들건 이벤트는 subscriber method의 파라미터에 의해 처리자가 결정되므로 작동에는 문제없다. 개발자가 적당히 선택해서 사용한다.
java/guava/eventbus.txt · 마지막으로 수정됨: 2021/10/28 16:39 저자 kwon37xi