Spring

Async를 이용하여 EventListener 사용하기

leeheefull 2022. 3. 16. 18:32

이전 포스팅에서 트랜잭션을 주의하여 비동기 처리하는 방법에 대해 알아봤습니다. 이번에는 해당 방법을 적용했을 때의 문제점을 고민해봤습니다.

 

우선, 서비스 로직에 비동기 메서드가 존재하기 때문에 의도하지 않은 다른 곳에서 비동기 메서드를 실수로 사용할 수도 있겠다는 생각을 하게 됐습니다. 비동기 처리에는 앞선 포스팅에서 설명드린 것처럼 트랜잭션 문제를 야기할 수 있기 때문에 실수로 사용하지 않기 위해서 똑같은 메서드를 구현해주어야 하는 불편함도 있습니다.

 

그래서, 여러 메서드에 각각 비동기 처리를 해주는 대신, 하나의 메서드에서 비동기로 처리할 각 함수를 호출하고 이를 비동기 처리하면 좋겠다는 생각을 했습니다.

 

또한, 게시판을 삭제하기 위해서 게시글과 댓글, 좋아요 등을 삭제하는 추가적인 로직을 수행해야 합니다. 여러 로직을 수행하는 만큼 예외가 발생할 수 있는 확률이 있기 때문에 트랜잭션을 분리하여 추가적인 로직에서 예외가 발생하더라도 중요한 로직에서는 영향을 받지 않도록 구현하고 싶다는 생각을 하게 됐습니다.

 

그래서, 여러 비동기 메서드를 하나의 비동기 메서드로 합치고 이를 이벤트성 메서드로 관리해 준다면, 좋겠다는 생각을 하게 되어 리팩토링 해보려고 합니다.

 

저번 포스팅 내용에 이어서 설명해 보겠습니다.

 

 

 

리팩토링 전

 

Board 삭제 로직

Board 삭제 로직

 

PostService의 Board 관련된 Post 제거

PostService의 Board 관련된 Post 제거

 

CommentService의 각 Posts의 Comment 제거

CommentService의 각 Posts의 Comment 제거

 

 

 

리팩토링 후

 

Board 삭제 로직

Board 삭제 로직

 

이벤트 메서드를 통한 비동기 처리

이벤트 메서드를 통한 비동기 처리

 

PostService의 Board 관련된 Post 제거

PostService의 Board 관련된 Post 제거

 

CommentService의 각 Posts의 Comment 제거

CommentService의 각 Posts의 Comment 제거

 

중점적으로 신경 써야 할 부분은 BoardFacade.delete()와 BoardEventListener.deleteByBoard()의 관계입니다. 전자를 부모 메서드라고 하고 후자를 자식 메서드라고 하여 설명드리겠습니다.

 

 

 

Event

 

스프링에서 이벤트 메서드를 사용하려면, Event Object와 Event Listener, Application Event Publisher라는 세 가지 개념을 알아야 합니다. 다음 세 가지에 대해 설명하겠습니다.

 

 

Event Object

 

모든 이벤트 상태의 객체가 파생되는 루트 클래스입니다. 이벤트를 발생시킬 인자를 이벤트 객체로 생성하고 이벤트 리스너에서 이를 이용하여 이벤트의 행위를 구현해주면 됩니다.

 

현재 프로젝트 사용될 Event Object

현재 프로젝트 사용될 Event Object

 

 

Event Listener

 

이벤트 오브젝트의 행위를 구현하는 장소입니다. 이를 사용하기 위한 방법으로는 두 가지가 있습니다. @EventListener@TransactionalEventListener입니다. 두 방법의 차이에 대해서 설명하겠습니다.

 

@EventListener

이벤트 리스너로 등록한 경우에는 이벤트가 퍼블리싱된 이후에 리스너가 바로 동작하게 됩니다.

 

위의 방법은 부모 메서드에서 예외가 발생하더라도, 예외 발생 전에 퍼블리싱해줬다면 동작하기 때문에 부모 메서드에서는 예외가 발생하고 자식 메서드에서는 정상 작동을 하게 됩니다.

 

결론적으로 게시판은 삭제가 되지 않지만, 게시글과 댓글은 삭제되는 현상이 발생하게 됩니다.

 

@TransactionalEventListener

트랜잭셔널 이벤트 리스너 같은 경우는 퍼블리싱해 준 메서드의 트랜잭션이 커밋된 이후에 동작하게 됩니다.

 

위의 방법은 부모 메서드가 커밋되고 나서 퍼블리싱이 되기 때문에, 부모 메서드에서 예외가 발생한다면, 자식 메서드를 호출하지 않게 됩니다.

 

결론적으로 게시판이 삭제되지 않고 예외가 발생하여, 게시글과 댓글도 삭제되지 않아서 현재 상황에서는 해당 방법이 적합한 방법입니다.

 

@TransactionalEventListener를 사용한 모습

@TransactionalEventListener를 사용한 모습

 

 

Application Event Publisher

 

스프링에서는 ApplicationEventPublisher를 통해서 이벤트에 필요한 인터페이스를 제공해줍니다.

 

Event Object를 생성하고 Event Listener로 행위를 정의하고 Application Event Publisher를 통해서 이벤트를 퍼블리싱할 수 있습니다.

 

ApplicationEventPublisher를 사용한 모습

ApplicationEventPublisher를 사용한 모습

 

 

 

만약, 자식 메서드에서 예외가 발생한다면

 

위에서는 부모 메서드에서 예외가 발생했을 때에 대한 트랜잭션 처리를 해줬습니다. 하지만, 자식 메서드에서 예외가 발생한다면, 어떻게 될지 고민하게 됐습니다.

 

트랜잭션의 디폴트 프로파게이션은 REQUIRED이기 때문에, 자식 메서드에서 예외가 발생하여 롤백이 된다면, 이가 전파되어 부모 메서드의 트랜잭션도 롤백을 하게 됩니다.

 

그렇기 때문에 REQUIRED가 아닌 REQUIRES_NEW를 해줍니다. 그러면 매번 새로운 트랜잭션을 만들어 부모 메서드와 자식 메서드의 트랜잭션을 서로 분리해줄 수 있습니다.

 

분리하게 되면, 자식 메서드에서 발생한 예외가 부모 메서드에게는 영향을 주지 않게 됩니다.

 

하지만, 사실 REQUIRES_NEW를 할 필요가 없습니다.

 

자식 메서드의 @TransactionalEventListener를 달면, 부모 메서드가 커밋된 이후에 이벤트 리스너인 자식 메서드가 발동되기 때문에 자식 메서드의 예외로 인해서 부모 메서드가 롤백되지 않습니다. 그래서 REQUIRES_NEW를 해주지 않아도 됩니다.

 

https://user-images.githubusercontent.com/58816862/158553953-8b575c89-37f0-48f3-a707-d15145c7b9c0.png

 

 

 

결론

 

자식 메서드에서 예외 발생

 

자식 메서드에서 예외가 발생한다면, 자식 메서드의 트랜잭션은 롤백이 되고 부모 메서드의 트랜잭션은 커밋이 됩니다. 즉, 게시글과 댓글은 지워지지 않지만 게시판은 지워집니다.

 

 

부모 메서드에서 예외 발생

 

부모 메서드에서 예외가 발생한다면, 부모 메서드의 트랜잭션은 롤백이 되고, 자식 메서드는 호출이 되지 않습니다. 즉, 게시판과 게시글, 댓글 모두 지워지지 않습니다.

 

 

 

자식 메서드 예외 발생 추후 처리

 

자식 메서드에서 예외가 발생한다면, 게시판은 지워지지만 게시글과 댓글이 지워지지 않는 상황이 발생한다. 그렇다면 이것은 해결해야 할까요?

 

아직 정확하게 어떻게 해결해야 할지는 모르지만, 이렇게 즉각적으로 처리하지 않아도 될 로직들은 에러 테이블을 생성하여 기록해두고 나중에 여유 있을 때 처리할 수 있도록 하면 좋겠다는 생각이 들었습니다.

 

이후에 이것 또한 개발을 완료하여 포스팅하겠습니다.

 

이상입니다. 수정 사항이나 피드백이 있으시다면 편하게 댓글 부탁드리겠습니다. 글 읽어 주셔서 감사합니다