배치를 다시 돌려도 되게 만드는 법 — 멱등성 설계

야간 배치가 실패했을 때 가장 먼저 떠오르는 대응은 다시 돌리는 것이다. 그런데 다시 돌려도 되는 배치인지가 늘 확실하지는 않다. 절반쯤 처리하다 죽은 배치를 재실행하면 이미 처리된 건이 두 번 반영될 수 있기 때문이다.

이 문제를 없애는 설계 원칙이 멱등성(idempotency)이다. 같은 입력으로 몇 번을 실행해도 결과가 한 번 실행한 것과 같은 성질을 말한다.

멱등하지 않은 배치의 모양

다음은 설명을 위한 예시다. 일별 집계를 누적하는 흔한 형태다.

insert into stat_daily (work_dt, ord_cnt, amt_sum)
select work_dt, count(*), sum(amt)
from orders
where work_dt = v_dt
group by work_dt;

이 문장은 실행할 때마다 행을 하나씩 더 만든다. 두 번 돌리면 같은 날짜가 두 건이 된다. 다음은 더 위험하다.

update acct_balance
set balance = balance + v_amt
where acct_no = v_acct;

현재 값에 더하는 방식이라 재실행할 때마다 금액이 늘어난다. 에러가 난 지점 이전까지는 이미 반영돼 있으므로, 전체 재실행은 그 구간을 두 번 더한다.

멱등하게 만드는 방법

1. 누적 대신 확정값으로 쓴다. 가장 강력한 방법이다. “더한다” 대신 “이 값으로 만든다”로 바꾸면 몇 번을 실행해도 결과가 같다.

merge into stat_daily t
using (
select work_dt, count(*) cnt, sum(amt) amt
from orders where work_dt = v_dt group by work_dt
) s
on (t.work_dt = s.work_dt)
when matched then update set t.ord_cnt = s.cnt, t.amt_sum = s.amt
when not matched then insert (work_dt, ord_cnt, amt_sum)
values (s.work_dt, s.cnt, s.amt);

원천 데이터에서 매번 다시 계산하므로 이전 실행이 어디까지 갔든 상관이 없다. 처리 시간은 조금 더 들지만 재실행 걱정이 사라진다.

2. 처리 구간을 먼저 지운다. 집계 대상 구간이 명확하다면 삭제 후 삽입도 깔끔하다.

delete from stat_daily where work_dt = v_dt;

insert into stat_daily (work_dt, ord_cnt, amt_sum)
select work_dt, count(*), sum(amt)
from orders where work_dt = v_dt group by work_dt;

commit;

두 문장이 한 트랜잭션 안에 있어야 한다. 중간에 커밋하면 삭제만 되고 삽입이 안 된 상태로 남을 수 있다.

3. 자연키에 유일 제약을 건다. 코드로 막는 것보다 제약으로 막는 편이 확실하다.

alter table stat_daily add constraint uk_stat_daily unique (work_dt);

이러면 중복 삽입이 애초에 불가능해진다. 코드를 고치다 실수해도 데이터베이스가 막아 준다. 제약이 있으면 위의 MERGE도 더 안전하게 동작한다.

4. 처리 이력을 남긴다. 외부 시스템 호출처럼 되돌릴 수 없는 작업이 섞여 있으면 값 계산만으로는 부족하다. 무엇을 처리했는지 기록해 두고 건너뛰게 만든다.

create table batch_run_log (
batch_nm varchar2(50),
work_dt date,
stat varchar2(10),
start_dt timestamp,
end_dt timestamp,
constraint uk_batch_run unique (batch_nm, work_dt)
);
select count(*) into v_done
from batch_run_log
where batch_nm = 'DAILY_STAT' and work_dt = v_dt and stat = 'S';

if v_done > 0 then
return;
end if;

이력 테이블의 유일 제약은 동시 실행 방지에도 쓰인다. 시작 시점에 상태 R로 행을 넣고, 이미 있으면 다른 인스턴스가 돌고 있다는 뜻이므로 중단한다.

부분 재실행을 고려한다

건수가 많아 한 번에 못 도는 배치는 중간 커밋이 불가피하다. 이때는 전체 재실행 대신 남은 것만 처리하는 구조로 만든다.

update tx_master
set stat_yn = 'Y', proc_dt = systimestamp
where work_dt = v_dt
and stat_yn = 'N'
and rownum <= 5000;

조건에 stat_yn = 'N'이 들어 있으므로 이미 처리된 건은 다시 잡히지 않는다. 어디서 멈췄든 다시 실행하면 남은 것부터 이어서 처리한다. 상태 컬럼 하나가 재실행 안전성을 만든다.

설계할 때의 점검 항목

  • 이 배치를 지금 두 번 연속 돌리면 결과가 달라지는가
  • 절반쯤 처리된 상태에서 다시 돌리면 어떻게 되는가
  • 같은 시각에 두 개가 동시에 돌면 어떻게 되는가
  • 재실행 전에 사람이 수동으로 지워야 하는 것이 있는가

마지막 항목이 특히 중요하다. “재실행하려면 먼저 그 테이블 지우고 돌리세요”라는 운영 문서가 있다면, 그건 멱등하지 않다는 뜻이다. 새벽에 장애 대응하는 사람이 그 절차를 기억하리라 기대하기 어렵다.

정리

멱등성은 성능이나 기능이 아니라 운영 비용의 문제다. 멱등한 배치는 실패하면 그냥 다시 돌리면 된다. 그렇지 않은 배치는 실패할 때마다 사람이 상태를 확인하고 판단해야 하고, 그 판단은 대개 새벽에 이루어진다.

핵심은 두 가지다. 누적 대신 확정값으로 쓰고, 처리 여부를 데이터 자체에 남긴다. 여기에 유일 제약을 얹으면 코드가 틀려도 데이터는 지켜진다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤