최근 스터디에서 '가상면접 사례로 배우는 대규모 시스템 설계 기초 2'를 하고있는데
11장 결제 시스템에서 멱등성이라는 개념이 나와 전에 작업했을때 비슷한 경험이 있어서 정리해볼까한다

가상면접 책에서의 멱등성

책에서는 멱등성이란 최대 한 번 실행을 보장하기 위한 핵심 개념으로 안내하고있다
위키백과에 따르면 “멱등성은 수학 또는 컴퓨터 과학적 연산이 가질 수 있는 한 가지 속성으로, 연산을 여러 번 실행하여도 최초 실행 결과가 그대로 보존되는 특성을 일컫는다”
API 관점에서 보면 멱등성은 클라이언트가 같은 API 호출을 여러 번 반복해도 항상 동일한 결과가 나온다는 뜻이 된다

시나리오 1: 고객이 ‘결제’버튼을 빠르게 두 번 클릭하는 경우

  • 사용자가 ‘결제’를 클릭하면 멱등 키가 HTTP 요청의 일부로 결제 시스템에 전송됨
    - 전자상거래 웹사이트에서 멱등 키는 일반적으로 결제가 이루어지기 직전의 장바구니 ID
  • 결제 시스템은 요청에 포함된 멱등 키를 이전에 받은적이 있기 때문에 두 번째 요청을 재시도로 처리
    - 이런 경우 결제 시스템은 이전 결제 요청의 가장 최근 상태를 반환함
  • 동일한 멱등 키로 동시에 많은 요청을 받으면 결제 서비스는 그 가운데 하나만 처리하고 나머지에 대해서는 429 Too Many Requests 상태 코드를 반환
  • 멱등성을 지원하는 한 가지 방법은 데이터베이스의 고유 키 제약 조건(unique key constraint)을 활용하는 것

시나리오 2: PSP가 결제를 성공적으로 처리했지만 네트워크 오류로 응답이 결제 시스템에 전달되지 못하여, 사용자가 ‘결제' 버튼을 다시 클릭하는 경우

PSP란? 결제 서비스 공급자, 즉 Payment Service Provider

  • 결제 서비스는 PSP에 비중복 난수를 전송하고 PSP는 해당 난수에 대응되는 토큰을 반환
    - 이 난수는 결제 주문을 유일하게 식별하는 구실을 하며, 해당 토큰은 그 난수에 일대일로 대응됨
    - 따라서, 토큰 또한 결제 주문을 유일하게 식별 가능
  • PSP는 이 토큰을 멱등 키로 사용하므로, 이중 결제로 판단하고 종전 실행 결과를 반환

HTTP Method에서의 멱등성

참고 : RFC 7231

MethodSafeIdempotentReference
CONNECTnonoSection 4.3.6
DELETEnoyesSection 4.3.5
GETyesyesSection 4.3.1
HEADyesyesSection 4.3.2
OPTIONSyesyesSection 4.3.7
POSTnonoSection 4.3.3
PUTnoyesSection 4.3.4
TRACEyesyesSection 4.3.8

표에 따르면

  • GET 메소드는 여러 번 호출해도 같은 결과가 돌아오고, 리소스에 변화를 일으키지 않기 때문에 멱등성이 보장됨
  • PUT과 DELETE 메소드는 멱등성을 가지는 것으로 간주되며, 이를 통해 데이터의 생성, 수정, 삭제 작업을 안정적으로 처리할 수 있지만
    - 단, DELETE 메소드 작업시 맨 마지막 row 삭제 등 명확하지 않은 리소스에 대한 삭제는 명등성이 보장되지않는다고 한다
  • POST 메소드는 멱등성을 가지지 않는 경우가 많아, 중복 요청에 대한 처리가 필요

책과의 차이

그렇다면 책에 나온 방법대로 하는것이 맞는가?
나는 아니라고 생각한다
책은 참고일뿐 항상 실제 구현할때와 책과는 조금 차이가 있다
실제 구현은 많이 쓰는 방법으로 구글링해서 정리하였고 시스템마다 각각 좋은 방법이 다를수 있으니 이런 방법도 있었구나 하고 참고하시면 되겠다

구분실제 구현
멱등키 조건유니크한 UUID유니크한 UUIDv4
API 호출시 멱등키의 위치body내 필드에 존재요청 header에 포함 권장
멱등키 UUID 발급 주체클라이언트에서 발급클라이언트에서 발급(권장)하거나 내부서버나 외부의 고유 ID 발급 서비스(예: AWS의 DynamoDB, Redis)를 사용해 멱등 키를 발급하여 응답으로 내려줌
멱등키 UUID 저장멱등키 DB에 별도 저장멱등키 DB에 별도 저장
에러 코드처리 후 그외의 같은 멱등키일 경우
429 Too Many Requests
이전 요청 처리가 아직 진행 중에 같은 멱등키로 새로운 요청이 올 때 409 Conflict
payload가 처음 요청과 다른데 같은 멱등키를 또 사용했을 때
422 Unprocessable Entity

개인적인 경험과 정리

개인적으로 전직장때 비슷한 작업을 한 적있다
어드민에서 저장을 누르면 캐싱이 갱신되는 방식이였는데
저장을 누를때마다 갱신되니 누가 나쁜마음을 먹고 연속적으로 누르거나 따닥을 하게되면
N번 저장되고 N번 캐싱이 같은 내용으로 갱신된다는게 비효율적으로 느껴졌었다.

이걸 고쳐보고자 처음에는 서버에서 동일한 요청인지 payload와 db에 저장되어있는 값 하나하나 체크했었는데
어느순간 이게 많아지면 너무 속도에 영향을 준다는 생각이 들어서
클라이언트에 멱등키 UUID를 발급을 요청했던 경험이 있었다
하지만 클라이언트도 값이 변경될때마다 UUID를 재발급해야되는 로직이 필요했고
이게 클라이언트도 조금 부담인건지 로직이 꼬여 에러가 발생하는 상황이 생겼었다
괜히 나의 욕심으로 클라이언트가 부담이 된 것같아 다시 초반처럼 서버에서 payload를 하나하나 체크하는 것으로 롤백하였던 기억이있다
(에러코드는 어드민이라 운영자가 따닥 하는 실수를 방지하거나 같은 사용자가 같은 내용을 계속 보내는 걸 방지하는게 목적이라 그때 당시에는 이전 요청과 동일할 경우엔 별도로 에러코드로 안뱉고 서버내에서만 저장로직을 안타고 그대로 응답을 내려주었었다)

결제할때의 멱등성 보장은 반드시 해야되는 문제고 작업하다보면 상황에 따라 다른 도메인에서도 멱등성을 보장해야되는 도메인이 있을것이다
멱등성을 보장하기 전 멱등키를 어디서 어떻게 발급하고 서버에서는 어떻게 관리를 해나갈것인가? 에 대해 좀더 고민해보고 구현하는 것이 좋을것같다

참조문헌
알렉스 쉬/산 람, 「가상 면접 사례로 배우는 대규모 시스템 설계 기초 2」, 이병준, 인사이트(insight)
https://docs.tosspayments.com/blog/what-is-idempotency
https://f-lab.kr/insight/understanding-idempotency-and-its-applications
https://f-lab.kr/insight/understanding-idempotency-in-api-design

0개의 댓글