[SNS 개발] 분산 트랜잭션에 관한 고민
분산 트랜잭션에 관한 고민
서비스를 나누어 개발을 하다보니 결국은 분산 트랜잭션을 도입해야하는 단계가 왔다. 애초부터 Axon과 같은 오케스트레이션 Saga 방식은 고려를 하고 있지 않았다. 도입한다면 콜레오그래피 방식을 고려하고 있었는데... 생각보다 난이도가 높다.
우선 시나리오 설명에 앞서, 삭제 라는 단어는 데이터베이스에서 저장된 레코드를 정말로 삭제하는 것을 말한다.
시나리오는 다음과 같다.
- 멤버 서비스에 회원 탈퇴 요청을 보낸다.
- 멤버 서비스는 카프카에 회원 탈퇴 이벤트를 발행한다.
- 인증, 그룹 서비스는 카프카에서 토픽을 구독하고 있다가 회원 탈퇴 이벤트를 소비한다.
- 인증 서비스는 회원 탈퇴 이벤트를 받으면, 회원의 인증 정보를 삭제한다.
- 그룹 서비스는 회원 탈퇴 이벤트를 받으면, 자신의 ID가 존재하는 그룹 회원을 삭제한다. 그리고 자기 자신이 owner인 그룹을 모두 삭제한다.
카프카에 이벤트가 발행되기만 하면 모든 로직이 반드시 수행된다고 보장할 수 있다면 정말 단순하게 구현 가능하다.
문제 시나리오
- 회원 서비스에서 회원 탈퇴 요청을 받고, 회원을 삭제한다.
- 회원 서비스에서 회원 탈퇴 이벤트를 발행한다.
- 인증 서비스에서 회원 탈퇴 이벤트를 소비한다.
- 인증 서비스에서 인증 정보를 삭제한다.
- 그룹 서비스에서도 회원 탈퇴 이벤트를 소비한다.
- 그룹 회원 및 그룹 삭제 트랜잭션이 실패한다.
- 그룹 서비스에서는 트랜잭션 실패 이벤트를 발행한다.
위 시나리오에서 Saga Pattern이라면 당연히 보상 트랜잭션이 실행되면서 데이터를 롤백 시킬 수 있어야한다. 그런데 그룹 서비스의 실행 시점에서 회원 서비스와 인증 서비스에서 해당 회원이 이미 삭제된 상태라면? 이미 삭제된 데이터를 어떻게 롤백 시킬 것인가?
방안 1. CQRS
복잡한 머리 속에서 가장 먼저 떠오른 방안은 CQRS이다. 모든 데이터베이스를 커맨드 / 쿼리용으로 분할하면 커맨드에는 변경 관련 로직이 그대로 남아있을 것이니 실패한 최종 트랜잭션을 제외하고 전부 조회하여 순차적으로 리플레이를 하는 로직을 구성하면 되지 않을까? 그런데 현실적으로 불가능한 상태이다. 데이터베이스의 용량은 어마무시하게 많이 필요하게 될 것이고, 머신 숫자도 지금 서비스를 위해 준비 해놓은 하드웨어 자원으로는 택도 없다. 무엇보다 현재 작성해놓은 로직들이 CQRS를 통한 분리까지 고려를 어느정도 해서 구성을 해뒀지만, 어쨌든 나눠야하는 작업이 필요하기도 하고...
방안 2. 분산 트랜잭션에 의한 데이터 변경을 모두 로깅
이벤트를 구독하여 데이터를 변경하는 리스너에 분산 트랜잭션에 의한 데이터 변경을 모두 저장한다면? CQRS 처럼 모든 커맨드를 다 저장하지는 않아도 최소한 해당 분산 트랜잭션에 의한 레코드의 변경을 저장해두면 재조회를 통해 복구를 할 수 있지 않을까? 이 방식도 분산 트랜잭션이 발생 했을 때, 데이터의 변경을 저장하기 위한 어떤 데이터베이스가 필요한 것은 여전하다. 이걸 할바엔 그냥 CQRS로 전환이 나아보인다.
방안 3. 소프트 삭제
현재 상황에서 그나마 고려해볼만 한 방식이지 않을까 싶다. 모든 테이블의 레코드는 isDeleted 와 같은 boolean 필드를 포함하고, 사용자가 삭제 요청을 하더라도 물리적으로 삭제하는 것이 아닌 저런 필드만을 수정하는 방식이다. 이렇게 되면 보상 트랜잭션은 굉장히 간결해진다.
단순하게 각 서비스에서 isDeleted를 true 표기한 레코드들을 트랜잭션 실패 이벤트에서 회원 ID을 이용하여 다시 검색해서 false로 변경해주기만 하면 된다. 실제 서비스에서는 하드 삭제를 하는 경우가 드물긴 하니 괜찮은 방법 같기는 하지만 회원 탈퇴를 소프트로 처리하는게 맞는가? 라는 의문이 든다.
분산 트랜잭션이 실패 자체도 로그로 남길 것이지만, 완전 비동기 방식을 사용하는 경우 다음과 같은 문제가 발생할 수도 있다.
- 회원이 회원 탈퇴 요청을 보낸다.
- 회원 서비스가 회원 레코드를 소프트 삭제를 한다.
- 회원 서비스가 회원 탈퇴 이벤트를 발행한고, 회원 탈퇴 요청이 200 응답을 반환한다.
- 인증 서비스에서 회원 탈퇴 이벤트를 구독하고 삭제 이벤트를 구독하여 해당 레코드를 소프트 삭제한다.
- 그룹 서비스에서 회원 탈퇴 이벤트를 구독하고 회원이 소유자인 그룹과, 회원이 그룹회원인 모든 그룹회원을 소프트 삭제한다.
- 4,5 트랜잭션 중 하나가 실패하였고, 회원, 인증, 그룹 서비스는 적절한 보상 트랜잭션을 실행한다.
그런데 회원은 탈퇴 요청이 정상 처리된 것으로 생각하고 해당 서비스에 접속 종료 또는 앱을 삭제하였다면...? 삭제된 것으로 생각했던 요청이 처리되지 않았기 때문에 자신의 정보는 서비스에 계속 남아있게 된다. 그렇다면 회원 탈퇴와 같은 중요한 이벤트는 상태를 추적할 수 있어야한다.
주기적 폴링을 하던가 해서 해당 트랜잭션이 정상 처리되었음을 확인한 뒤에야 프론트엔드에서 사용자에게 통보를 해줘야한다.
고민
그나마 현실적으로 구현 가능한 방법은 방안 3 을 고민하고 있다. 멤버 탈퇴 요청이 들어오면 MEMBER_DELETE_TRANSACTION 테이블에 해당 요청을 처리하기 위한 트랜잭션 아이디와 멤버 아이디를 저장해두고, 분산 트랜잭션을 진행시킨다. 응답으로는 ACCEPTED 정도를 보내주고, 주기적 폴링을 통해서 해당 트랜잭션의 처리결과를 확인한 뒤 회원이 탈퇴하는 방식이다. 뭔가 좀 어정쩡한 느낌이 들기는 하지만 지금 인프라 상황에서 가장 현실적인 방법이 아닐까 싶기도 하고...