이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • backfill은 단순 대량 SQL이 아니라 어떤 운영 작업으로 설계해야 하는가?
  • backfill range, batch size, sleep, checkpoint, stop condition은 각각 어떤 위험을 줄이는가?
  • 운영 DB와 downstream mart에 부담을 주지 않기 위해 어떤 속도 제한과 중단 조건이 필요한가?
  • backfill 완료를 처리 건수가 아니라 어떤 business 검증으로 확인해야 하는가?

개요

backfill은 과거에 누락되었거나 잘못 계산된 데이터를 다시 채우는 운영 작업이다.

단순히 큰 SQL 하나를 실행하는 일이 아니다.

범위를 정하고, 작은 batch로 나누고, 진행 위치를 남기고, 운영 부하를 보며 속도를 조절해야 한다.

backfill은 보통 사용자 요청 경로 밖에서 실행되지만, 운영 DB와 downstream 시스템의 자원을 사용한다.

그래서 속도 제한과 중단 조건이 설계의 핵심이다.

Range 선정

backfill은 입력 범위를 명시해야 한다.

target: order_summary
source: orders
range_type: created_at
range: 2026-04-01T00:00:00Z..2026-06-30T23:59:59Z
reason: 2026-Q2 summary bug

range가 없으면 재시작과 검증이 어렵다.

id range, timestamp range, partition date 중 무엇을 기준으로 자를지 정한다.

운영 DB에서 timestamp index가 없다면 timestamp range backfill은 위험할 수 있다.

range 기준은 query plan과 함께 확인한다.

Batch Size

batch size는 처리량과 장애 범위를 함께 결정한다.

크면 빠르지만 lock, memory, transaction 시간이 커진다.

작으면 안전하지만 시간이 오래 걸린다.

batch_size: 1000
commit_unit: batch
checkpoint_unit: last_order_id

한 batch가 실패하면 그 batch만 다시 처리할 수 있어야 한다.

전체 range를 하나의 transaction으로 묶으면 rollback 비용과 lock 시간이 커진다.

속도 제한

backfill은 운영 traffic과 자원을 나눠 쓴다.

sleep, rate limit, concurrency limit를 둔다.

sleep_ms_between_batches: 300
max_concurrency: 2
max_rows_per_minute: 100000

속도 제한은 느리게 만들기 위한 장치가 아니다.

서비스를 살려 둔 채 보정하기 위한 장치다.

replica lag, API latency, DB CPU, lock wait를 보며 동적으로 낮출 수 있어야 한다.

Stop Condition

중단 조건은 시작 조건만큼 중요하다.

stop if:
  api_p95 > 800ms for 5m
  replica_lag > 30s
  db_cpu > 80%
  lock_wait_count > threshold
  error_rate > 1%

중단 조건이 없으면 backfill이 장애를 키울 수 있다.

중단되더라도 checkpoint가 있으면 나중에 이어서 실행할 수 있다.

운영자는 “멈춰도 안전한가”를 먼저 확인해야 한다.

Checkpoint

checkpoint는 마지막 성공 위치다.

CREATE TABLE backfill_runs (
    run_id varchar(80) PRIMARY KEY,
    target_name varchar(80) NOT NULL,
    range_start varchar(120) NOT NULL,
    range_end varchar(120) NOT NULL,
    last_success_key varchar(120),
    status varchar(20) NOT NULL,
    updated_at timestamp NOT NULL
);

checkpoint는 batch commit 이후 갱신한다.

검증 실패 상태까지 표현하려면 PROCESSEDVERIFIED를 분리하는 편이 낫다.

처리는 끝났지만 검증이 실패한 상태를 성공으로 표시하면 데이터 오염이 고착된다.

Idempotent Write

backfill은 같은 batch가 다시 실행될 수 있다.

target write는 idempotent해야 한다.

INSERT INTO order_summary (order_id, paid_amount, updated_at)
VALUES (:order_id, :amount, now())
ON CONFLICT (order_id)
DO UPDATE SET paid_amount = EXCLUDED.paid_amount,
              updated_at = now();

insert only 방식은 재실행 시 중복 row를 만들 수 있다.

add amount 방식은 재실행 시 금액을 두 번 더할 수 있다.

검증

완료 기준은 처리 건수가 아니다.

source와 target의 business 합계가 맞아야 한다.

SELECT count(*) AS source_count, sum(amount) AS source_amount
FROM orders
WHERE created_at >= :from AND created_at < :to;
SELECT count(*) AS target_count, sum(paid_amount) AS target_amount
FROM order_summary
WHERE order_date >= :from_date AND order_date < :to_date;

count가 같아도 amount가 다를 수 있다.

최신 시각과 sample row 비교도 함께 본다.

운영 지표

  • backfill processed rows: 진행량이다.
  • rows per second: 처리 속도다.
  • checkpoint position: 마지막 성공 위치다.
  • duplicate skip count: 재실행 중 건너뛴 입력이다.
  • DB CPU and lock wait: 운영 DB 부담이다.
  • replica lag: 읽기 복제 지연이다.
  • validation gap: source-target 차이다.

backfill 지표는 작업 지표와 서비스 지표를 함께 봐야 한다.

개인 프로젝트 기준

개인 프로젝트에서는 작은 backfill runner를 만든다.

range, batch size, sleep, checkpoint를 설정 파일이나 command argument로 받는다.

같은 range를 두 번 실행해도 결과가 같아야 한다.

중간에 강제로 종료한 뒤 checkpoint부터 재시작하는 테스트가 있으면 충분하다.

위험 신호!

  • 운영 DB에 대량 update SQL 하나를 바로 실행한다.
  • range와 checkpoint 없이 “전체 재계산”만 제공한다.
  • backfill 중단 조건이 없다.
  • 처리 건수만 보고 source-target 합계를 검증하지 않는다.
  • 재실행하면 target 값이 누적된다.

확인 질문

확인 질문

  • backfill range와 checkpoint key는 무엇인가?
    • timestamp, id, partition date 중 하나로 명확히 답해야 한다.
  • 작업을 중간에 멈추면 어디서 다시 시작하는가?
    • 마지막 검증된 checkpoint 이후부터 재개해야 한다.
  • backfill이 운영 서비스에 부담을 주면 무엇을 기준으로 속도를 낮추거나 멈추는가?
    • latency, replica lag, DB CPU, lock wait 같은 stop condition이 있어야 한다.

참고 문서