사전 증분 처리
개념

증분 처리

gabury1고친 사람 github-actions[bot]

증분 처리는 지난번 처리 뒤로 새로 들어오거나 바뀐 데이터만 골라 처리하는 일입니다. 데이터 전체를 매번 처음부터 다시 처리하지 않습니다. 그래서 한 번에 할 일이 전체 크기가 아니라 바뀐 양을 따라갑니다. 빌드나 백업에서도 바뀐 것만 다시 하는 방식을 같은 이름으로 부릅니다.

쉽고 빠른 이해

증분 처리는 어제 이후 생긴 것만 처리하는 방식입니다. 주문이 1억 건 쌓인 테이블에서 매일 밤 어제 들어온 10만 건만 읽어 집계에 더하는 식입니다.

왜 하나 — 데이터는 계속 쌓입니다. 매번 전부 다시 읽으면 처리 시간과 비용이 데이터 크기만큼 계속 늘어납니다.

언제 안 쓰나 — 원본이 작아 전체를 다시 읽어도 금방 끝나면 전체 재처리가 더 단순합니다. 계산 로직을 고쳐 지난 결과까지 바로잡아야 할 때도 전체를 다시 돌립니다.

어떻게 도나:

  1. 지난번에 어디까지 처리했는지 적어 둔 값을 읽습니다
  2. 그 뒤에 생긴 데이터만 골라 처리하고 결과에 반영합니다
  3. 반영이 끝나면 적어 둔 값을 이번에 처리한 끝으로 고칩니다

무엇이 나빠지나 — 「무엇이 새것인가」를 가려내는 일이 까다롭습니다. 늦게 도착한 데이터나 지워진 데이터를 놓치기 쉽습니다. 실패한 뒤 다시 돌리면 같은 데이터가 두 번 반영될 수도 있습니다.

상세

도서관 사서가 매일 저녁 장부를 정리한다고 합시다. 사서는 서가의 책을 전부 다시 세지 않습니다. 그날 새로 들어온 책과 빠져나간 책만 장부에 더하고 뺍니다.

데이터를 읽고 다듬고 저장하는 단계를 차례로 이어 놓은 처리를 파이프라인이라고 합니다. 증분 처리는 이 파이프라인이 매번 원본 전체를 읽지 않고 지난 실행 뒤에 생긴 몫만 읽어 결과에 더하는 방식입니다. 주문 테이블에서 어제 들어온 주문만 읽어 날짜별 매출 테이블에 한 줄을 더하는 처리가 한 예입니다.

반대편은 원본 전체를 다시 읽어 결과를 처음부터 새로 만드는 방식입니다. 이 방식을 전체 재처리라고 부릅니다. 실무에서는 영어로 full refresh 나 full load 라고도 합니다.

영어 incremental 은 「조금씩 늘어나는」이라는 뜻입니다. increment 가 늘어난 몫 하나입니다. 증분(增分)도 같은 뜻의 한자어입니다.

전체 재처리와 견준 차이

증분 처리를 고르는 이유는 전체 재처리와 견줘야 보입니다. 아래 표는 두 방식이 읽는 양과 감당할 일을 견줍니다.

전체 재처리 증분 처리
한 번에 읽는 양 원본 전체 지난번 뒤에 생긴 몫
데이터가 쌓이면 실행 시간이 계속 늘어난다 하루치 양이 비슷하면 실행 시간도 비슷하다
기억해 둘 상태 없다 어디까지 처리했는지
계산 로직을 고치면 다음 실행이 지난 결과까지 바로잡는다 지난 결과는 틀린 채 남는다

전체 재처리는 단순합니다. 매번 처음부터 만드니 어제 실행이 어떻게 끝났는지 신경 쓸 필요가 없습니다. 대신 원본이 커질수록 실행이 길어집니다.

증분 처리는 실행 시간을 바뀐 양에 묶어 둡니다. 대신 「어디까지 했나」라는 상태를 들고 다녀야 합니다. 이 상태가 틀리면 데이터를 빠뜨리거나 두 번 처리합니다.

어디까지 처리했는지 적어 두는 값

증분 처리의 중심에는 값 하나가 있습니다. 그 값이 무엇인지 짧은 코드로 봅니다.

지난번 실행이 처리한 데이터의 끝을 적어 둔 값을 하이 워터마크(high-water mark)라고 부릅니다. 강물이 가장 높이 올라온 곳에 그어 둔 금에서 온 말입니다. 대개 마지막으로 처리한 행의 ID(identifier, 식별자)를 쓰거나, 그 행에 적힌 시각을 씁니다. 파이프라인이 돈 시각이 아니라 데이터에 적힌 값입니다. 다음 실행은 이 값보다 뒤에 있는 데이터만 읽습니다.

아래 코드에서 orders 는 원본의 주문 ID 목록이고 mark 가 하이 워터마크입니다.

Python
orders = [1, 2, 3]
mark = 0

def run():
    global mark
    new = [i for i in orders if i > mark]
    if new:
        mark = max(new)
    return new

run()              # [1, 2, 3]
orders.append(4)
run()              # [4]
run()              # []

첫 실행은 하이 워터마크가 0 이라 셋을 다 읽습니다. 주문 4 가 들어온 뒤의 실행은 4 하나만 읽습니다. 새 주문이 없으면 아무것도 읽지 않습니다. 원본이 1억 건이어도 실행 한 번이 읽는 양은 그사이 늘어난 몫뿐입니다.

하이 워터마크를 고치는 순서

하이 워터마크는 결과를 다 반영한 뒤에 고쳐야 합니다. 먼저 그 순서를 그림으로 봅니다. 그다음 순서가 틀리면 무엇이 깨지는지 봅니다.

sequenceDiagram
    participant 파이프라인
    participant 상태 저장소
    participant 원본
    participant 결과
    파이프라인->>상태 저장소: 하이 워터마크를 읽는다
    상태 저장소-->>파이프라인: 마지막 ID 1000
    파이프라인->>원본: ID 1000 보다 큰 행을 달라
    원본-->>파이프라인: 새 행 50개
    파이프라인->>결과: 새 행을 반영한다
    파이프라인->>상태 저장소: 하이 워터마크를 1050 으로 고친다

상태 저장소는 하이 워터마크를 담아 두는 곳입니다. 작은 테이블 하나나 파일 하나인 경우가 많습니다. 그림에서 하이 워터마크를 고치는 일은 결과 반영 뒤에 옵니다.

순서를 거꾸로 하면 데이터를 잃습니다. 하이 워터마크를 1050 으로 먼저 고쳤다고 합시다. 그 뒤 결과를 반영하다 실패합니다. 다음 실행은 1050 뒤부터 읽으니 1001 부터 1050 까지는 영영 처리되지 않습니다.

순서를 지켜도 다른 고장이 남습니다. 반영은 끝났는데 하이 워터마크를 고치기 직전에 실행이 죽을 수 있습니다. 다음 실행은 1001 부터 다시 읽으니 같은 50행이 한 번 더 반영됩니다. 그래서 같은 행을 두 번 반영해도 결과가 한 번 반영한 것과 같도록 짜 둡니다. 이 성질을 멱등성이라고 합니다.

새로 생긴 데이터를 골라내는 방법

「지난번 뒤에 생긴 데이터」를 어떻게 알아보느냐는 원본이 무엇을 남겨 두느냐에 달렸습니다. 아래 표에 흔한 방법 넷과 각각이 놓치는 것을 모았습니다.

방법 무엇으로 고르나 놓치는 것
늘어나기만 하는 ID 하이 워터마크보다 큰 ID 고친 행 · 지운 행
갱신 시각 열 하이 워터마크 뒤에 고쳐진 행 지운 행
변경 데이터 캡처 데이터베이스가 남긴 변경 기록 없다. 넣기 · 고치기 · 지우기를 다 받는다
날짜별 조각 · 새 파일 지난번 뒤에 생긴 날짜 조각이나 파일 이미 있던 조각 안의 변경

ID 는 새 행이 생길 때마다 커지기만 하는 번호입니다. 넣기만 하는 데이터라면 이것으로 충분합니다. 애플리케이션이 남기는 로그나 클릭 기록처럼 한 번 쌓이면 고치지 않는 데이터가 그렇습니다.

행을 고치는 데이터라면 행마다 마지막으로 고친 시각을 적은 열을 둡니다. 흔히 updated_at 이라고 이름 붙입니다. 하이 워터마크도 ID 대신 시각이 됩니다. 이 시각은 원본이 행을 적거나 고친 때입니다.

지운 행은 두 방법 모두 못 봅니다. 두 방법은 원본에 지금 남아 있는 행 가운데서 새것을 고릅니다. 지운 행은 원본에 남아 있지 않으니 고를 대상에 아예 들지 않습니다.

그래서 행을 실제로 지우지 않고 「지웠음」 표시만 남기는 소프트 삭제를 같이 씁니다. 표시를 남기는 일도 행을 고치는 일이라 updated_at 이 새 시각으로 바뀝니다. 그러면 갱신 시각 방법이 이 행을 고친 행으로 골라 옵니다.

변경 데이터 캡처는 CDC(Change Data Capture)라고 줄여 부릅니다. 데이터베이스가 남긴 변경 기록을 읽어 넣기·고치기·지우기를 일어난 차례대로 받아 오는 방법입니다.

이 변경 기록은 데이터베이스가 안에서 쓰는 장부이고, 트랜잭션 로그라고 부릅니다. 데이터베이스는 테이블을 고치기 전에 무엇을 바꿀지를 이 장부에 먼저 적습니다. 도중에 꺼져도 장부를 보고 되살리기 위해서입니다. 앞에서 처리 대상으로 든 애플리케이션 로그와는 다른 것입니다.

커다란 테이블을 날짜 같은 기준 값으로 여러 조각으로 나눴을 때 그 조각 하나를 파티션이라고 합니다. 원본이 날짜별 파티션이나 날짜별 파일로 쌓이면 지난번 뒤에 생긴 파티션만 읽으면 됩니다.

결과에 반영하는 방법

새 데이터를 골라낸 다음에는 결과 테이블에 넣어야 합니다. 넣는 방법 둘과 각각에 맞는 데이터를 봅니다.

첫째는 덧붙이기입니다. 새 행을 결과 끝에 더하기만 합니다. 넣기만 하는 데이터에 맞습니다. 다만 같은 행이 두 번 오면 두 줄이 됩니다.

둘째는 UPSERT(update + insert, 고치거나 넣기)입니다. 키가 같은 행이 결과에 이미 있으면 고치고 없으면 넣습니다. 주문 상태가 「결제」에서 「배송」으로 바뀌면 그 주문의 줄을 고쳐 씁니다.

UPSERT 는 같은 행을 두 번 받아도 한 줄만 남습니다. 「하이 워터마크를 고치는 순서」에서 본 고장, 곧 하이 워터마크를 고치기 직전에 실행이 죽는 고장을 이 방법이 막아 줍니다.

늦게 도착하는 데이터

하이 워터마크는 데이터가 차례대로 도착한다고 기대합니다. 그 기대가 깨지면 데이터가 빠집니다. 먼저 무엇이 빠지는지 봅니다. 그다음 흔한 대응을 봅니다.

이벤트가 일어난 시각과 파이프라인에 도착한 시각은 다를 수 있습니다. 휴대폰 앱이 비행기 안에서 남긴 클릭 기록은 착륙한 뒤에야 서버로 올라옵니다. 이렇게 일어난 시각보다 한참 늦게 들어오는 데이터를 지연 도착 데이터라고 합니다.

하이 워터마크가 이벤트가 「일어난 시각」이라면 이 기록은 빠집니다. 기록이 도착했을 때 하이 워터마크는 이미 그 시각을 지나 있습니다. 다음 실행은 하이 워터마크 뒤만 읽으니 이 기록을 영영 안 읽습니다.

흔한 대응은 조금 겹쳐 읽는 것입니다. 하이 워터마크보다 몇 시간 앞에서부터 다시 읽습니다. 실무에서는 이 겹치는 구간을 lookback window 라고 부릅니다. 겹쳐 읽은 행은 이미 반영된 것이라 UPSERT 로 넣어야 두 줄이 되지 않습니다.

다른 대응은 하이 워터마크를 「도착한 시각」으로 잡는 것입니다. 원본에 행이 들어온 시각을 따로 적어 두면 늦게 온 기록도 들어온 순서대로 잡힙니다. 앞에서 본 updated_at 도 원본이 행을 적은 때를 담으니 이쪽에 듭니다.

증분으로 계산하기 어려운 집계

결과가 단순한 행 목록이 아니라 집계일 때도 있습니다. 증분으로 되는 집계와 안 되는 집계를 방문자 수로 견줍니다.

합계와 개수는 더하면 됩니다. 어제까지의 매출 합에 오늘 매출을 더하면 오늘까지의 매출 합입니다. 지난 데이터를 다시 읽을 필요가 없습니다.

중복을 뺀 개수는 그렇게 안 됩니다. 아래는 이틀 동안의 방문자를 사람 단위로 센 것입니다.

Python
day1 = {"A", "B"}       # 1일 방문자
day2 = {"B", "C"}       # 2일 방문자

len(day1) + len(day2)   # 4
len(day1 | day2)        # 3

날마다 센 수를 더하면 4명입니다. 이틀 동안 온 사람은 A·B·C 셋입니다. 둘째 날의 B 가 첫째 날에도 왔는지 알려면 첫째 날의 방문자 목록이 있어야 합니다.

그래서 중복을 뺀 개수나 중앙값 같은 집계는 증분으로 만들기 어렵습니다. 지난 원본을 다시 읽거나 중간 결과를 따로 들고 있어야 합니다.

증분 처리를 쓰는 때와 안 쓰는 때

증분 처리는 늘 이득이 아닙니다. 증분과 전체 재처리 가운데 무엇을 고르는지 상황별로 봅니다.

상황 고르는 쪽
원본이 크고 한 번에 바뀌는 양이 작다 증분 처리
원본이 작아 전체를 읽어도 금방 끝난다 전체 재처리
계산 로직을 바꿔 지난 결과도 틀렸다 전체 재처리나 백필(지난 기간만 골라 다시 돌리기)
원본이 지운 행을 알려 주지 않는다 전체 재처리나 주기적인 전체 비교

원본이 작으면 증분 처리가 얻는 것이 적습니다. 하이 워터마크와 UPSERT 를 관리하는 수고만 늘어납니다.

계산 로직을 고치면 증분 처리는 그 뒤에 들어오는 데이터만 새 로직으로 계산합니다. 이미 만든 지난 결과는 옛 로직의 값으로 남습니다. 그래서 백필이나 전체 재처리를 따로 합니다.

원본이 지운 행을 알려 주지 않으면 증분 처리로는 지운 행을 알 길이 없습니다. 이럴 때는 가끔 원본의 키 목록 전체를 결과의 키 목록과 맞대 봅니다. 결과에만 남은 키가 원본에서 지워진 행입니다. 표의 「주기적인 전체 비교」가 이 일입니다.

증분 처리를 오래 돌리면 빠뜨린 행이나 두 번 들어간 행이 조금씩 쌓일 수 있습니다. 그래서 둘을 섞기도 합니다. 평소엔 증분으로 돌립니다. 주말마다 한 번씩 전체 재처리로 결과를 새로 맞춥니다.

데이터 밖에서 쓰는 증분

같은 생각이 다른 분야에도 있습니다. 여기서는 그 이름만 짚고 넘어갑니다.

증분 빌드는 소스 파일 가운데 바뀐 것만 다시 컴파일합니다. 증분 백업은 지난 백업 뒤에 바뀐 파일만 복사합니다. 증분 색인은 검색 색인에 바뀐 문서만 반영합니다. 셋 다 「지난번 뒤에 바뀐 것만 다시 한다」는 같은 뼈대를 씁니다.

관련 항목

증분 처리가 도는 데이터 작업

파이프라인 · 데이터 파이프라인 · 배치 처리 · ETL · ELT · 추출 · 적재 · 변환 · 데이터 웨어하우스

증분 처리와 맞세워지는 처리 방식

전체 재처리 · 전체 추출 · 백필 · 재처리 · 스트림 처리 · 마이크로배치

증분 처리가 새 데이터를 골라내는 수단

하이 워터마크 · 증분 추출 · 변경 데이터 캡처 · 체크포인트 · 오프셋 · 파티션 · 파티셔닝 · 소프트 삭제 · 트랜잭션 로그

증분 처리를 안전하게 만드는 성질과 기법

멱등성 · UPSERT · MERGE 문 · 트랜잭션 · 정확히 한 번 처리

증분 처리가 잘못 돌 때 나는 장애

중복 처리 · 중복 적재 · 데이터 누락 · 지연 도착 데이터 · 데이터 품질

증분으로 계산하기 어려운 집계

집계 · 고유 개수 · 중앙값 · HyperLogLog · 구체화 뷰

늦은 데이터를 다루는 시간 개념

이벤트 시간 · 처리 시간 · 워터마크 · 윈도 집계

증분 처리를 지원하는 도구

오케스트레이션 · Apache Airflow · dbt · Apache Spark · Delta Lake · Apache Iceberg

같은 뼈대를 쓰는 다른 분야의 증분

증분 빌드 · 증분 백업 · 증분 색인 · 증분 계산 · 차등 백업

증분 처리가 속하는 분야

데이터 엔지니어링 · 데이터 통합 · 분석 엔지니어링

다른 이름: incremental processing · incremental load · 증분 적재 · 증분 로드 · 델타 처리