회원가입, 메일전송 문제해결과정 (1) - ApplicationEventListener, @Async

깡통·2023년 8월 28일
0

영프런

목록 보기
1/1

개인프로젝트에서 회원가입에 대한 기능을 개발하던중 생겼던 문제와
문제를 해결했을 당시의 기준과 생각을 최대한 bold처리 해두고 정리해보려고합니다.

플로우

회원가입 플로우는 대략 아래와 같습니다.

  • 회원가입 요청시 메일전송후 회원을 DB에 저장한다
  • 메일에는 회원의 고유한 UUID토큰과 이메일 인증확인 API uri가포함된다
  • 메일 인증을 완료한다

요구사항

  • 회원가입시 메일전송의 문제때문에 사용자가 한번더 회원가입 해야하는일은 최대한 없어야한다.
  • 메일전송 이라는 이벤트의 손실은 최대한 방지해야한다.
  • 메일전송으로 인해 회원가입의 응답이 지연되지않았으면 한다.

고려할 점

  • 이메일전송이라는 이벤트가 지금도, 앞으로도 트래픽이 엄청나게 몰리는 이벤트까지는 아니라고생각한다 (회원가입 이벤트같은걸 하지않는이상..)
    => 그래도 대규모 트래픽이 몰릴수있다는 가정하에 어떻게 해볼건지 까지 생각해보자

문제 상황

해당 서비스 클래스(MemberService)에는 @Transactional이 붙어있습니다.

해당 코드를 작성하고 조금더 개선해야할 부분이있을까?에대해 고민하다가
mailService.send(emailMessage(member)) 에서 예외가 발생한다면 사용자의 요청을 롤백해야만하는건가?
사용자의 귀중한시간으로 폼을작성하고 요청을 보냈는데 롤백이된다면 서비스에대한 호감도가 낮아지고 떠나는사람도 생길겁니다.

이 시점에서 들었던 첫번째 생각은 해당 트랜잭션에서 회원저장과 메일전송을 어떻게 분리하지? 였습니다. 메일전송에 실패해도 회원은 저장하고싶었기 때문입니다.

ApplicationEventListener와 @Transactional 전파옵션

방법은 크게 위의 두가지가 있었습니다.

  • 단순히 트랜잭션을 분리하기위해서는 @Transactional(propagation = Propagation.REQUIRES_NEW)로도 해결할수있겠지만
    EventListener를 사용하면 MemberService와 MailService를 분리할수있습니다.
    • MemberService는 메일전송 때문에 MailService를 의존하고있습니다. 만약 회원가입시 이벤트가 추가되거나 변경된다면 MemberService의 코드가 변경되야합니다.
    • 분리한다면 회원가입시 추가적인 행위가 추가되더라도 이벤트를 핸들링하는 객체만 추가로 생성해주면 됩니다.
  • 둘의 복잡도도 크게 차이나지않았고 ApplicationEventListener를 선택했습니다.(트랜잭션의 분리는 @Async를 같이 사용한다는 전제하에 고려했습니다)

다음은 이벤트를 처리하는 여러가지 방법(메시징 시스템사용, DB같은 저장소에 보관 등)이 있었는데 스프링의 기술을 선택한 이유는 아래와같습니다

  • 학습비용이 가장 낮았습니다.

scale-out

  • 동일한 스프링서버를 여러대 늘릴때 지장이 없다고 판단했습니다. 이벤트에 대한 요청을 받는 서버는 한대일것이고 요청을 받은 해당서버가 이벤트를 생성해 이벤트를 핸들링할것입니다.
    • 예를들면 스프링서버 A,B,C,D로 증설하더라도 A서버에서 이벤트 요청을받으면 A에서 핸들링, B서버에서 이벤트 요청을 받으면 B서버에서 이벤트를 핸들링합니다.

이벤트의 성질

  • 이벤트가 다른 서버와 공유되는 성질이아닙니다. 따라서 ApplicationEventPublisher의 아래단점들은 문제되지않습니다.
    • ApplicationEventPublisher는 스프링 Bean으로 동작하기때문에 A서버에서 등록된 Bean을 B서버와 공유하기엔 무리가 있다는점
      • 예를들면 회원서비스, 인증서비스가 다른서버로 분리되어있다면 ApplicationEventPublisher사용시 회원서비스에서 등록된 이벤트를 인증서비스로 공유하기 어렵습니다.

트래픽 관련

  • 이벤트를 처리하는 주체가 스프링서버이기 때문에 스프링서버에 이벤트를 관리하는 리소스가 필요해지며, 이는 문제가 될수있습니다.
  • 만약, 이메일 전송이라는 이벤트에 트래픽이 엄청 몰릴수있는 상황이라면 메시징 시스템을 선택고려해볼수 있습니다. 그러나, 현재 프로젝트에서는 회원가입후 이메일 인증이라는 이벤트에 엄청난 트래픽이 몰릴 예정이 없기때문에(선착순 가입이라던지) nginx 같은 웹서버에서 rate limiter기능을 구현하는정도면 충분하지 않을까 해서 ApplicationEventPublisher를 선택했습니다.
  • 어느정도 tps까지 스프링에서 감당할수있을지 테스트는 해봐야합니다. 이는 2편에서 테스트해볼 예정입니다.

위와같은이유로 ApplicationEventPublisher를 사용했습니다.

이제 메일전송의 책임은 더이상 해당 서비스(MemberService)에서 짊어지지않아도 되며, 별도의 이벤트 발생만 알리면 이벤트핸들러로 관리됩니다.

그러나 문제가 여전히 존재했습니다.

  • 이벤트로 메일전송을 분리하더라도 같은 트랜잭션에 묶여있기때문에 회원저장을 무사히 마쳐도 이메일전송에 실패하면 롤백해야하는건 매한가지였습니다.
  • 이메일 전송에 지연이 생긴다면 사용자는 회원가입에 대한 응답을 늦게 받아볼수밖에 없습니다.

제가 선택한것은 비동기로 처리하기였습니다. 요구사항에서도 메일전송으로 인해 회원가입의 응답이 지연되지않았으면 한다. 라는 내용이 포함되어있었기때문입니다.

@Async

@Async는 스레드를 하나 생성해서 해당 스레드에서 DB 커넥션 하나를 추가로 사용해 다른 트랜잭션을 사용하므로 , 해당 방법으로 요구사항중 2개를 해결했습니다.

  • 회원가입시 메일전송에 실패가 생기더라도 회원저장을 롤백하고싶지않다.
  • 메일전송 이라는 이벤트의 손실은 최대한 방지해야한다.
  • 메일전송에 문제가 생기더라도 회원가입의 응답이 지연되지않았으면 한다.

그러나 요구사항을 모두 만족하기위한 문제는 아직 남아있습니다.
추가적으로, Async로 처리하게되어 추가적인 고민거리도 생겼습니다.

  • 회원저장은 성공했는데 메일전송 측에서 문제가 생긴다면?
  • 메일 전송은 성공했는데 회원저장 에서 문제가 생긴다면?
  • 스레드를 추가로 사용하게되어 문제점이 생길수 있지않을까? 스레드 관리는 어떻게 할까?

메일 전송은 성공했는데 회원저장 측에서 문제가 생긴다면?

스프링에서 제공하는 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)를 사용합니다.

이 옵션으로 인해 이벤트의 실행시점을 변경할수있는데 AFTER_COMMIT 옵션(default)을 준다면 이벤트 발행쪽에서의 트랜잭션이 커밋된후 해당 이벤트가 실행될수있으므로
회원이 저장된후에야 이메일이 이벤트가 실행됩니다.

성공적으로 테스트 성공했습니다.

남은 문제는 아래와같습니다.

  • 회원가입시 메일전송에 실패가 생기더라도 롤백하고싶지않다.
  • 메일전송 이라는 이벤트의 손실은 최대한 방지해야한다.
  • 메일서버에 문제가 생기더라도 회원가입의 응답이 지연되지않았으면 한다.
  • 회원저장은 성공했는데 메일전송 측에서 문제가 생긴다면?
  • 메일 전송은 성공했는데 회원저장 측에서 문제가 생긴다면? (여기서 해결된 문제)
  • 스레드 풀, 스레드 관리는 어떻게 할까?

다음편에 이어집니다.

0개의 댓글

관련 채용 정보