Spring Events
- Spring ApplicationContext는 이벤트를 발행하는 기능을 제공한다.
- 스프링이 관리하는 이벤트는 기본적으로 다음 가이드를 따른다.
- 스프링 이벤트는
ApplicationEvent
을 상속한다.
publisher
는ApplicationEventPublisher
객체를 주입받아 사용해야 한다.
listener
는 ApplicationListener
인터페이스를 구현해야 한다.
- 이벤트는 기본적으로 동기적으로 동작한다.
ApplicationEventMulticaster
빈을 생성하고 Executor로 SimpleAsyncTaskExecutor
를 사용하면 비동기적으로 동작하는 이벤트를 발행할 수 있다.
- 이벤트 리스너의 동작이 다른 스레드에서 수행된다.
@TransactionalEventListener
를 사용하면 트랜잭션의 여러 위치에서 이벤트 리스너를 수행할 수 있다.
- AFTER_COMMIT (default) : 트랜잭션 커밋 후 리스너 실행
- AFTER_ROLLBACK : 트랜잭션이 롤백 될 때만 실행
- AFTER_COMPLETION : 트랜잭션 종료 후(커밋 or 롤백) 리스너 실행
- BEFORE_COMMIT : 트랜잭션이 커밋되기 전에 리스너 실행
Demo
시나리오
- 서비스를 만들다 보면, 처음에는 단순한 crud로 시작했던 API도 점차 복잡한 연관 관계가 생기고, 동시에 처리해야 할 일들이 생긴다. 그리고 더욱 복잡한 기능을 구현하기 위해 외부 모듈이나 시스템을 연동하여 사용하면서 하나의 요청에 함께 묶여 수행되는 로직이 점차 많아지는 것을 느껴본 적이 있을 것이다.
- 하지만 이렇게 요청에 묶인 트랜잭션에서 많은 일을 수행하게 되면, 사용자가 원하는 요청의 의도와 서버에서 실제로 수행되는 로직 간의 차이가 생기게 된다.
아래 작성한 코드는 간단하게 사용자가 회원가입 할 때 가입 축하 메일을 발송하는 서비스 코드이다. 외부 모듈을 연동해서 사용하는 간단한 예제이다.
- 그러나 이 같은 코드 구조는 다음과 같은 문제를 낳을 수 있다.
- 현재 메일 발송을 위해 외부 SMTP 서버를 사용하여 새로운 요청을 보내고 있다. 메일을 발송하는 속도가 느려지거나 외부 서버의 문제로 인해 메일을 보내는 중간에 요청이 실패한다면 어떻게 될까?
- 호출되는
sendMail
메서드를 보면, MemberService의 save 메서드와 하나의 트랜잭션으로 묶여있다.
- 그래서 sendMail이 실패하면 당연히 회원가입 로직도 실패하고 정상적으로 회원가입이 되지 않는다.
- 연관된 로직의 원자성을 보장하는 것은 분명 좋은 일이지만, 사용자는 회원가입을 원한 것이지 메일 발송을 요청한 것이 아니다.
- 클라이언트 요청의 의도(회원가입)와 다른 로직(메일발송) 때문에 속도가 느려지거나, 실패하여 전체 회원가입이 원점으로 돌아간다면 이것은 분명 문제로 인식되어야 한다.
회원가입과 축하 메일 발송 작업의 트랜잭션을 분리하고 시간의 순서를 보장할 수 없을까?
이벤트를 사용한 메일 발송 로직 분리
- "회원가입이 성공"이라는 이벤트를 만들고, 이벤트 리스너에서 MailSender의 메서드를 호출한다면, 회원가입이 일어나는 트랜잭션과 분리해서 메일 발송 로직을 수행할 수 있다.
- 회원가입 트랜잭션 안에서 해당 이벤트를 함께 발행한다.
- 여기서는 사용자의 회원가입과 데이터베이스 저장이 모두 완료된 Transaction이 끝난 이후에 메일 보내야 하므로,
@TransactionalEventListener(phase = AFTER_COMMIT, fallbackExecution = true)
어노테이션 속성을 사용한다.
- fallbackExecution 값이 true이면 만약 트랜잭션이 존재하지 않는 곳에서 이벤트를 발행했을 경우, 예외를 던진다.
- 결과적으로 회원가입 로직 수행 및 트랜잭션에서 성공적으로 커밋된 이후에 이벤트 리스너에 정의된 sendMail 메서드를 호출할 수 있다.
결합도
- 이벤트를 사용하면 트랜잭션 안의 관심사를 분리할 수 있지만, 결과적으로 멤버도메인에서 MailService를 직접 의존하지 않기 때문에 결합도를 낮출 수 있는 장점이 있다.
- 이벤트를 사용하지 않고 MemberService에서 다른 서비스들을 직접 의존하고 호출한다면, 기능이 수정되거나 확장될 때 유지보수를 어렵게 만든다.
- 그러나 문자를 보내는 서비스를 다음 그림과 같이 추가했을 때, 회원가입 요청은 해당 트랜잭션에서 응집도 높은 로직을 수행할 수 있고 이벤트는 이벤트 관리 큐에서 종합적으로 관리할 수 있다.
- 이는 외부 모듈과의 결합을 약하게 하므로 변경과 확장에 유연해진다는 장점이 있다.
결론
이벤트를 발행한다는 것은 메시지-통신 기반 플로우를 이해하고, 카프카나 리액티브 프로그래밍과 같은 비동기 논블로킹 프로그래밍으로 입문하기 위한 첫 걸음이라고 할 수 있다.
결과적으로 이벤트 발행을 통해 특정 트랜잭션과 분리하여 로직을 수행하고, 이는 곧 결합도를 낮추는 역할을 했다.
이벤트를 발행하고 소비하는 것은 외부 모듈 간의 낮은 결합도를 유지하면서 협력관계를 유지하고자 할 때 유용하게 사용할 수 있다. ex) 엘라스틱 서치와 기존 데이터베이스 간의 데이터 동기화를 할 때 이벤트 발행으로 처리 등.
이처럼 이벤트 발행은 분산 시스템을 효과적으로 관리하게 해주기 때문에 단순히 외부 시스템 간의 소통뿐만 아니라, 도메인 주도 설계와 MSA 관점에서도 의미 있는 방식이라고 할 수 있다.