본문 바로가기

3.구현/Java or Kotlin

[java] spring에서 event 사용하기

들어가기

스프링에서 이벤트 처리하는 방법을 알아보자. 일반적으로 스프링에서 이벤트 처리하는 경우는 많지가 않다. 대부분 빈객체의 메소드를 직접적으로 호출하여 처리한다. 대부분 직관적이고 코드도 명확하기 때문에 많이 사용한다. 이는 스프링 세션을 처리하는 내부에서도 사용되고 있다.
이벤트 처리는 대부분 비동기적으로 처리되어 추적하거나 분석하기 쉽지 않다. 그래도 이벤트 처리로 서로 간에 종속성을 끊어주고, 동기적 처리로 인한 작업 지연이 최소화된다. 이벤트로 처리할 경우에 장점이 많기 때문에 알아두면 좋다.

작성자: http://ospace.tistory.com, (ospace114@empal.com)

기본 이벤트 정의

이벤트 처리하는 로직을 들어가기 전에 기본적인 이벤트 정의를 보자. 스프링에서는 이벤트의 기본 클래스로 ApplicationEvent를 제공한다.

ApplilcationEvent 정의를 살펴보면 다음과 같다.

public abstract class ApplicationEvent extends EventObject {
    private static final long serialVersionUID = 7099057708183571937L;
    private final long timestamp;

    public ApplicationEvent(Object source) {
        super(source);
        this.timestamp = System.currentTimeMillis();
    }

    public final long getTimestamp() {
        return this.timestamp;
    }
}

클래스 자체는 구조가 복잡하지 않기에 이해하는데 크게 어렵지 않다. 자세히 보면 ApplicationEvent가 abstract로 되어 있다. 즉, ApplicationEvent을 직접 객체로 만들어서 사용할 수 없고 새로운 이벤트 클래스로 상속해서 사용해야 한다.

좀더 자세히 보면 부모 클래스가 EventObject이다. 이 클래스는 자바의 기본 제공 클래스이다. 이 구조를 클래스 다이어 그램으로 표현하면 다음과 같다.

Fig 01. AppplicationEvent Class

EventObject의 구조는 단순하다. 이벤트 발생한 소스를 저장하고 있다. 당연하겠지만 소스 객체를 null로 설정하면 안된다. 그리고, ApplicationEvent에서 추가로 이벤트가 발생한 시점을 갖고 있다.

여기서는 이벤트 처리를 위한 이벤트 객체를 FooBeginEvent와 FooEndEvent를 정의해보자. 각각 무엇인가의 시작과 끝을 알려주기 위해서 임시로 만들었다.

public class FooBeginEvent extends ApplicationEvent {
    public FooBeginEvent(Object source) {
        super(source);
    }
}
public class FooEndEvent extends ApplicationEvent {
    public FooBeginEvent(Object source) {
        super(source);
    }
}

혹시나 해서 언급하는데 위의 클래스가 같이 있다고 하나의 파일로 작성하면 안된다. 분리해서 작성해야 한다. ㅡ.ㅡ;;;

이벤트 클래스까지 정의했으니 이제 이벤트 처리 구조를 살펴보자.

스프링의 이벤트 처리 구조

전체적인 이벤트 처리구조를 클래스 다이어그램으로 표현하면 다음과 같다.

Fig 02. 이벤트 처리 구조

스프링의 이벤트 처리에서 핵심은 ApplicationEventPublisherAware, ApplicationListener, ApplicationEventPublisher이다. 각각의 역할은 아래와 같다.

  • ApplicationEventPublisherAware: 스프링에서 자동으로 인식하여 ApplicationEventPublisher를 주입한다. ApplicationEventPublisher 인스탄스를 사용해서 이벤트를 스프링으로 보내서 처리하게 된다.
  • ApplicationListener: 이벤트를 수신해서 처리하는 역할이다. 스프링에서 이를 구현한 빈 객체를 자동으로 인식하여 이벤트 리스너로 등록해서 이벤트를 전달해서 처리하도록 한다.
  • ApplicationEventPublisher: 이벤트를 발송하는 역할로 ApplicationListener으로 이벤트를 보낸다.

클라이언트인 DemoController를 구현해보자. DemoController는 이름으로도 알 수 있듯이 스프링의 컨트롤이다. 단순 예제이므로 왜 이렇게 사용하고 있지라고 생각하지 마시고 어떻게 사용하는지만 참고하기 바란다.

@RestController
public class DemoController implements ApplicationListener<ApplicationEvent>, ApplicationEventPublisherAware {
    static Logger logger = LoggerFactory.getLogger(DemoController.class);

    private ApplicationEventPublisher eventPublisher = null;

    @RequestMapping("/hello")
    public String hello() {
        eventPublisher.publishEvent(new FooBeginEvent(this))
        logger.info("hello called");
        String ret = "Hello world!";
        eventPublisher.publishEvent(new FooEndEvent(this));

        return ret;
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
        logger.info("setApplicationEventPublish : {}", eventPublisher);
        this.eventPublisher = eventPublisher;
    }

    @Override
    public void handleApplicationEvent(ApplicationEvent event) {
        logger.info("onApplicationEvent : {}", event);
        if(event instanceof FooBeginEvent) {
            logger.info("fooBegin : {}", event);    
        } else if (event instanceof FooEndEvent) {
            logger.info("fooEnd   : {}", event);
        }
    }
}

ApplicationEventPublisherAware에 의해서 setApplicationEventPublisher() 호출하여 ApplicationEventPublisher 인스탄스를 eventPublish에 주입된다. 그리고 주입받은 eventPublish 객체의 publishEvent()를 통해 이벤트 객체를 넘기면 된다.

그리고, ApplicationListener에 의해서 onApplicationEvent()로 다른 곳에서 발생한 이벤트 객체를 받고 처리한다. instanceof를 사용해 이벤트 객체가 FooBeginEvent와 FooEndEvent인지 식별하여 어떤 이벤트인지 구분해서 처리하면 된다.

앞의 경우는 간단한 예를 보이기 위해서 한 클래스에서 모두 구현했을뿐이지 실제로는 서로 분리되서 사용할 수 있다.

실제 실행한 결과 출력되는 로그는 다음과 같다. 보기 편하게하기 위해 로그 앞에 불필요한 정보는 삭제해서 정리했다.

c.t.ospace.controller.DemoController : onApplicationEvent : com.tistory.ospace.controller.DemoController@1388039c
c.t.ospace.controller.DemoController : fooBegin : com.tistory.ospace.event.FooBeginEvent[source=com.tistory.ospace.controller.DemoController@1388039c]
c.t.ospace.controller.DemoController : hello called
c.t.ospace.controller.DemoController : onApplicationEvent : com.tistory.ospace.controller.DemoController@1388039c
c.t.ospace.controller.DemoController : fooEnd : com.tistory.ospace.event.FooEndEvent[source=com.tistory.ospace.controller.DemoController@1388039c]
c.t.ospace.controller.DemoController : onApplicationEvent : org.springframework.web.servlet.DispatcherServlet@62a3379b

hello()호출하면서 FooBeginEvent 이벤트 발생에 의해 onApplicationEvent()에 의해 처리됨을 볼 수 있다. 다음으로 hello()호출에 대한 로그가 출력된다. 마지막으로 FooEndEvent 이벤트 발생으로 인해 onApplicationEvent()에 의해 처리되었다.

손쉽게 사용할 수 있는 방법

@EventListener 어노테이션

앞에 ApplicationListener를 상속하고 처리하기가 불편하다. 스프링에서는 더 쉬운 방법이 있다. 어노테이션을 사용해서 이벤트 핸들러를 쉽게 지정할 수 있다. 기존에는 ApplicationListener를 구현했지만 @EventListener으로 메소드를 지정하면 이벤트를 받을 수 있다. 그리고 메소드 명도 임의로 정할 수 있다.

@RestController
public class DemoController implements ApplicationEventPublisherAware {
    static Logger logger = LoggerFactory.getLogger(DemoController.class);

    private ApplicationEventPublisher eventPublisher = null;

    @RequestMapping("/hello")
    public String hello() {
                eventPublisher.publishEvent(new FooBeginEvent(this))
        logger.info("call hello : id[{}]", Utils.getSessionId());
                eventPublisher.publishEvent(new FooEndEvent(this));

        return "Hello world";
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
        logger.info("setApplicationEventPublish : {}", eventPublisher);
        this.eventPublisher = eventPublisher;
    }

    @EventListener
    public void onApplicationEvent(ApplicationEvent event) {
        logger.info("handleApplicationEvent : {}", event);
        if(event instanceof FooBeginEvent) {
            logger.info("fooBegin : {}", event);    
        } else if (event instanceof FooEndEvent) {
            logger.info("fooEnd   : {}", event);
        }
    }
}

좀더 간편해졌다.

@Autowired 어노테이션

다음은 ApplicationEventPublisherAware을 사용하지 않고 자동으로 주입되게 해보자.

@RestController
public class DemoControlle {
    static Logger logger = LoggerFactory.getLogger(DemoController.class);

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @RequestMapping("/hello")
    public String hello() {
                eventPublisher.publishEvent(new FooBeginEvent(this))
        logger.info("call hello : id[{}]", Utils.getSessionId());
                eventPublisher.publishEvent(new FooEndEvent(this));

        return "Hello world";
    }

    @EventListener
    public void handleApplicationEvent(ApplicationEvent event) {
        logger.info("handleApplicationEvent : {}", event);
        if(event instanceof FooBeginEvent) {
            logger.info("fooBegin : {}", event);    
        } else if (event instanceof FooEndEvent) {
            logger.info("fooEnd   : {}", event);
        }
    }
}

상속 구조가 없어지고 어노테이션으로 처리하게 되었다. 정말 편하게 사용할 수 있게 되었다.
어노테이션으로 처리되고 있지만 실제 내부적으로 ApplicationListener, ApplicationEventPublisherAware를 상속해서 처리하는 방식으로 되어 있다.

이벤트 처리 개선하기

이번에는 이벤트를 매번 조건문으로 구분해서 처리하기 보다, 메소드로 분리해서 호출하는 방식을 살펴보자.

앞의 예제를 보면 DemoController가 자신이 EventListener가 되어서 이벤트를 직접 처리하고 이벤트의 종류를 instanceof를 사용해서 구분을 하고 있다. 이벤트 객체와 리스너가 한번만 사용하고 한 곳에서만 제한적으로 사용하고 있다면 문제가 없지만, 다른 곳에서도 사용할 것이라면 좀 더 개선을 하면 사용하기 편리해질 것이다.

여기서 작업할 내용은 EventListener에서 FooBeginEvent와 FooEndEvent를 객체로 넘기는 것이 아니라 메소드 형태로 구분해서 넘기는 방식으로 개선하는 작업이다.

새로 추가할 클래스는 FooEventListenerAdapter로서 FootBeginEvent와 FooEndEvent 객체를 메소드 단위로 구분해서 이벤트 처리해주는 역할을 한다.

@Component
public class FooEventListenerAdapter implements ApplicationListener<ApplicationEvent> {
    static Logger logger = LoggerFactory.getLogger(FooEventListenerAdapter.class);

    private FooEventListener listener;

    public void setEventListener(FooEventListener listener) {
        this.listener = listener;
    }

    @Override
    public void onApplicationEvent(ApplicationEvent ev) {
        logger.info("onApplicationEvent : {}", ev);

        if(ev instanceof FooBeginEvent) {
            listener.fooBeginEvent(ev);
        } else if (ev instanceof FooEndEvent) {
            listener.fooEndEvent(ev);
        }
    }
}

코드에도 나왔듯이 ApplicationListener를 상속해서 빈으로 등록하기 때문에 onApplicationEvent()로 이벤트를 수신받는다. 이벤트 개체 타입에 따라서 FooEventListener의 fooBegin() 또는 fooEnd()를 호출한다. FooEventListener는 인터페이스로 다음과 같이 선언되어있다.

public interface FooEventListener {
    public void fooBeginEvent(EventObject ev);
    public void fooEndEvent(EventObject ev);
}

이를 사용한 클래스 구조는 다음과 같이 구성된다.

최종적인 DemoController 클래스는 다음과 같이 정의된다.

@RestController
public class DemoController implements FooEventListener, ApplicationEventPublisherAware {
    static Logger logger = LoggerFactory.getLogger(DemoController.class);

    @Autowired
    FooEventListenerAdapter fooEventListenerAdapter;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @PostConstruct
    private void init() {
        fooEventListenerAdapter.setEventListener(this);
    }

    @RequestMapping("/hello")
    public String hello() {
        eventPublisher.publishEvent(new FooBeginEvent(this));
        logger.info("call hello : id[{}]", Utils.getSessionId());
        eventPublisher.publishEvent(new FooEndEvent(this));

        return "Hello world";
    }

    @Override
    public void fooBeginEvent(EventObject ev) {
        logger.info("fooBeginEvent : {}", ev);
    }

    @Override
    public void fooEndEvent(EventObject ev) {
        logger.info("fooEndEvent : {}", ev);
    }
}

FooEventListenerAdapter 객체에 자신 이벤트 리스너 객체를 등록하여 발생한 이벤트를 메소드로 호출하도록 해야 한다. 당연히 FooEventListner를 구현해야하고 이벤트를 받을 fooBegin()과 fooEnd() 메소드도 구현해야 한다. 앞의 예제는 이벤트를 생성하는 객체와 이벤트를 받는 객체가 동일하기 어떤 효용성이 있느냐고 생각할 수 있다. 여기는 단순히 예제일 뿐이기에 사용하는 용도는 적절하게 적용하면 된다.

Kotlin

마지막으로 참고삼아 Kotlin 코드도 올려본다.

@RestController
class DemoController(
    private val eventPublisher:ApplicationEventPublisher 
) {
    companion object
    private val logger = LoggerFactory.getLogger(DemoController::class.java)

    @GetMapping("/hello")
    fun hello() : String {
        eventPublisher.publishEvent(FooBeginEvent(this))
        logger.info("hello called");
        val ret = "Hello world"
        eventPublisher.publishEvent(FooEndEvent(this))
        return ret
    }

    @EventListener
    fun onApplicationEvent(event: ApplicationEvent):Unit {
        logger.info("onApplicationEvent : {}", event.getSource());

        when(event) {
            is FooBeginEvent -> logger.info("fooBegin : {}", event)
            is FooEndEvent -> logger.info("fooEnd   : {}", event)
        }
    }
}

결론

이렇게 해서 스프링에서 이벤트 처리하는 방법을 간단하게 살펴보았다. 일반적으로 이벤트를 생성하는 객체와 이벤트를 수신하는 객체는 분리되어 처리한다. 이를 잘 활용하면 좀더 유연한 구조를 만들 수 있다.
부족한 글이지만 여러분에게 도움이 되었으면 합니다. 모두 즐거운 코딩생활하세요.^^ ospace.

참조

[1] JDK, EventObjec class and EventListener class

[2] Stephane nicoll, Better application events in spring framework 4.2, https://spring.io/blog/2015/02/11/better-application-events-in-spring-framework-4-2, 2018.7.16

[3] Spring, ApplicationEvent class and EventListener class source

반응형