회원가입이벤트와 토큰 생성 이벤트 1(설계 이유,@Transactional)

유수민·2023년 1월 19일
0

DDD 프로젝트

목록 보기
6/7
post-thumbnail

이 주제의 시작은 '만명의, 천명의 사람들이 한번에 회원가입 요청을 했을때 터지지 않게 하고 싶다' 였다. 물론 회원가입가지고 트래픽이 몰릴 일은 없지만, 일단 회원가입요청을 대상으로 설계해보고 싶었다. 회원가입로직은 '회원정보저장 -> 토큰 생성 -> 회원인증메일보내기 ->회원인증하기'로 구성 될 예정이다. 설계의 기준은 동시에 회원가입 요청에 많은 사람들이 몰렸더라도 무사히 잘 돌아가게 하고 싶은데 이것을 어떻게 설계하면 좋을까였다.

이번 주제 시리즈는 회원가입로직에서 '회원정보저장 -> 토큰 생성'까지의 로직만 기준으로 이벤트처리와 비동기적 처리를 다루면서 @Async, @Transactional에 대한 문제들을 해결하고 그때 당시 한 생각들을 풀어나갈 예정이다.

  1. 설계 이유, @Transactional
  2. @Async
  3. @EventListener와 @TransactionalEventListener

이번 글 관련 깃허브 PR : https://github.com/sue4869/ottsharing/pull/2

📌이벤트를 왜 적용했는가?

회원가입로직은 앞서 말했듯이 '회원정보저장 -> 토큰 생성 -> 회원인증메일보내기 ->회원인증하기'로 4가지 구성으로 되어 있다. 만약 한 서비스단위에서 이 4가지를 다 한다면, 아래의 문제점이 발생할 것이라 생각했다.

  1. 결합도가 높아진다.
  • 해당서비스가 후처리 로직들을 모두 직접적으로 알고 있어야 한다.
  • 후처리 로직들이 변경, 추가 되면 전체 로직에 직접적인 영향을 끼칠 수 있다. OCP 위반이다.
  1. 객체 책임이 너무 크다.
  • DDD 구조의 가장 중요한 특징 중 하나는 객체 책임 분리이다. 한 책임에 여러 책임이 같이 있는 것은 DDD 구조에 맞지 않다. 책임을 분리하여 각 책임들은 다른 책임에 영향을 끼칠 수 없도록 해야한다.

따라서, 결합도를 낮추고, 객체 책임을 분리하는 방법인 이벤트를 적용하게 되었다.

📌비동기 프로그래밍을 적용?

생각해보자.
한 요청당 1초의 시간이 걸린다면 기존의 구조에서라면 100명의 사람이 동시에 요청이 오면, 마지막 사람은 100초까지 대기를 해야한다. 이것은 동시성있는 서비스라고 할 수 없다. 나는 100명의 사람들이 모두 1초만에 결과를 받을 수 있는 서비스를 짜고 싶었다. 즉, 대용량 트래픽을 위해, 동시성있는 서비스를 위해 비동기 프로그래밍을 수행하려고 한다. 비동기 프로그래밍을 통해, 놀고 있는 쓰레드를 최소화하고 자원을 최대한 효율적으로 쓰도록 하는 것이 목표이다.
좀 더 보충설명을 위해, 그림을 그려보자면,

동기적이라면,

위의 그림처럼, 요청이 3개가 들어왔다고 하면, 한 쓰레드에서 모두 작동을 해야하니 첫번째의 요청이 뒷부분의 처리까지 모두 완료되고 난 후, 두번째 요청을 시작할 수 있다.

하지만 비동기적이라면,

요청1이 들어오면 회원정보저장를 한 후, 요청2가 바로 수행될 수 있다. (요청1은 회원정보저장이후 후처리는 다른 쓰레드가 알아서 해줄 사항이니까 단순히 이벤트를 publish할 뿐 처리는 신경쓰지 않는다)

📌Transactional

🌱@Transactional

이벤트 처리를 위해 Transactional을 통해 작업 단위를 분리해주었다.
즉, rollback의 범위를 경계짓기 위해 난 Transactional로 각 이벤트들의 경계를 분리하였다. 상황을 가정해보자. 회원가입 요청을 받으면, 회원가입을 하고 토큰을 생성할 때 만약 토큰 생성하면서 오류가 난다면? 어디까지 rollback을 해야할까? 난 토큰 생성에서 오류가 나더라도 처음 행한 회원 정보 저장부분은 그대로 유지되어야한다고 판단했다. 따라서 토큰생성에서 exception이 발생하면, 모든 처리를 다 rollback하여 사용자에게 회원가입요청을 다시 요청하기 보다는 그대로 유지하되, 토큰 생성로직만 rollback하고 후에 우리가 다시 토큰을 생성해줘야하는 것이 합리적이라 생각했다.

따라서,

이벤트 발행하는 쪽인 회원을 저장하는 processor와 이벤트 처리하는 쪽인 토큰 생성하는 processor에 각각 @Transactional을 적용하였다.

🌱아직은 동기상태다.

이것으로 끝? 아니다. 이렇게만 적용하면 동기적으로 작동한다. 내가 원하는 바가 아니다. 즉, 한 쓰레드 내에서 회원 정보 저장하고 토큰을 생성한다는 말이다. 왜? 각 경계짓고 싶은 곳마다 @Transactional 걸어줬는데? Spring boot의 내장 서블릿 컨테이너인 tomcat은 ThreadPool을 사용하고, @Transactional 걸어준 곳 마다 ThreadPool에서 하나씩 Thread를 할당해줘야 하지 않나?라고 생각할 수 있다. 결론부터 말하자면, @Transactional을 걸어준다고 해서 각각 Thread가 할당되지 않고, 같은 Thread에서 작동되기 때문에 결국 하나의 트랜잭션에서 진행되어 동기적으로 작동된다.
이유는 transactional 전파 속성 default버전에 있다.
현재 spring boot에서의 transactional 전파속성의 default는 "REQUIRED"이다.

위의 정의된 것처럼 "REQUIRED"는 이미 현재 트랜잭션이 있다면, 해당 트랜잭션에 합류하고 없어야 새로운 트랜잭션을 생성하는 것이다. 따라서,
코드로 말하면,

위 그림에서 빨간 박스는 회원정보저장끝났다는 이벤트를 발행하는 구간이다. 해당 이벤트가 발행되는 것을 확인하면 토큰생성로직이 발생된다. 테스트해보자
각 구간마다 회원정보 저장 시작, 회원정보 로직끝나기 전 회원정보저장이벤트 발행이 된 후인 회원정보 저장중간, 토큰 생성시작, 토큰 생성 완료를 찍도록 만들었다.

테스트 결과 '회원정보 저장 시작 -> 토큰생성시작 -> 토큰 생성 완료 -> 회원정보 저장 중간' 으로 로직 흐름이 되는 것을 알 수 있었다.
즉, 회원 정보 저장 로직이 토큰이 생성 처리가 완료가 되는 것을 다 기다린 후에야 회원 정보 저장 로직이 끝난다는 것을 확인할 수 있었다.

🌱전파속성을 REQUIRES_NEW로 바꾼다면?


'REQUIRES_NEW'는 매번 새로운 트랜잭션을 생성하는 속성이다.
테스트하기전 난 새로운 경계로 구분했기 때문에 회원저장로직과 토큰생성로직이 서로 영향이 없는 줄 알았다. 하지만 아니였다.
우선 내가 간과한것은 매번 새로운 트랜잭션을 만드는 것이 새로운 쓰레드가 부여된다는 것은 아니라는 말이다.

위 사진에서 보여주듯이, 트랜잭션 전파속성을 REQUIRES_NEW로 바꿔주더라도 같은 쓰레드를 사용하는 것을 확인할 수 있다.
또한, 다른 트랜잭션이더라도 뒤에 트랜잭션에서 예외가 발생시 앞 트랜잭션까지 rollback이 되는 것을 확인할 수 있었다. 즉, 토큰생성로직에서 예외가 발생하면 회원정보저장로직까지 rollback이 되는 것을 확인하였다.
모두 다 rollback되는 것을 막기위해, try-catch문으로 잡아주거나 @async를 이용하는 방법이 있다. 난 async를 적용하여 비동기적으로 작동시키는 것이 try-catch문을 잡기보다는 더 합리적이라 생각하게 되기도 하고, 대용량요청 목적에는 비동기적으로 작동시키는 것이 더 부합하다는 생각이 들어 @Async를 적용하였다.

Async에 대한 내용으로 2탄에서 계속 이어집니다...!

profile
배우는 것이 즐겁다!

0개의 댓글