여러 서비스에서 트랜잭션을 어떻게 보장해줄 수 있을까 (2)
How can transactions be guaranteed across multiple services
이전 글에서 외부 서비스로부터 영향 범위를 최소화하기 위한 방법들에 대해 알아봤다. 이번에는 여러 서비스에서 트랜잭션을 어떻게 보장해 줄 수 있는지에 대해 알아보자.
여러 서비스에서의 트랜잭션
구매자가 어떤 상품을 주문을 했는데 주문은 성공적으로 주문되었지만 상품은 제대로 차감이 되지 않았다면? 상품 서비스에서는 예외가 터져서 처리가 될 것인데 현재 서비스가 분리되어 있기 때문에 주문 쪽은 상품 서비스가 성공을 했는지 실패를 했는지 모른다. 그래서 실패를 한 경우 주문은 성공하고 상품은 차감되지 않아 데이터의 정합성이 맞지 않는 경우가 발생할 수 있다.
해결 방법으로 Saga 패턴 같은 글로벌 트랜잭션을 다루는 방법들을 사용할 수 있다. 하지만, 이번 글에서는 어떤 네트워크 예외적 상황이 있을 수 있고 이를 코드 레벨에서 어떻게 다룰 수 있을지 살펴보자.
Saga 패턴: 마이크로 서비스 간 이벤트를 주고받아 특정 마이크로 서비스에서 작업이 실패하면, 이전까지의 작업이 완료된 마이크로 서비스에 보상 이벤트를 발행함으로써 원자성을 보장하는 패턴
다양한 네트워크 예외와 예외 처리
네트워크 통신을 하다 보면 다음과 같은 예외 상황이 있을 수 있다.
- 같은 요청이 짧은 시간에 두 번 이상 발생
- 네트워크 순서가 뒤집힌 경우(취소 요청 후 결제 요청)
- 각종 타임아웃
- 인프라 문제로 실패
위와 같은 이유들로 다음과 같은 상황이 발생할 수 있다.
- 여러 번 결제 요청 발생
- 결제 취소 요청이 성공 요청보다 먼저 오는 경우
- 결제가 실패했는지 성공했는지 알 수 없는 경우
- 결제가 실패됐는데 기록이 없는 경우
API를 요청하면 성공, 실패, 알 수 없음 같은 3가지 결과를 받을 수 있고 각 상태에 따른 적절한 처리가 필요하다.
성공과 실패는 비교적 명확하다. 성공은 로직 그대로 수행하면 되고 실패하면 그전 상황들을 롤백 하면 된다. 하지만, 여기서 어려운 부분은 바로 알 수 없음인데 성공을 했는지 실패를 했는지 판단하기 어려운 경우이다.
주문을 통해 결제를 하는 서비스가 있다고 생각해 보자. 만약, 주문 서비스에서 결제 요청을 하고 성공적으로 결제가 되었는데 주문 서비스로 통신이 오던 도중 타임아웃이 나면? 혹은 결제에 실패하였는데 타임아웃이 나면? 만약 단순하게 다 실패하는 쪽으로 처리 하게 되면 큰 문제가 발생할 수 있다.
결제 서비스에서 요청을 받아 결제에 성공하고 잔액이 차감되었지만 주문 서비스로 가던 도중 타임아웃이 발생했고 이를 실패하는 쪽으로 처리하기로 했으므로 주문을 취소하게 된다. 이렇게 되면 돈은 계좌에서 차감되었지만 주문은 들어가지 않은 상황이 발생한다.
알 수 없음 같은 상황들은 다음과 같은 후처리를 통해 보정해볼 수 있다.
- 즉시 재요청 시도
- 일정 시간 뒤 재시도
- 결제 취소 요청 (트랜잭션 무효화 요청)
- 무조건 성공 후 뒤처리
근데 이런 후처리 또한 API를 요청하는 것이기 때문에 똑같이 알 수 없음으로 결과가 나올 수도 있다. 그니깐 재시도를 보냈는데 또 알 수 없음으로 와서 재시도의 재시도의 재시도..? 같은 무한 루프가 반복될 수 있다. 카카오페이에서는 다음과 같이 처리하고 있다고 한다.
처음 요청의 예외상황까지는 고객 응답이 나가기 전에 후처리를 진행해보지만, 이것마저 실패가 되면 '알 수 없는 상태'로 저장 후 사용자에게 재시도 안내와 함께 응답을하게 됩니다. 그리고 고객에 의해 재시도하게 되면, 해당 결제건이 알 수 없음으로 저장된 트랜잭션인지 확인하고 후처리를 다시 진행하게 됩니다. 혹여나 이마저도 실패하는 경우가 있다면 다른 장치들로 데이터를 보정하고 있습니다.
한 번까지 후처리를 하고 그 뒤로는 트랜잭션을 종료하는 것 같다. 여기서 궁금한 점이 생겼었다. 만약 결제가 완료된 상태고 한 번 더 알 수 없음으로 오면 트랜잭션이 종료될 텐데 이대로 종료를 해도 되는 건가? 재시도를 보낸다고 하지만 고객이 재시도를 하지 않으면 결제만 빠져나가는 게 아닌가 생각이 들었다. 또한, 재시도를 했을 때 또 실패하면 어떻게 보정해 주는지도 궁금했는데 그 내용은 나와 있지 않았고 검색을 해도 찾을 수 없었다 🥹
그래서 한참 끙끙 앓다가 눈우(feat.토스개발자님..)에게 HELP를 쳤더니 한방에 해결해 주었다. 위와 같이 의문을 던졌더니 추후에 배치로 맞추는 것 같다고 했다. 그때 딱 머릿속에 지나간 영상이 있었는데 해당 영상에서 주문 및 체결 정합성이 틀어질 경우 대사 배치에서 잡히게 되고 이는 별도 오퍼레이션에 의해...
부분이 딱 생각나서 소름 돋았다.
정합성이 틀어지면 추후에 배치에서 잡히게 되고 후보정이 들어간다. 근데 여기서 또 궁금점이 하나 더 생긴 게 만약 추후에 배치로 맞춰서 보상해 준다고 하면 그때 당시에는 고객 계좌에 바로 돈이 들어가지 않아 고객이 당황하지 않을까? 했었는데 가지고 있는 여유금으로 해결할 수 있다고 한다. 일단 여유금으로 해결을 하고 후에 보정을 하는 그런 방식인 것 같다. 그래서 거래 금액이 커서 여유금이 많이 필요하면 배치 시간 간격을 줄인다고 한다.
멱등성 API
위에서 알 수 없는 상황이면 후처리 API를 요청한다고 하였다. 만약에, 주문 서비스에서 결제 서비스에 결제 요청을 하고 성공적으로 결제가 되었는데 타임아웃으로 인해 알 수 없음으로 왔다고 하자. 이때 후처리로 재시도를 하게 된다면 어떻게 될까?
첫 번째 주문일 때 결제가 돼서 금액이 차감되고 한 번 더 재시도를 해서 한 번 더 결제가 되어 금액이 또 차감되게 된다. 즉, 1번의 주문에 2번의 결제가 되어 정합성이 어긋나게 될 수 있는데 이를 멱등성 API로 해결해 볼 수 있다.
멱등성: 동일한 요청을 한 번 보내는 것과 여러 번 연속으로 보내는 것이 같은 값을 유지하는 성질
다음과 같이 API 요청 값에 유니크한 멱등키(idempotency-key)를 추가해서 보내 동일한 요청임을 알려주면 동일한 응답을 받을 수 있다.
curl https://checkout-test.adyen.com/v70/payments \
-H 'x-api-key: YOUR_API_KEY' \
-H 'idempotency-key: YOUR_IDEMPOTENCY_KEY' \
-H 'content-type: application/json' \
-d '{
"merchantAccount": "YOUR_MERCHANT_ACCOUNT",
"reference": "My first Adyen test payment",
"amount": {
"value": 1000,
"currency": "EUR"
},
"paymentMethod": {
"type": "scheme",
"encryptedCardNumber": "test_4111111111111111",
"encryptedExpiryMonth": "test_03",
"encryptedExpiryYear": "test_2030",
"encryptedSecurityCode": "test_737"
}
}'
즉, 하나의 주문키에는 하나의 주문만 생성하도록 약속한 것이다. 이렇게 하게 되면 주문 쪽에서 타임아웃으로 인한 알 수 없음 응답을 받아도 굳이 결제 무효화 같은 불필요한 작업을 하지 않아도 그냥 재요청을 통해 성공 응답을 받을 수 있다. (만약 멱등성이 없는 상태에서 알 수 없음을 받으면 있는지 없는지 모르니 확인 요청을 하거나 결제 무효화 같은 추가적인 요청 필요)
+타임아웃 특성상 짧은 주기로 재요청을 하게 되면 네트워크 지연상황을 더욱 악화 시킬 수 있다. 네트워크 지연으로 인해 더 빈번한 타임아웃이 발생하므로 재시도를 할 때 지수 백오프 전략을 사용하면 더 좋다(1, 2, 4, 8…)
지수 백오프: 허용 가능한 속도를 점진적으로 찾기 위해 일부 프로세스의 속도를 곱셈적으로 감소시키는 알고리즘
FIN.
두 개의 포스팅으로 외부 서비스로부터 영향 범위를 최소화하기위한 방법과 여러 서비스에서 어떻게 트랜잭션을 보장할 수 있을지에 대해 알아보았다.
현업에서는 정말 다양한 상황이 발생하는 것 같고 각자 다양한 방법으로 풀어 나간다는 게 너무 재밌는 거 같다. 해당 주제에 대해 공부하면서 앞으로 성장할 수 있는 키워드를 많이 얻은 것 같고 빨리 적용할 수 있는 날이 왔으면 좋겠다 🙌
참고:
*오타가 있거나 피드백 주실 부분이 있으면 편하게 말씀해 주세요.