Spring Events

bp.chys·2020년 10월 1일
1

Spring Framework

목록 보기
15/15

Spring Events

  • Spring ApplicationContext는 이벤트를 발행하는 기능을 제공한다.
  • 스프링이 관리하는 이벤트는 기본적으로 다음 가이드를 따른다.
    • 스프링 이벤트는 ApplicationEvent을 상속한다.
    • publisherApplicationEventPublisher 객체를 주입받아 사용해야 한다.
    • listenerApplicationListener 인터페이스를 구현해야 한다.
  • 이벤트는 기본적으로 동기적으로 동작한다.
  • 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 관점에서도 의미 있는 방식이라고 할 수 있다.

profile
하루에 한걸음씩, 꾸준히

0개의 댓글