본문으로 바로가기

36

멱등성

같은 요청이 두 번 도착해도 결과가 한 번과 같게 만드는 일

먼저 보면 좋은 강의: 메시지 큐, 재시도와 백오프

단계별로 보기

스페이스바로 재생/정지, 좌우 화살표로 단계 이동, R로 처음으로 돌아갑니다.

대기 중1 / 4 단계 — 응답이 사라진다

사용자가 결제를 요청했습니다. 서버는 정상적으로 처리했습니다. 그런데 돌아오는 길에 응답이 사라졌습니다. 사용자 화면은 계속 돌고 있습니다. 여기서 핵심을 보세요. 결제는 이미 됐습니다. 그런데 사용자는 모릅니다. 응답을 못 받은 것과 처리가 안 된 것은 밖에서 보면 똑같습니다.

이 강의를 마치면

  • 재시도가 안전하려면 받는 쪽이 먼저 준비되어 있어야 함을 설명한다.
  • '응답을 못 받았다'와 '처리가 안 됐다'가 다른 상황임을 구분한다.
  • 멱등성 키로 중복을 걸러내는 흐름을 상태 변화로 설명한다.
  • 중복 처리를 막는 구조를 직접 만든다.

화면이 멈췄다

결제 버튼을 눌렀습니다. 화면이 빙글빙글 돕니다. 10초가 지났습니다. 응답이 없습니다.

결제가 된 걸까요, 안 된 걸까요?

사용자는 모릅니다. 그리고 서버도 사용자가 아는지 모릅니다. 이 상황에서 가능한 경우는 두 가지입니다.

경우 1: 요청이 서버에 도착도 못 했다      → 결제 안 됨. 다시 해야 한다.
경우 2: 서버가 처리는 했는데 응답이 유실됐다 → 결제 됨. 다시 하면 두 번 된다.

밖에서 보면 이 둘은 완전히 똑같습니다. 구분할 방법이 없습니다. 응답이 없다는 사실 하나만 알 뿐입니다.

"응답을 못 받았다"는 "처리가 안 됐다"가 아닙니다. 이 문장이 이 강의의 전부입니다. 분산 시스템의 가장 근본적인 불확실성입니다.

그래서 재시도가 위험하다

앞 강의에서 재시도는 좋은 습관이라고 했습니다. 백오프와 지터를 붙이면 안전하다고 했습니다. 절반만 맞습니다.

백오프와 지터는 보내는 쪽을 예의 바르게 만듭니다. 부하 문제를 고칩니다. 하지만 중복 문제는 전혀 건드리지 않습니다.

경우 2에서 재시도하면? 결제가 두 번 됩니다. 예의 바르게, 1초 기다렸다가, 무작위로 어긋나게, 두 번 결제합니다.

그리고 이 상황은 흔합니다. 앞 강의들에서 본 것을 떠올려 보세요.

  • 클라이언트가 재시도합니다.
  • 웹훅 보내는 쪽이 재전송합니다.
  • 큐가 "최소 한 번" 배달을 보장합니다. 최소 한 번은 두 번일 수 있다는 뜻입니다.

재시도가 있는 모든 곳에 중복이 있습니다. 그리고 우리는 지금까지 재시도를 여기저기 넣어왔습니다.

어떤 일은 두 번 해도 괜찮다

모든 중복이 문제인 건 아닙니다. 두 부류로 나뉩니다.

요청두 번 하면
"잔액을 알려줘"잔액을 두 번 알려준다. 잔액은 그대로안전
"이름을 '홍길동'으로 바꿔줘"두 번 바꿔도 홍길동. 결과 같음안전
"1만원 입금해줘"2만원이 들어온다위험
"댓글을 달아줘"댓글이 두 개 달린다위험

차이가 보이십니까? "어떤 상태로 만들어라"는 몇 번을 해도 같은 결과입니다. "얼마를 더해라"는 할 때마다 결과가 달라집니다.

이렇게 몇 번을 해도 결과가 한 번 한 것과 같은 성질을 멱등성이라고 합니다. 엘리베이터 버튼과 같습니다. 열 번 눌러도 한 번 누른 것과 같습니다.

위 표의 위쪽 두 개는 원래부터 멱등합니다. 문제는 아래쪽입니다. 그리고 돈이 오가는 중요한 일은 대부분 아래쪽입니다.

고유 번호를 붙인다

해법은 단순합니다. 요청마다 고유 번호를 붙이는 것입니다.

중요한 것은 누가 언제 만드느냐입니다. 번호는 클라이언트가, 재시도하기 전에 만들어야 합니다. 그리고 재시도할 때 같은 번호를 씁니다.

재시도할 때마다 새 번호를 만들면 서버 입장에서는 서로 다른 요청입니다. 아무 소용이 없습니다.

1차 시도: [번호 abc-123] 1만원 입금  → (응답 유실)
재시도  : [번호 abc-123] 1만원 입금  → 서버: "abc-123? 아까 했는데"
                                      → 처리하지 않고 아까 결과를 다시 준다

서버는 처리한 번호를 기록해 둡니다. 같은 번호가 또 오면 처리하지 않고 저장해 둔 결과를 돌려줍니다.

사용자는 성공 응답을 받습니다. 그리고 입금은 한 번만 됐습니다. 사용자는 중복이 걸러졌다는 사실조차 모릅니다. 모르는 것이 정답입니다.

결과를 저장해 두는 이유

"이미 했음"이라고 오류를 주면 안 될까요? 안 됩니다. 사용자는 아직도 결제가 됐는지 모릅니다. 오류를 받으면 또 불안해서 또 누릅니다.

첫 번째 결과를 그대로 다시 줘야 사용자의 불확실성이 끝납니다.

어디에 놔야 하는가

이 검사는 부수 효과가 일어나기 직전에 있어야 합니다. 더 정확히는, 기록하는 일과 검사하는 일이 하나로 묶여야 합니다.

아니면 이런 일이 생깁니다.

워커 A: abc-123 검사 → 없음
워커 B: abc-123 검사 → 없음      (동시에!)
워커 A: 입금 처리
워커 B: 입금 처리                 → 두 번 됐다

검사하고 처리하는 사이에 틈이 있으면 그 틈으로 중복이 지나갑니다. 그래서 실무에서는 이 검사를 데이터베이스의 고유 제약이나 트랜잭션에 맡깁니다. "먼저 쓴 쪽만 성공한다"를 저장소가 보장하게 하는 것입니다.

정직하게

멱등성 키는 영원히 보관할 수 없습니다. 대개 24시간쯤 두고 지웁니다. 그 뒤에 같은 번호가 오면 새 요청으로 처리됩니다. 실무에서는 대개 충분하지만, 완벽한 보장은 아닙니다.

그리고 여기서 자주 오해가 생깁니다. "정확히 한 번(exactly-once) 배달"이라는 말은 네트워크 수준에서는 불가능합니다. 우리가 할 수 있는 것은 "최소 한 번 배달 + 멱등한 처리" 입니다. 결과적으로 정확히 한 번처럼 보이게 만드는 것이지, 배달이 한 번만 일어나게 만드는 것이 아닙니다.

직접 해보세요

아래 과제는 쓰기 요청이 저장소에 동기로 몰려 무너지고 있습니다. 큐를 통해 안전하게 처리하도록 바꾸세요. 큐는 "최소 한 번"을 보장하므로 같은 작업이 두 번 배달될 수 있고, 그래서 처리 쪽이 멱등해야 합니다.

이 강의가 단순화한 것

교육용 모형은 언제나 무언가를 생략합니다. 무엇을 생략했는지 아는 것도 학습의 일부입니다.

전제한 것

  • 이 시뮬레이터의 큐는 배달 수가 접수 수를 넘지 않는지를 검사해 중복 처리 여부를 판정한다. 실제 중복 걸러내기(멱등성 키 조회·저장)는 워커와 저장소 안에서 일어나며 이 모형에서는 그 내부를 그리지 않는다.
  • 멱등성 키는 클라이언트가 재시도 전에 만들어 재시도 때도 같은 값을 쓴다고 가정한다. 이 가정이 깨지면(재시도마다 새 키) 멱등성은 아무 효과가 없다.
  • 키 저장소는 항상 살아 있고 빠르다고 본다. 실제로는 이 저장소 자체가 새로운 단일 장애점이자 병목이 될 수 있다.
  • 키의 보관 기간은 이 모형에서 다루지 않는다. 실제로는 유한하며, 그만큼 보장도 유한하다.

다루지 않은 것

  • 검사와 기록을 하나로 묶는 구체적 방법(고유 제약, 트랜잭션, 조건부 쓰기)
  • 여러 서버가 키 저장소를 공유할 때의 경쟁 조건
  • 키의 만료 정책과 저장 비용
  • 멱등하지 않은 외부 API를 감싸는 방법

더 읽을거리: Stripe — Idempotent Requests · AWS Builders' Library — Making retries safe with idempotent APIs

이제 직접 만들어 보세요

읽어서 아는 것과 만들어서 아는 것은 다릅니다. 컴포넌트를 배치하고 트래픽을 흘려서 실제로 동작하는지 확인해 보세요.

실습 과제 열기

이 강의에 물어보기

하루 5번

이 강의 본문만 근거로 답합니다. 강의에 없는 내용은 지어내지 않습니다.