LabHub
배우기 러닝패스 코스

Data Pipelines

Idempotency — The Pipeline Will Certainly Run Again

LabHub 에서 이어서 보기

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

한 줄 요약

파이프라인은 실패하고, 실패하면 다시 돌리며, 그때 결과가 달라지면 데이터가 오염되므로, 재실행 안전성은 선택이 아니라 설계의 전제다.

Flow map: 몇 번을 넣어도 같은 결과가 되도록 · 자연 키 · 내용 해시 · 중복 제거

왜 이게 필요했나

배치가 새벽에 실패했다고 하자. 아침에 다시 돌린다. 그런데 실패 지점이 적재 중간이었다면 일부는 이미 들어가 있다. 그대로 다시 넣으면 중복이 생기고, 통째로 지우고 다시 넣으면 그 사이 들어온 다른 데이터까지 날아간다.

이 상황을 매번 사람이 판단하게 두면 언젠가 실수가 난다. 애초에 몇 번을 넣어도 같은 결과가 되도록 만드는 편이 낫다.

어떻게 동작하나

멱등성의 출발점은 자연 키다. 각 행을 유일하게 식별하는 값이 있어야 "이미 들어온 것"을 알아볼 수 있다. 전표 번호, 주문 번호, 이벤트 ID 같은 것이다. 이 값에 유일 제약을 걸면 데이터베이스가 중복을 대신 막아 준다.

그다음이 upsert 다.

INSERT INTO orders_final (order_ref, amount, status, content_hash)
SELECT ...
ON CONFLICT (order_ref) DO UPDATE
  SET amount = EXCLUDED.amount,
      status = EXCLUDED.status,
      content_hash = EXCLUDED.content_hash,
      updated_at = now()
  WHERE orders_final.content_hash IS DISTINCT FROM EXCLUDED.content_hash;

마지막 WHERE 절이 중요하다. 이것이 없으면 내용이 하나도 안 바뀐 재실행에서도 모든 행의 updated_at 이 갱신된다. 그러면 "무엇이 실제로 바뀌었는가"를 알 수 없게 되고, 하류에서 변경분만 가져가는 증분 소비도 망가진다.

내용 해시를 두면 비교가 간단해진다. 값들을 정해진 순서로 이어 붙여 해시를 계산하고, 그 값이 다를 때만 갱신한다. 컬럼이 많아져도 비교 로직이 한 줄로 유지된다.

중복 제거도 필요하다. 같은 전표가 두 번 적재되었다면 어느 쪽을 남길지 규칙을 정해야 한다. 보통 나중에 들어온 것을 남긴다. 원본에 적재 순서를 나타내는 증가 키가 있으면 전표별로 최대값 행만 고르면 된다.

파티션 단위로 갈아 끼우기

행 단위 upsert 로 안 되는 경우가 있습니다. 원본이 하루치를 통째로 다시 주는 경우, 또는 삭제된 행을 알아낼 방법이 없는 경우입니다. 이때는 덮어쓰기 단위를 파티션으로 잡습니다.

BEGIN;
  -- 1) 새 데이터를 임시 표에 적재한다
  CREATE TEMP TABLE stage_20260906 (LIKE orders_final INCLUDING ALL);
  COPY stage_20260906 FROM ...;

  -- 2) 그날 파티션만 통째로 바꿔 끼운다
  ALTER TABLE orders_final DETACH PARTITION orders_20260906;
  ALTER TABLE orders_final ATTACH PARTITION stage_20260906
        FOR VALUES FROM ('2026-09-06') TO ('2026-09-07');
COMMIT;

핵심은 "지우고 넣는다" 가 아니라 "만들어 두고 바꿔 끼운다" 는 것입니다. 지우고 넣는 사이에는 데이터가 없는 구간이 생기고, 그때 조회한 사람은 빈 결과를 봅니다. 바꿔 끼우기는 트랜잭션 안에서 원자적으로 끝나므로 그 구간이 없습니다.

이 방식은 재실행에도 안전합니다. 몇 번을 돌려도 그 파티션의 최종 상태는 마지막 실행 결과 하나입니다.

안전하지 않은 연산을 알아보는 법

멱등하지 않은 연산에는 공통점이 있습니다. 현재 값을 읽어서 그것을 바탕으로 쓰는 것 입니다.

안전하지 않다 안전하다
UPDATE t SET n = n + 1 UPDATE t SET n = <계산된 절대값>
INSERT (제약 없음) INSERT ... ON CONFLICT DO UPDATE
파일에 append 파일을 새로 쓰고 rename
큐에 메시지 발행 발행하되 소비자가 중복을 거른다
외부 API 호출(결제 등) 멱등 키를 함께 보낸다

마지막 줄이 중요합니다. 파이프라인이 외부 시스템을 부를 때는 그쪽의 멱등성에 기대야 합니다. 대부분의 결제 API 는 Idempotency-Key 헤더를 받습니다. 같은 키로 두 번 부르면 두 번째는 첫 번째의 결과를 그대로 돌려줍니다. 키는 재시도해도 같아야 하므로 요청마다 새로 만들면 안 되고, 원본 데이터에서 결정적으로 유도해야 합니다(예: sha256(주문번호 + 금액)).

워터마크와 재처리 범위

증분 적재에서는 "어디까지 처리했는가" 를 기록합니다. 이 값을 처리한 뒤에 갱신 하는 것이 원칙입니다. 먼저 갱신하면 중간에 죽었을 때 그 구간을 영영 건너뜁니다.

읽기 → 변환 → 적재 → (성공 시에만) 워터마크 갱신

그리고 워터마크에 여유를 둡니다. 원본이 이벤트 시각 기준으로 늦게 도착하는 데이터를 허용한다면(late arrival), 워터마크를 마지막 시각이 아니라 마지막 시각 − 유예 기간 으로 잡습니다. 그러면 겹치는 구간을 매번 다시 읽게 되는데, 멱등하게 만들어 두었으니 문제가 되지 않습니다. 멱등성이 있으면 겹쳐 읽기가 공짜가 됩니다.

현장에서 만나는 모습

실행 로그를 남기는 것이 큰 도움이 된다. 실행마다 삽입 건수와 갱신 건수를 기록해 두면, 아무것도 바뀌지 않은 실행은 둘 다 0 으로 남고 늦게 도착한 변경이 반영된 실행은 갱신 건수가 올라간다. 이 두 숫자만 봐도 파이프라인이 정상인지, 원천에 이상이 있는지 판단할 수 있다.

그리고 재시도 코드 안에는 데이터베이스 작업만 넣어야 한다. 메일 발송이나 외부 API 호출이 섞여 있으면 재시도할 때마다 그 부수 효과가 반복된다. 외부 호출이 필요하다면 그쪽도 멱등성 키를 받는 방식이어야 한다.

다음 실습에서 할 것

자연 키에 기본 키를 걸고, 중복 전표를 제거하고, 내용 해시로 변경 여부를 판단하는 upsert 를 만든다. 그리고 아무것도 바뀌지 않은 재실행과 한 건만 바뀐 재실행이 각각 어떻게 기록되는지 확인한다.