====== Guava Event Bus ====== * 매우 간단하고 쉬운 [[java:eventbus|Java Event Bus]] 구현체 * [[:springframework|Spring Framework]] 사용시에는 [[springframework:transaction:transactional_event_listener|TransactionalEventListener]] 를 우선할 것. * [[https://github.com/google/guava/wiki/EventBusExplained|EventBusExplained]] * [[http://www.baeldung.com/guava-eventbus|Guide to Guava's EventBus]] * [[http://knight76.tistory.com/entry/Guava-EventBus-%EC%98%88%EC%A0%9C|[Guava] EventBus 예제]] * [[https://www.slideshare.net/koneru9999/guavas-event-bus|Guava’s Event Bus]] ===== 왜 필요한가 ===== * 개발을 하다보면 본질적인 비즈니스 로직(예: 사용자 가입. 사용자정보 검증 및 DB 저장)과 비본질적인 비즈니스에 대한 후처리 로직(예: 가입 축하 Email발송, 통계 서비스에 가입자 통지 등)이 강하게 결합(Tight Coupling)하는 경우가 발생한다. * 실제 비즈니스 로직과 그 후처리 로직을 완전히 분리하여 비즈니스 로직을 간결하게 유지하며 코드 유지보수성을 높인다. * 비즈니스 로직과 후처리 로직이 섞여 있으면 후처리 로직에서 발생하는 예외 등으로 인해 비즈니스 로직이 영향을 받을 수 있는데 Event Bus를 사용하면 서로간의 영향을 분리할 수 있다. * 비즈니스에 직접적인 관련이 없는 코드를 추가할 일이 있을 때마다 비즈니스 코드 자체를 수정하는 것은 [[https://ko.wikipedia.org/wiki/%EA%B0%9C%EB%B0%A9-%ED%8F%90%EC%87%84_%EC%9B%90%EC%B9%99|개방 폐쇄 원칙]]에 어긋나고 유지보수성도 떨어진다. 비본질적 코드는 본질적코드에서 알 필요가 없게 분리하는 것이 좋다. * 비즈니스 코드의 마지막에서 Event를 발생시키고, 비본질적인 후처리 로직들을 Event Listener로 만들어 처리하도록 한다. ===== 정리 ===== * [[https://google.github.io/guava/releases/22.0/api/docs/com/google/common/eventbus/EventBus.html|EventBus]] : 동기식 처리. * 이벤트를 발생(''post'')시킨 것과 동일한 쓰레드에서 Subscriber들이 이벤트를 처리한다. * 이 경우 **이벤트를 발생시킨 쓰레드는 모든 이벤트 Subscriber가 일을 마칠 때까지 Blocking** 된다. * [[https://google.github.io/guava/releases/22.0/api/docs/com/google/common/eventbus/AsyncEventBus.html|AsyncEventBus]] : 비동기 처리 * 이벤트를 발생시킨 쓰레드가 지정된 쓰레드풀의 **별도 쓰레드**에서 처리 * ''@Subscribe'' 애노테이션이 붙은 메소드는 * 첫번째 인자값으로 된 이벤트를 받아서 처리한다. 이 메소드는 **무조건 하나의 파라미터**만 가지고 있어야 한다. * ''public void methodName(Param onlyOneParam)'' 여야한다. * 하나의 Listener 클래스에는 여러개의 ''@Subscribe'' 메소드가 있어도 된다. * 간단한 동기 / 비동기 이벤트 예제 : [[https://github.com/kwon37xi/research-java8/blob/master/guava-examples/src/main/java/guava/eventbus/EventBusBasicExample.java|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(); } } } 결과.. ''EventBus''는 ''main'' 쓰레드에서 ''AsyncEventBus''는 쓰레드 풀(''ExecutorService'')에서 subscribe 메소드 처리 ===== @AllowConcurrentEvents ===== * 기본적으로 ''@Subscribe'' 메소드는 **멀티 쓰레드에서 동시 호출이 막혀있다**하다. (''synchronized''로 감싸져서 호출됨. 단, EventBus를 통하지 않고 별도의 코드로 직접 호출할 때 제외) * ''@Subscribe'' 메소드에 [[https://google.github.io/guava/releases/22.0/api/docs/com/google/common/eventbus/AllowConcurrentEvents.html|@AllowConcurrentEvents]] 애노테이션을 붙여주면 여러 쓰레드에서 동시에 접근 가능해진다. * 멀티 쓰레드 환경(Web 환경 등)에서는 **특별한 사유가 없다면 ''@Subscribe'' 메소드는 모두 Thread-Safe 하게 작성하고 ''@AllowConcurrentEvents'' 어노테이션을 붙여줘야** 성능 저하없이 사용가능하다. * [[https://github.com/kwon37xi/research-java8/blob/master/guava-examples/src/main/java/guava/eventbus/EventBusAllowConcurrentEventsExample.java|EventBusAllowConcurrentEventsExample.java]] 예제. * ''@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 에서는 그냥 사용하지 말고, [[springframework:transaction:transactional_event_listener|TransactionalEventListener]] 사용하는게 맞다. * 따라서 EventBus 선언과 ''register'' 코드를 한 군데에 모아서 관리 편의성을 꾀하는게 좋아 보인다. * 특별한 이유가 없다면 ''@Subscribe'' 메소드는 **항상 Thread-Safe 하게 작성하고 ''@AllowConcurrentEvents''**를 붙인다. * ''@Subscribe'' 메소드가 **primitive type**을 받아서는 안 된다. 항상 객체를 받아야 한다. 또한 파라미터는 항상 **한 개**여야 한다. * subscribe 메소드에서 예외를 발생시키지 말도록 한다. EventBus가 예외를 로깅해주긴 하지만 그것에 의존하지 말 것. * ''EventBus'' 객체를 Singleton으로 한개만 생성해서 여러 이벤트들이 사용해도 되는가? -> 그래도 된다. 이벤트별로 EventBus 객체를 분리하건, 단 한개의 통합 EventBus를 만들건 이벤트는 subscriber method의 파라미터에 의해 처리자가 결정되므로 작동에는 문제없다. 개발자가 적당히 선택해서 사용한다.