https://tech.kakaopay.com/post/msa-transaction/
위 kakaopay 페이지를 참고하여 정리하였다.
✨ MSA의 트랜잭션
하나의 시스템은 수십여개의 MSA로 구성될 수 있다. MSA 환경에서 중요한 것은 연결된 서비스마다 DB를 관리하고 있을 때 일관성을 보장해야 한다는 것이다. 예를 들어 포인트, 상품권 등 여러 서버가 엮인 결제 트랜잭션이 그렇다.
결제 트랜잭션에서의 예외는 이런 것들이 있다.
- [고객 입장] 두 번 결제됨 / 결제취소 안됨 / 결제 성공했는데 잔액 안빠져나감 / 결제취소시 잔액 안돌아옴 / 잘못 돌아옴 ...
- [서버 입장] 결제요청이 여러번 옴 / 결제실패되었는데 기록되지 않음 / 결제 취소요청이 성공요청보다 먼저 옴 / 결제 성공여부 알 수 없음 ...
하지만 이런 예외상황에서 실패했다고 무조건 롤백하는 것은 위험하다.
섬세하고 적절한 트랜잭션 관리가 필요하다는 말이다.
✨ 글로벌 트랜잭션 (분산 트랜잭션)
글로벌 트랜잭션 (분산 트랜잭션)이란, 여러 서버 및 DB에 걸친 트랜잭션을 말한다. 카카오에서 상품권을 구매하는 상황을 생각해보자. '상품권 발송' 트랜잭션에 참여하는 서비스는 '상품권 서비스', '페이머니 서비스'가 있을 수 있다. 페이머니 서비스에서 잔액 차감 성공 데이터를 저장 및 응답한다면, 상품권 서비스는 발송성공 데이터를 저장한다.
MSA에서 분산 트랜잭션 관리는 네트워크 요청을 주고받으면서 이루어진다. 그렇다면 서버 개발자는 네트워크 요청의 예외상황을 잘 다룰 수 있어야 한다! 네트워크 통신의 예외상황은 이러한 것들이 있다
- 같은 요청이 단시간에 여러번 발생함
- 네트워크 요청 순서가 뒤집힙 (취소요청 후 결제요청)
- 타임아웃
- 인프라 문제로 인한 실패
이런 네트워크 문제는 근본적인 예외 발생 이유가 된다.
그렇다면 어떻게 네트워크 요청을, 즉 API 관련 이슈를 안전하게 다룰 수 있을까?
기본적으로 API 요청측, 응답측이 깔끔히 설계되어야 한다.
- API 요청측은 "알 수 없는 에러" 처리를 잘 해야함
- API 응답측은 "멱등성"을 지켜야 함
그렇다면 알 수 없는 에러가 온다는 것은 뭘까? 그리고 API 응답이 멱등성을 지킨다는 것은 뭘까?
🪄 알 수 없는 에러를 처리한다는 것
요청에 대한 응답은 "Success", "Fail", "Unknown"으로 분류된다. "Unknown"에 대해 이야기해보려 한다. 어떤 트랜잭션에서 어떤 서비스가 요청 처리를 성공했더라도, Success 응답을 받지 못할 수도 있다. 이럴 때는 요청을 보냈다는 사실만 알 뿐, 도대체 요청이 제대로 들어갔는지, 요청이 갔는데 성공했는지, 실패했는지 알 수가 없다. 이런 상황이 위험한 이유는, 결제 트랜잭션을 생각해보면 결제서버가 주문서버로부터 요청을 잘 받아 잔액 차감을 했지만 주문서버는 time out으로 실패로 기록할 수 있다. 그럴 경우 돈만 빠져나가고 주문은 실패한 것이다.
이 경우 결제서버에서 사실상 처리가 된 것인데 응답을 못 받은 것이다(Unknown). 성공 응답을 받아야 하기때문에 Unknown 상황에서 이런 시도를 해볼 수 있다. "재요청 시도 (즉시 or 일정시간 뒤 or 요청 성공 확인 후) / 결제 취소 요청 (트랜잭션 무효화 요청 : 보상 트랜잭션 개념) / 무조건 성공으로 여기고 뒤처리하기"
또한 응답을 보내지 못한 결제 서버에서 트랜잭션 결과를 확인할 수 있는 API를 제공하거나, 결제처리를 취소(무효)할 수 있는 API를 제공하는 것이 필요할 수도 있다. 그러나 이런 후처리 API 또한 네트워크 요청을 한다는 것은 매한가지이므로 또 예외가 발생할 수 있다. 에러에 에러에 에러에 에러를 물다보면 무한 루프로 에러 처리를 해야 할 수도 있다. 그렇지만 그런 후처리는 당연히 피해야 한다. 적절한 처리 방법은, 알수없음 응답을 받고나서 한번까지는 후처리(재요청하기 or 취소하기) API를 호출하고, 만약 여기서 또 Unknown 응답을 만나면 트랜잭션을 종료시키는 것이다. 그러면 DB에는 Unknown으로 저장해야하고, 고객측에게 재시도를 권해야 한다. 만약 고객이 재시도를 한다면, 이전 결과가 Unknown임을 확인하고 후처리를 다시 진행하면 된다. (이후 또 Unknown일 경우 다른 장치들로 데이터를 보정하는 방법도 생각해볼 수 있다.)
Unknown 에러는 코드상으로는 try-catch로 처리할 수 있다. Success, Fail, Unknown에 대한 예외처리를 구분하며, Unknown일 경우 재요청을 시도한다. 이 재요청에 대해서도 Success, Fail, Unknown을 구분한다. 이때 try-catch문이 깊어지게 되면서, 예외를 상위로 전파시키게 되는데, 이런 식으로 트랜잭션을 분리하면 의도치 않은 롤백이 될 수 있으므로 주의해야 한다. 트랜잭션에서 예외가 터지면 롤백 마크를 하고, 해당 트랜잭션을 재사용하는 것이 불가능하다고 한다. 이때 롤백 예외를 확인해보면 Transaction silently rolled back becaeuse it has been marked as rollback-only라는 예외 메세지를 확인할 수 있다. 즉 내부 트랜잭션에서 예외가 발생했을 때, 최초 트랜잭션에서 catch했더라도 최종커밋하는 것이 아니라 roll-back only가 마킹되어있을 경우 롤백을 해버린다. (참여 중 트랜잭션이 실패하면, 기본 정책이 전역롤백이라고 한다) : 이 내용은 우아한기술블로그를 참고하였다. kakaopay 페이지에서는 함수형 패러다임을 사용하여 이런 예외를 안전하게(예외의 전파 없이!) 처리하도록 했다.
🪄 API 응답의 멱등성
동일한 요청을 여러번 해도, 같은 응답(성공/실패)를 보장하는 것이 멱등성이다. 개발에서 필수적인 개념이다. 특히 분산 트랜잭션에서 데이터의 일관성을 유지해야 하기 때문이다. 어떤 알수없는 에러로 인해 요청을 여러번 다시 보내더라도, 트랜잭션이 단 한번만 처리되어야 한다. 분산 트랜잭션에서 서버 양쪽이 동일한 상태를 유지하는 것이 MSA에서 중요한데, 만약 요청이 삭제되거나 중복될 수 있는 경우를 신경써야 한다. 요청이 중복되더라도, 그리고 중간에 삭제되더라도, 현 상태에 대한 동일한 응답을 돌려주어야 한다.
Unknown 상황에서 후처리(재시도) 목적으로 다시 여러번 결제요청을 보냈다고 하자. 여러번 요청이 들어왔는데 언제한번 이미 Success한 요청이라면, 모든 요청을 항상 Success으로 응답해야 한다. 와닿는 예를 들어 예외상황이 '일시적 네트워크 지연'으로 인한 Unknown 응답이었다면, 재요청을 통해서 금방 Success를 받을 수 있다. 이때 필요한 것은 동일한 요청을 구분하는 것이다. 동일요청을 구분할 수 있도록, 요청을 보낼 때 요청값 또는 헤더에 유니크한 값을 함께 보낸다. 그럼 요청을 받은 서버에서 동일한 응답을 돌려줄 수 있다.
이렇게 같은 요청에 대해 같은 응답을 보장할 수 있다면, 요청하는 서버는 Unknown 상황에서 결제무효화(보상 트랜잭션) 요청을 사용해야 하는 경우가 줄어든다. 트랜잭션 보상은 정상적으로 수행된 서비스 트랜잭션을 역으로 되돌리기하는 것이다. 트랜색션 자체를 롤백하는 것이 아닌, 비즈니스 로직상으로 롤백같은 과정을 밟는 것이다. 아무튼 멱등성이 보장될 경우 트랜잭션 보상의 경우가 줄어드는 이유는, 여러 요청중 한번만 성공해도 계속 Success 응답을 보내주는 것을 보장하므로 취소해야 할 경우가 줄어들기 때문이다.
'Database' 카테고리의 다른 글
DB 데이터 변경 감지 방법 | 폴링 or 이벤트 (2) | 2024.11.29 |
---|---|
[상속관계 매핑] 싱글 테이블 전략을 선택한 이유 (0) | 2024.05.16 |
[MySQL + JPA] field 'mark_id' doesn't have a default value | 그리고 auto increment (0) | 2024.05.11 |
컬럼 수 적절성에 대한 고민 및 테이블 분할 (0) | 2024.04.15 |
Docker Mysql Container의 sql 파일 꾸준히 백업: crontab 사용 (0) | 2024.01.04 |