결제 도메인의 재시도 상황 및 전략, 멱등성 연계

결제 도메인의 재시도 상황 및 전략, 멱등성 연계

결제 과정은 금전과 관련된 일이기 때문에 반드시 한번 이상의 처리를 보장해야합니다. 이를 보장하려면 필연적으로 재시도가 생겨납니다.

네트워크 끊김 등 모든 상황에서도 한번 이상 올바르게 처리되도록 하기 위해서는 트랜잭션으로 보장받지 못하는 모든 연결부에서 발생하는 케이스에서 실패 시 재시도가 되어야 하기 때문입니다.

따라서 똑같은 로직이 여러번 발생하는 경우 생기는 문제를 막기 위해 멱등성을 보장했습니다. 시리즈의 전 포스트를 참고해주세요.

이번 글에선 결제 도메인에서 재시도 처리를 해야하는 케이스들을 살펴보고, 함께 활용할 수 있는 재시도 전략들도 살펴보겠습니다.

재시도 전략

고정 간격 재시도 (Fixed Interval Retry)

제일 기본적이고 직관적인 재시도 방식입니다. 특정 고정 간격을 두고 실패 시 해당 간격 후 재시도를 실행하는 방식입니다.

예를 들어, 실패할 때마다 5초 후에 다시 시도하도록 설정하는 방식입니다. 단순한 로직으로 구현이 쉽지만, 여러 요청이 동시에 실패할 경우 같은 시점에 다시 몰려 시스템에 부하를 줄 수 있습니다. 실패의 원인이 치명적인 에러가 아니거나 의도한 경우, 혹은 빠른 응답이 중요한 경우 주로 사용됩니다.

지수 백오프 (Exponential Backoff)

재시도 실패 시 다음 재시도까지의 대기 시간을 점진적으로 늘려나가는 방식입니다. 첫 번째 재시도 시 1초, 두 번째는 2초, 세 번째는 4초와 같이 대기 시간이 기하급수적으로 증가합니다.

결제 시스템에 일시적으로 부하가 걸렸을 때, 재시도 요청이 한꺼번에 몰리는 것을 효과적으로 방지합니다. 실패가 생길 수 있는 원인이 시스템 다운 등 예측 불가능한 상황인 경우, 복구까지 기다릴 수 있기에 유용합니다.

다만 client와 동기적으로 주고받는 API 단에서 사용하기엔 응답을 받는 데 드는 시간이 지수적으로 늘어나서 UX를 해칩니다.

분산 재시도 (Jitter)

지수 백오프고정 간격 재시도 등에 임의의 지연 시간을 추가하는 방식입니다. 예를 들어, 2초 후에 재시도해야 할 경우, 1.8초에서 2.2초 사이의 무작위 시간으로 재시도합니다. 이는 여러 요청이 정확히 같은 시간에 재시도되어 다시 충돌하는 ‘재시도 폭풍’을 방지하는 데 효과적입니다.

실패 로그 / 데드 레터 큐

만약 임계 까지 재처리하여도 성공하지 못해 영구적으로 실패로 처리되거나, 내부 DB 등 영속성 영역에 문제가 생겼을 때는 SW 상에선 즉각적으로 자동화된 대응이 불가능 합니다.

이런 작업이 발생했을 경우, 반드시 다원화된 창구를 통해 이 사항을 기록하여야 합니다.

API 단에서 생기는 쓰기 작업은 DB에 에러 테이블을 만들거나, slack에 결제 실패 스레드를 만들고 로그를 쌓는 등을 통해 처리할 수 있습니다.

데드 레터 큐는 메시지 브로커 재처리 쪽에서 주로 쓰이는 개념으로 실패한 메시지만 모아놓는 토픽을 따로 개설하여 재처리 실패시 해당 토픽에 적재하는 방식으로 사용됩니다. 아래 글에서 다뤘는데 흥미 있으시면 참고 바랍니다.

Kafka Consumer - 오프셋 커밋과 재처리 전략, Dead Letter Queue(DLQ)

거래 생성 시 재시도 - 낙관적 락

거래 생성이 만약 상품의 재고를 관심사로 두고 있는 경우, 쓰기 정합성 보장 및 조회 시의 오버헤드를 줄이기 위해 낙관적 락을 많이 사용합니다. 이 케이스에선 결제가 생성되는 시점에 재고에 대한 변동이 있을 경우 실패 처리가 아니라 동일 로직을 재시도해주어야 합니다.

재시도 전략을 신중히 고려해야합니다. 만약 한 칼럼에 일 순간에 수많은 거래 생성 요청이 생기는 경우 (ex: 티켓팅 서비스) 바로 재시도 시기는 경우 충돌 빈도가 늘어나고 시스템에 부하가 갈 수 있습니다.

제가 권장하는 방식은 고정 간격 재시도지터 추가입니다.

낙관적 락 자체는 굉장히 빈번하게 실패할 수 있는 상황이며 서버 다운같은 상황으로 실패되는 것이 아니기에 오래 기다릴 필요가 없습니다.

다만 어느정도의 지터를 두어 동시에 요청이 쏟아 진 경우 적절히 분산되게 처리합니다.

거래 승인 시 재시도 - 지수적 백오프, 실패 로그 적재

결제 승인 파이프라인을 도식화 했습니다. 보통 실패할 수 있는 상황은 아래와 같습니다.

  • 네트워크 불안정
  • 외부 서비스의 에러
    • 유저 귀책 에러 (미지원 결제 수단, 잔액 부족 등)
    • 의도된 시스템 에러 (은행 점검 등)
    • 의도되지 않은 시스템 에러 (서버 장애 등)
  • DB 연결 실패

외부 시비스 (PG사)에서 정의해둔 에러들을 그대로 처리할 게 아니라, 유저에게 바로 실패로 돌려줄 에러재시도해야할 에러를 정의해두어야 합니다.

또한 제일 문제가 생길 수 있는 부분은 승인 API 호출 이후 DB 커넥션 문제로 이 경우 유저 돈은 빠져나갔지만 시스템은 결제 사실을 모르게 됩니다.

DB가 터지는 등의 케이스를 대비해서 아무리 재시도해도 문제가 생길 경우 반드시 다른 창구에 로그를 남겨야 합니다. Slack 알림, DB 에러 테이블, 에러용 메시지 토픽 등을 활용할 수 있습니다.

제일 중요한건 멱등성 보장입니다.

지난 글에서 살펴본 멱등성 보장과 재시도가 시너지를 불러

  • 유저가 요청 처리 중 새로고침을 해도
  • 유저가 요청이 완료되고 뒤로가기를 통해 웹훅 url에 접속해도
  • pg사에 일시적 장애가 발생해도

결제 요청을 안정적으로 처리할 수 있습니다.

번외 - 메시지큐 활용

만약 결제 승인 요청 과정 도중에 서버가 터지면 어떻게 될까요? 이 경우엔 어떤 대응도 받지 못할 것입니다. 가장 유효한 방법은 승인 로직 자체를 메시지큐로 처리하는 것 입니다.

메시지큐를 활용하여, 온전히 로직을 수행했을 때만 커밋하게 한다면 서버가 터지더라도 별도의 워커가 작업을 마저 수행할 수 있습니다. 다만 이를 이용한다면 Kafka 급의 고가용성과 안정성을 챙긴 메시지 큐 운용이 필수적이겠죠.

환불 완료 시 재시도

환불 API 역시 결제 승인과 동일하게 처리하실 수 있습니다.

다만 여기서 중요한 점은 환불은 재시도 시 두번 처리가 될 경우 문제가 생길 수 있다는 점 입니다.

반액 부분 환불을 재시도를 통해 2번 처리한 경우 전액 환불이 생길 수 있습니다.

따라서 환불 API를 재시도 로직을 담을 경우 요청 처리 부분에서 PG API 호출 자체가 반드시 멱등하게 처리되어야 합니다. 이 부분은 TossPayments 같은 PG사에서 대부분 요청 당 멱등 키를 인자로 제공해줍니다.

https://docs.tosspayments.com/blog/what-is-idempotency

환불 처리에 활용되는 고유 키 값을 활용해서 PG사에 멱등키로 던져줍니다. 이 경우 재처리 시 외부 통신부 마저 안정적으로 처리할 수 있습니다.

멱등키는 승인부에서도 쓸 수 있으니 활용하면 좋을 것 같네요.


결제 요청 파이프라인에서 주요하게 엔지니어링 해야 할 부분들은 대부분 다 살펴본 것 같습니다. 추가적으로 오늘 다룬 내용처럼 메시지큐를 활용해서 개별 요청 자체도 고가용성을 지닌채 재시도 가능하게 한다면 좋을 것 같네요.

일단 잠시 이정도로 마무리하고 또 추가적으로 다룰 내용이 떠오르면 시리즈 이어 보겠습니다.