LabHub
배우기 러닝패스 코스

System Integration (EAI)

Defending Against Duplicate Delivery With Idempotency Keys

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

멱등키를 설계하고 DB 제약으로 중복을 막고, 동시 실행에서도 안전하게 만들고, 저장-후-응답까지 구현해 재전송이 안전한 수신측을 만들 수 있게 됩니다.

왜 중요한가

비동기 연동과 재시도가 있는 세상에서 중복은 예외가 아니라 기본값입니다. 그런데 중복 방어를 애플리케이션의 조회 후 없으면 삽입 으로 구현하면 동시에 두 건이 들어오는 순간 뚫립니다. 조회와 삽입 사이에 틈이 있기 때문입니다. DB 제약은 그 틈을 없애고, 애플리케이션에 버그가 있어도 뚫리지 않습니다. 그리고 한 걸음 더 나가 최초 응답을 저장해 두고 중복 요청에 그대로 돌려주면, 송신측이 타임아웃 후 마음 놓고 재전송할 수 있게 됩니다. 결제 API 들이 Idempotency-Key 를 쓰는 이유가 이것입니다.

단계

  1. /root/i/idem.db (sqlite)에 inbox_log 테이블을 만듭니다. 컬럼은 msg_id, biz_key, status, response, created_at 이고, msg_idPRIMARY KEY 또는 UNIQUE 제약이 있어야 합니다.
  2. /root/i/key.md 를 작성합니다. 아래 세 가지가 본문에 있어야 합니다.
    • 이 인터페이스의 멱등키를 무엇으로 잡을지와 그 이유
    • order_no 하나만으로 잡으면 안 되는 이유
    • 멱등 이력의 보관 기간과 그 근거 멱등키, 보관, 수정 세 단어가 모두 등장해야 합니다.
  3. /root/i/apply.sh 를 만듭니다. 인자 두 개(DB파일 JSON파일)를 받아
    • 신규면 적재하고 첫 줄에 applied 출력, 종료코드 0
    • 이미 있으면 아무것도 하지 않고 첫 줄에 duplicate 출력, 종료코드 0 조회 후 삽입 방식이 아니라 제약을 이용한 방식이어야 합니다.
  4. /opt/lab/fixtures/eai/idem/messages/ 의 모든 JSON 을 apply.sh 로 적재합니다.
    • inbox_log 행 수가 고유 msg_id 수와 같아야 합니다
    • 중복으로 무시된 msg_id/root/i/dup.txt 에 오름차순으로 저장합니다
  5. /root/i/race.sh 를 만듭니다. 인자 두 개(DB파일 JSON파일)를 받아 같은 메시지를 동시에 10번 적재 시도하고, 마지막 줄에 rows=<해당 msg_id 의 행 수> 를 출력합니다. 값은 1 이어야 합니다. 실행 결과를 /root/i/race.txt 에 저장하세요. (sqlite 잠금 경합에 대비해 PRAGMA busy_timeout 을 설정하세요.)
  6. /root/i/purge.sh 를 만듭니다. 인자 두 개(DB파일 보관일수)를 받아 created_at 이 보관일수보다 오래된 행만 삭제하고, 마지막 줄에 deleted=<건수> remain=<건수> 를 출력합니다.
  7. apply.sh 를 확장해 저장-후-응답을 구현합니다.
    • 신규 처리 시 응답 JSON 을 response 컬럼에 저장하고, applied 다음 줄(둘째 줄)에 그 응답을 출력합니다
    • 중복이면 duplicate 다음 줄(둘째 줄)에 저장된 최초 응답을 그대로 출력합니다 같은 메시지를 두 번 적용했을 때 두 번째 출력의 2번째 줄이 첫 번째의 응답과 같아야 합니다. 확인 결과를 /root/i/replay.txt 에 저장하세요.
  8. /root/i/report.md 를 작성합니다. 중복이 발생하는 네 가지 경로를 각각 한 항목으로 적고, 각 경로마다 방어 지점을 함께 적습니다. 재시도, , 수동, 배치 네 단어가 모두 등장해야 합니다.

참고

멱등 이력 테이블

/root/i/idem.db (sqlite)에 inbox_log 테이블을 만듭니다. 컬럼은 msg_id, biz_key, status, response, created_at 이고, msg_idPRIMARY KEY 또는 UNIQUE 제약이 있어야 합니다.

중복 방어의 마지막 방어선은 애플리케이션이 아니라 DB 제약입니다. 어떤 컬럼에 제약을 걸어야 할지 먼저 정하세요.

멱등키 설계 문서

/root/i/key.md 를 작성합니다. 아래 세 가지가 본문에 있어야 합니다.

업무 키만으로는 부족한 경우가 많습니다. 같은 주문에 대한 수정 전문이 있을 수 있고, 전문번호가 일 단위로 순환할 수도 있습니다.

적재 스크립트

/root/i/apply.sh 를 만듭니다. 인자 두 개(DB파일 JSON파일)를 받아

이미 처리한 키면 아무것도 하지 않고 그렇게 알려야 합니다. 조회 후 삽입은 동시 실행에서 뚫리니, 제약을 활용하는 방식을 쓰세요.

일괄 적재와 중복 집계

/opt/lab/fixtures/eai/idem/messages/ 의 모든 JSON 을 apply.sh 로 적재합니다.

적재 결과에서 신규와 중복을 구분해 세어야 합니다. 중복된 키 목록을 남겨 두면 나중에 원인 분석에 씁니다.

동시 실행 방어

/root/i/race.sh 를 만듭니다. 인자 두 개(DB파일 JSON파일)를 받아 같은 메시지를 동시에 10번 적재 시도하고, 마지막 줄에 rows=<해당 msg_id 의 행 수> 를 출력합니다. 값은 1 이어야 합니다. 실행 결과를 /root/i/race.txt 에 저장하세요. (sqlite 잠금 경합에 대비해 PRAGMA busy_timeout 을 설정하세요.)

같은 메시지를 병렬로 여러 번 넣어도 한 건이어야 합니다. sqlite 는 잠금 경합이 잦으니 대기 시간을 설정해야 합니다.

보관 기간 정리

/root/i/purge.sh 를 만듭니다. 인자 두 개(DB파일 보관일수)를 받아 created_at 이 보관일수보다 오래된 행만 삭제하고, 마지막 줄에 deleted=<건수> remain=<건수> 를 출력합니다.

정리하는 순간 그 구간의 중복 방어가 사라집니다. 기간 조건으로만 지워야 하고, 건수 기준으로 지우면 최근 것을 지울 수 있습니다.

저장-후-응답

apply.sh 를 확장해 저장-후-응답을 구현합니다.

중복 요청에 최초 응답을 그대로 돌려주면 송신측 입장에서 재전송이 완전히 안전해집니다. 결제 API 들이 쓰는 방식입니다.

중복 발생 경로 정리

/root/i/report.md 를 작성합니다. 중복이 발생하는 네 가지 경로를 각각 한 항목으로 적고, 각 경로마다 방어 지점을 함께 적습니다. 재시도, , 수동, 배치 네 단어가 모두 등장해야 합니다.

중복은 네 가지 경로로 옵니다. 각 경로에서 어느 지점이 방어선인지 함께 적으세요.