과제를 마무리하고 제출한 후 각 과제에 대한 해설 세션이 있었다. 이번엔 중요하지 않고 자주 했던 부분들을 제외하고 새로 공부하는 부분에 대해서만 집중했는데도 고생이었다.
개선 과제, 복습 과제 모두 진행됐고 개선 과제의 경우 과제에 해당하는 부분은 영상으로 이미 촬영되어 있고 라이브는 사전에 어려운 부분에 대한 질문을 받은 내용에서 진행됐다.
이번 주차는 Event 관련한 주제를 다뤘다.
아키텍쳐는 범위를 어떻게 생각하느냐에 따라 사용하는 기술과 생각해야하는 부분도 달라진다.
더 큰 규모의 EDA도 가능하지만 강의는 가장 작은 범위인 Spring application 내에서의 EDA를 다뤘다.
EDA의 최종 목적은 Event를 이용한 비동기 통신이다.
이벤트를 처리하는 Event Queue는 Kafka, Redis Pub/Sub, AWS SNS/SQS, Spring event(단일 Application만) 등이 역할을 수행할 수 있다.
Spring event에서 위 2개는 Spring event에서는 크게 와닿을 내용은 아니다.
반드시 기억해야 할 키워드는 멱등성이다.
멱등성이란 동일한 연산을 여러 번 수행해도 결과가 달라지지 않는 성질
하나의 Spring application에서 Event를 이용하기 쉽게 준다.
Spring Event가 Event Queue의 역할을 맡아주고 Publisher, Subscriber는 객체가 된다. 이렇게 해서 손쉽게 EDA의 구조를 녹여낼 수 있다. 추가로 IntelliJ의 지원까지 받을 수 있다.
Bean으로 등록한 ApplicationEventPublisher의 .publish에 Event object를 담아서 발행하면 Publisher의 역할이 끝난다.
Event의 내용이 들어가는 객체로 Publisher가 발행하고 Subscriber가 구독한다.
Spring 4.2부터 POJO도 Event object로 사용이 가능해졌다.
@EventListener를 이용하면 쉽게 구현할 수 있다.
@EventListener fun listener(event: Type)
의 형태가 된다.
구현 자체는 매우 쉬운 편이니 결합도, 장애의 전파, 관심사의 분리 측면에서 어디에 써야할지 신경쓰는 것에 집중함이 좋다.
Spring Event는 이 구조가 특이해서 기본적으로 동기(Sync) 로 동작한다.
위에서 다룬 내용대로면 비동기로 동작하는 걸 기대했겠지만 Spring Event는 기본적으로 동기기 때문에 @Async 등을 활용해서 직접 해당 메소드가 비동기로 동작되도록 수정해야 한다.
위에서 다룬 동기식의 다른 의미는 같은 Thread에서 동작하게 된다는 의미이고 Event를 Publish하는 메소드에 Transaction이 걸려있다면 Subscriber 메소드도 하나의 트랜잭션이 된다.
그렇기에 Event Listener의 로직에서 에러가 나면 롤백 이 되기 때문에 주의해야 한다.
Event Listener에서 격리된 트랜잭션을 처리하고 싶다면 @Transactional(propagation = Propagation.REQUEST_NEW) 전파 수준을 고려해볼 수 있다.
단, @Async를 통해 비동기로 처리했다면 이 이슈를 회피할 수 있는데 우리가 다루는 Transaction은
ThreadLocal
을 기반으로 동작하게 되고 @Async를 통해 쓰레드가 분리되면 동일한 Transaction으로 묶을 수 없게 된다.
EventLister와 유사한 어노테이션으로 Spring Transaction에서 트랜잭션 동기화를 위한 Synchronization 이라는 기능을 제공한다.
이를 이용한 Listener로 Transaction을 모니터링 하고 있다가 특정 시점에 호출되는 Callback 메소드다.
이 또한 ThreadLocal 기반으로 동작하기 때문에 비동기를 다루고 있었다면 사용에 주의해야한다.
TransactionSynchronizationManager.registerSynchronization(
object: TransactionSynchronization {
// override afterCommit /* etc.. */
})
TransactionSynchronization 기능은 afterCommit, afterCompletion, beforeCommit, beforeCompletion 등의 생명 주기 시점을 제공한다.
이를 Event와 결합해서 Publish한 Transaction이 Commit 된 후 Event Listener 동작하기 같은 요구사항을 구현할 수 있다.
@EventListener
fun listener(event: SomethingEvent) {
TransactionSynchronizationManager.registerSynchronization(
object: TransactionSynchronization {
// override afterCommit /* etc.. */
})
}
이런 구조를 반복해서 쓰기 복잡하기 때문에 @TransactionalEventListener 가 등장했다.
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun listener(event: SomethingEvent) { /*etc*/ }
Event의 단점을 보완해주는 방식으로 Event를 발행 / 구독하는 과정에서 아무런 정보도 없이 Event를 Callback처럼 사용해서 실제로 구독자가 해당 시점에 API를 호출하게 만드는 방식이다.
복잡도는 동일하지만 Payload의 변경이 일어날 때 전파를 최소화할 수 있는 방법이라는 생각이 든다.