결제 도메인에서의 멱등성 보장

결제 도메인에서의 멱등성 보장

결제 도메인과 같이 일관성이 중요하면서도 PG사에 통신이 들어가는 등 일련된 비즈니스 로직을 하나의 트랜잭션에 담기 어려운 경우 멱등성 처리가 굉장히 중요해집니다. 일관성과 안전성을 챙기다 보면 자연스럽게 재시도 혹은 중복 요청 발생에 대한 위험성이 같이 늘어가기 때문이죠.

멱등성(Idempotency)은 동일한 연산을 여러 번 적용해도 그 결과가 최초 한 번 적용했을 때와 달라지지 않는 성질을 뜻합니다.

가령 유저가 PG 통신을 통한 결제 이후 결제 승인 API를 연속적으로 호출해도, 서버 내 결제 승인에 관한 처리는 한번 호출한 것과 동일한 결과를 불러온다면 멱등하다 볼 수 있습니다.

결제 도메인 내 여러 서비스 로직들에서 멱등성이 필요한 곳과 보장하는 방법을 살펴보겠습니다.

결제 승인 / 환불 처리 시 멱등성 보장

결제 승인은 유저가 PG사와 통신하여 실제 결제를 완료한 후 우리 서버 측에 상품 결제 처리를 요청하는 일련의 과정을 뜻합니다.

이 과정에서 네트워크 불안정, 유저의 새로고침 등으로 인해 동일한 승인 요청이 여러 번 들어올 수 있습니다. 이 경우에도 멱등한 응답이 와야 유저가 결제가 중복 처리됐다는 등의 오류 메시지를 보지 않을 수 있습니다.

이를 해결하려면 유저가 결제 승인 요청을 한 후 일정 시간 동안의 멱등성을 보장해 주어야 합니다. 이 때는 아래와 같은 방법을 사용합니다.

  1. REDIS 등을 활용해서 결제 당 고유 key를 통해 락을 걸고, 락이 있는 경우 락이 릴리즈될 때 까지 기다린다.
  2. 실행 중인 로직은 처리 완료 시점의 결과를 담아 캐싱하고 락을 해제한다.
  3. 캐싱된 결과가 있을 경우 비즈니스 로직을 스킵하고 캐시를 리턴한다.

이를 통해 캐시 TTL에 해당하는 기간동안의 응답은 최초의 요청에 대한 값을 그대로 받을 수 있습니다. 또한 TTL 동안 비즈니스 로직은 한번만 실행되도록 보장할 수 있습니다.

캐싱의 TTL을 통해 응답의 멱등함에 대한 기간을 조율합니다. 가령 유저가 최초 승인 API를 성공적으로 호출한 하루 뒤 또 API를 호출하면, 이 때는 멱등하게 결제 성공 응답을 주는게 아니라 오히려 중복 승인 에러를 주는 것이 더 합리적이기 때문입니다.

번외 - 멱등 키를 클라이언트에게 직접 받기

위의 경우 TTL 이후엔 동일 request에 대해서 다른 응답을 주게 설계했습니다. API 자체가 멱등성을 일시적으로만 보장하게 설계되었기 때문입니다.

만약 이렇게가 아니라 동일한 컨디션으로 들어온 요청임을 따로 식별하고 해당 요청에 대해서는 영구적으로 멱등하게 처리하고 싶다면, 멱등 키 자체를 서버가 아니라 클라이언트에서 만들어서 보내면 됩니다.

토스페이먼츠에서는 이 방법을 채택해서 활용하고 있습니다.

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

이벤트 컨슈머 측 멱등성 보장

결제 원천에게 결제의 결과를 전파하는 등의 경우엔, 메시지 큐 특성 상 성공 했는데 실패로 인식 후 로직 재처리 등이 발생할 수 있습니다. 따라서 이벤트에 대한 로직 처리는 단 한번만 일어나도록 멱등성을 보장해야합니다.

이 부분은 https://astor-dev.com/blog/posts/620239513989419008/ 글에서 다루었으니 참고 바랍니다.

멱등성 보장 시 주의할 점 - 순서 보장

이렇게 멱등성이 되면 조심해야 되는 케이스가 생기는데, 서로 다른 토픽에서 들어오는 이벤트가 동일 관심사(자원의 상태 등)를 수정하려 하는 경우에 순서 보장이 안되는 경우, 오히려 결과가 꼬일 수 있습니다.

따라서 유효성 검증을 주의깊게 하시면 좋습니다.

가령 환불 처리됨과 결제 승인이 동시에 메시지 큐에 담긴 상황에서 예약 도메인의 상태 변경에 대해서 보겠습니다.

환불 완료 시 예약은 취소 상태로 변경된다고 가정 하겠습니다. 업데이트 쿼리는 여러번 이행되어도 결과가 같으니 멱등하죠.

결제 완료 시 예약은 활성 상태로 바뀌며 결제 id라는 논리적 fk가 업데이트 된다고 가정하겠습니다. 이 또한 멱등하죠.

다만 환불 완료가 먼저 소비되어 버리면, 이후에 처리된 결제 완료 로직에 따라 최종적으로는 예약은 활성 상태로 남게 됩니다.

이 경우엔 결제 완료 전에 들어온 환불 처리 이벤트를 유효하지 않은 처리로 인지하여 실패 시켜야 합니다. 이후 실패한 이벤트에 대한 재처리 로직을 통해 일정 기간 뒤 요청 처리가 재시도 될 것이고, 결제 완료 전파가 완료된 후 처리되도록 보장할 수 있습니다.


이를 통해 결제 도메인에서 멱등성이 필요한 상황과 멱등성 보장 방식에 대해 알아봤습니다. 허나 동시성 제어와 멱등성 만으로는 여전히 안정적인 시스템을 보장할 수 없습니다. 다음 글에서는 예외 핸들링 및 재시도에 대해 알아보겠습니다.