−Malcolm Featonby의 《Making retries safe with idempotent APIs》 PDF 4쪽·본문 3쪽 도해다. 응답을 잃은 호출자가 같은 식별자를 유지해 재요청하는 API 계약을 보여준다. 이 그림은 요청 중복 처리의 예이며, 대수에서 출발한 멱등성의 모든 쓰임을 나타내지는 않는다. © Amazon Web Services.
이 글이 요청 본문을 해시하는 방식보다 호출자가 정한 식별자를 중요하게 다루는 이유도 여기에 있다. 같은 상품을 같은 수량으로 두 번 주문할 수 있으니, 본문이 같다는 사실만으로 같은 의도라고 결정하기 어렵다. 호출자와 요청 키의 범위, 같은 키에 다른 내용을 보냈을 때의 처리, 기록 보존 기간을 정해야 같은 작업의 뜻이 유지된다. AWS 원문.
여기에는 두 선택이 있다. 연산 자체를 원하는 상태를 지정하는 형태로 만들 수도 있고, 추가 연산을 유지하면서 같은 요청의 재처리를 기록으로 제어할 수도 있다. 후자는 기록과 실제 효과를 원자적으로 다뤄야 하며, 외부 결제처럼 다른 시스템에서 일어나는 효과까지 로컬 DB 트랜잭션 하나로 묶을 수는 없다. 응답을 재사용할지까지 정하는 것도 이 API의 계약에 속한다.
멱등성은 효과를 반복해도 유지되는 성질이고, 중복 제거는 이미 본 작업을 알아내는 기법이다. 처리 기록이 유지되는 범위에서는 후자를 이용해 전자를 제공할 수 있지만, 기록이 사라진 뒤의 재요청이나 다른 시스템의 효과에는 별도의 근거가 필요하다. '정확히 한 번'이라는 표현을 쓸 때도 전달, 실행, 업무 효과 중 무엇을 보장하는지 밝혀야 한다.
같은 이름 아래 남아 있는 구분
문맥 | 반복하는 대상 | 같다고 보는 결과
대수 | 같은 원소의 곱 | 원소 자체
함수 합성 | 앞선 함수의 결과에 같은 함수를 적용 | 함수값
상태를 바꾸는 프로그램 | 같은 명령을 같은 조건에서 재적용 | 관찰하기로 한 상태
HTTP | 같은 요청의 재전송 | 요청이 의도한 서버의 효과
요청 키를 쓰는 API | 같은 작업 식별자의 재처리 | 계약에서 정한 업무 효과와 재응답
수학에서의 법칙이 통신과 업무 처리에서 쓰이면서, 같음을 판단하는 범위와 그 성질을 제공하는 방법이 함께 중요해졌다. 반복할 작업과 관찰할 효과의 범위를 밝혀야 어떤 보장을 하는지도 분명해진다. 주문 중복 방지는 그 가운데 한 적용 사례이고, 요청 키와 결과 저장은 그 성질을 제공하기 위해 선택한 구현이다.
+Malcolm Featonby의 《Making retries safe with idempotent APIs》에 실린 도해다. 호출자는 첫 응답을 받지 못하자 같은 식별자로 다시 요청하고 서비스는 StateStore에서 처리 상태를 조회한다.
사용자가 같은 상품을 같은 수량으로 두 번 주문할 수도 있으므로 서버가 요청 본문만 해시해 두 주문을 하나로 합치면 원래 의도를 잃는다. 그래서 재시도를 구별할 때는 호출자가 부여한 식별자(요청 키)를 쓴다. 같은 작업을 다시 보낼 때는 같은 키를 쓰고 별개의 주문에는 새 키를 쓴다. 서버는 호출자와 키의 범위를 정하고 같은 키에 다른 내용을 보냈을 때의 처리와 기록 보존 기간도 함께 정한다. AWS 원문.
아래는 한 프로세스의 한 스레드에서 순서대로 호출하는 Python 3 예제다. owner에는 서버가 인증한 호출자의 식별자를, key와 item에는 이미 검증한 문자열을 넣는다. 사전의 항목 하나에 주문 내용과 번호를 함께 보관하고 실행하는 동안 기록을 삭제하지 않는다.
requests = {}
def create_order(owner, key, item):
identity = (owner, key)
if identity in requests:
previous_item, order_id = requests[identity]
if previous_item != item:
raise ValueError("KEY_REUSED_WITH_DIFFERENT_REQUEST")
return order_id
order_id = len(requests) + 1
requests[identity] = (item, order_id)
return order_id
first = create_order("alice", "request-1", "book")
retry = create_order("alice", "request-1", "book")
another = create_order("alice", "request-2", "book")
print(first, retry, another) # 1 1 2
같은 호출자가 같은 키로 같은 상품을 보내면 앞서 만든 주문 번호를 돌려주고 새 키를 쓰면 같은 상품이라도 별개의 주문으로 만든다. 같은 키로 상품만 바꿔 create_order("alice", "request-1", "pen")을 호출하면 ValueError를 낸다. 다른 호출자의 키는 (owner, key) 쌍으로 구별한다.
이 예제의 저장소는 인메모리 사전이라 프로세스가 끝나면 주문과 재처리 기록도 함께 사라진다. 실제 서버에서는 여러 요청의 동시 실행과 장애 뒤 복구까지 처리해야 하므로 원자적 확정 문제가 뒤따른다.
서버가 처리 여부를 먼저 조회한 뒤 주문과 키를 따로 저장하면 두 요청이 동시에 '기록 없음'을 보고 각각 주문을 만들 수 있다. 주문만 저장한 직후 서버가 멈추는 경우도 있다. 그래서 요청을 식별하는 기록과 주문 효과를 하나의 원자적인 처리로 확정해야 한다. 어떤 요청이 아직 실행 중인지, 실패 뒤 다시 실행할 수 있는지, 성공 결과를 어떻게 다시 돌려줄지는 이 기록의 상태와 API 계약으로 표현한다.
요청 키는 보존하는 동안 같은 작업을 찾는 데 쓰인다. 기록을 지운 뒤 같은 키가 도착하면 서버는 새 요청과 구별할 정보를 잃을 수 있고 호출자가 키를 바꾸면 별개 요청으로 보일 수도 있다. 보존 기간과 호출자의 키 사용 규칙까지 알아야 보장 범위를 정할 수 있다.
로컬 DB 트랜잭션은 그 DB의 주문과 처리 기록을 함께 확정한다. 외부 결제의 재처리는 결제 서비스가 제공하는 식별자와 계약으로 다룬다. 멱등성은 반복 적용한 효과의 성질이고 중복 제거는 같은 작업을 식별하는 기법이므로 '정확히 한 번'이라고 표현할 때도 메시지 전달과 코드 실행, 최종 업무 효과 중 어느 것을 보장하는지 밝혀야 한다.
복제본의 상태를 합칠 때도 같은 법칙을 쓴다
2011년 Shapiro·Preguiça·Baquero·Zawirski의 《Conflict-free Replicated Data Types》는 상태를 보내 합치는 방식과 연산을 보내 적용하는 방식을 구분했다. 상태 기반 모델에서는 병합의 교환·결합·멱등 법칙과 상태 갱신의 단조성을 이용한다. 중복 전송에 대응하는 데 멱등성이 쓰이는 또 다른 계보다. 논문 §2.1–2.4.
[이미지]
(원문 출처)
논문 PDF 3쪽의 그림 1·2다. 왼쪽에서는 복제본이 상태를 전달하고 받은 쪽이 병합한다. 오른쪽에서는 복제본이 연산을 전달하고 받은 쪽이 적용한다. 아래의 집합 예제는 왼쪽 방식에서 쓸 수 있는 병합 연산을 보여준다.
def merge(left, right):
return left | right
local = {"a"}
remote = {"b"}
combined = merge(local, remote)
assert merge(combined, remote) == combined
assert merge(local, remote) == merge(remote, local)
print(sorted(combined)) # ['a', 'b']
Python 3의 집합 합집합이다. 같은 원격 상태를 다시 합쳐도 결과는 유지된다. 합집합의 교환법칙은 순서를 바꾸는 경우를, 결합법칙은 여러 상태를 묶는 방법을 다룬다. 항목을 추가하며 상태를 키우고 각 복제본이 갱신을 끝내 전달한다는 조건도 함께 있어야 복제본의 수렴을 설명할 수 있다.
같은 논문의 연산 기반 모델은 인과 순서와 정확히 한 번의 전달을 가정한다. 가령 '1 증가'라는 연산 메시지를 두 번 적용하면 값은 두 번 증가한다. 상태 병합의 중복 허용 조건과 연산 메시지의 전달 조건을 구별해서 읽어야 한다. 논문 §2.3–2.4.
문맥마다 무엇을 비교하는가
문맥 | 반복하는 대상 | 비교하는 결과
대수 | 같은 원소의 곱 | 원소 자체
함수 합성 | 앞선 결과에 같은 함수를 적용 | 함수값
상태를 바꾸는 프로그램 | 같은 명령의 재적용 | 관찰하기로 한 상태
HTTP | 같은 요청의 재전송 | 요청이 의도한 서버의 효과
요청 키를 쓰는 API | 같은 작업 식별자의 재처리 | 계약에서 정한 업무 효과와 재응답
상태 기반 복제 | 같은 상태의 재병합 | 병합한 상태
수학자는 연산의 법칙을, 클라이언트와 서버는 재시도 뒤의 효과를, 복제 시스템의 설계자는 반복 병합 뒤의 상태를 비교한다. 이 범위를 정해두면 요청 키 저장, 상태 지정, 집합 병합이 각각 어떤 방식으로 멱등성을 제공하는지 읽을 수 있다.