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

  • Extract, Transform, Load 각 단계는 어떤 입력과 출력을 가져야 하는가?
  • 단계별 실패를 분리하지 않으면 재처리 범위가 왜 커지는가?
  • Java/Spring 기반 batch에서 단계별 로그와 검증을 어떻게 남기는가?
  • source count, target count, 품질 rule은 어느 단계에서 확인해야 하는가?

개요

Extract는 원천에서 데이터를 가져오는 단계다.

Transform은 데이터를 목적에 맞게 바꾸는 단계다.

Load는 변환 결과를 target 저장소에 쓰는 단계다.

세 단계를 한 함수로 묶으면 실패 지점과 재처리 범위가 흐려진다.

운영 가능한 pipeline은 각 단계의 입력, 출력, checkpoint, 검증 기준을 분리한다.

원리

각 단계는 다른 질문에 답한다.

  • Extract: source에서 빠진 데이터가 없는가?
  • Transform: business rule이 올바르게 적용되었는가?
  • Load: target에 중복이나 누락 없이 반영되었는가?

이 질문이 다르므로 로그와 지표도 달라야 한다.

단계 흐름

Extract
  input: orders table, updated_at range
  output: raw_orders partition
 
Transform
  input: raw_orders partition
  output: fact_order_payment rows
 
Load
  input: fact_order_payment rows
  output: warehouse fact table
 
Validate
  input: source/raw/target counts
  output: quality result

Validate는 부가 작업이 아니다.

검증이 끝나기 전에는 pipeline 완료라고 말하기 어렵다.

Extract 예시

SELECT *
FROM orders
WHERE updated_at >= TIMESTAMP '2026-06-30 00:00:00'
  AND updated_at < TIMESTAMP '2026-07-01 00:00:00'
ORDER BY updated_at, order_id;

extract는 범위가 명확해야 한다.

updated_at만 쓰면 같은 timestamp 안에서 누락이 생길 수 있으므로 tie-breaker key를 함께 쓰는 편이 안전하다.

Transform 예시

public FactOrderPayment transform(RawOrder row) {
    return new FactOrderPayment(
        row.paymentId(),
        row.orderId(),
        row.memberId(),
        row.paidAmount(),
        row.paidAt(),
        row.sourceEventId()
    );
}

transform은 business rule을 담는다.

취소 주문 제외, 환불 반영, 테스트 주문 제외, 금액 반올림 같은 규칙은 여기서 명확해야 한다.

Load 예시

INSERT INTO fact_order_payment (
    payment_id, order_id, member_id, paid_amount, paid_at, source_event_id, loaded_at
)
VALUES (:paymentId, :orderId, :memberId, :paidAmount, :paidAt, :sourceEventId, now())
ON CONFLICT (payment_id)
DO UPDATE SET
    paid_amount = EXCLUDED.paid_amount,
    loaded_at = EXCLUDED.loaded_at;

load는 멱등해야 한다.

같은 range를 다시 적재해도 target 결과가 안전해야 backfill과 재처리가 가능하다.

단계별 기록

CREATE TABLE pipeline_step_run (
    run_id bigserial PRIMARY KEY,
    pipeline_name varchar(100) NOT NULL,
    step_name varchar(100) NOT NULL,
    source_range varchar(200) NOT NULL,
    input_count bigint NULL,
    output_count bigint NULL,
    status varchar(20) NOT NULL,
    started_at timestamp NOT NULL,
    finished_at timestamp NULL,
    error_message text NULL
);

step별 run 기록은 장애 때 가장 먼저 보는 자료다.

한 단계가 성공했는지뿐 아니라 몇 건을 읽고 몇 건을 썼는지도 남겨야 한다.

판단 축

  • extract range는 명시적이고 재현 가능해야 한다.
  • transform rule은 코드와 문서에 같은 의미로 남아야 한다.
  • load는 target key 기준으로 멱등해야 한다.
  • 검증은 checkpoint 갱신 전에 수행한다.
  • 단계별 count와 error를 같은 run id로 묶는다.

실패 모델

  • extract 범위가 now() 기준이라 재실행 때 다른 데이터를 읽는다.
  • transform에서 테스트 주문 제외 기준이 누락된다.
  • load가 append만 해서 재실행 때 중복 row가 생긴다.
  • 단계별 count가 없어 어느 단계에서 row가 줄었는지 모른다.
  • 검증 전에 checkpoint가 성공으로 갱신된다.

운영 지표

  • extract_input_count: source에서 읽은 건수다.
  • raw_output_count: raw에 저장한 건수다.
  • transform_reject_count: transform에서 제외하거나 실패한 건수다.
  • load_upsert_count: target에 반영한 건수다.
  • validation_failed_count: 검증 실패 수다.
  • step_duration_seconds: 단계별 소요 시간이다.

검증 SQL

SELECT COUNT(*) AS raw_count
FROM raw_orders
WHERE ds = DATE '2026-06-30';
SELECT COUNT(*) AS fact_count, SUM(paid_amount) AS paid_amount
FROM fact_order_payment
WHERE paid_at >= TIMESTAMP '2026-06-30 00:00:00'
  AND paid_at < TIMESTAMP '2026-07-01 00:00:00';

raw count와 fact count가 항상 같을 필요는 없다.

하지만 차이가 나는 이유는 transform rule로 설명되어야 한다.

개인 프로젝트 기준

개인 프로젝트에서는 하나의 Spring Batch job 안에서 세 단계를 구현해도 된다.

다만 log에는 extract count, transform count, load count를 따로 남긴다.

작은 table이라도 target key를 두고 upsert로 적재하면 재실행 연습이 된다.

기업 운영 기준

기업 운영에서는 단계별 owner와 알림 기준이 다를 수 있다.

extract 실패는 source 접근이나 schema 문제일 수 있다.

transform 실패는 business rule이나 코드 문제일 수 있다.

load 실패는 warehouse 권한, partition, storage 문제일 수 있다.

장애 알림에는 실패 단계가 반드시 들어가야 한다.

실전 팁

  • function 이름보다 step 이름을 운영자가 이해할 수 있게 짓는다.
  • extract range는 로그와 target metadata에 함께 남긴다.
  • transform에서 제외한 row는 reason과 함께 샘플을 남긴다.
  • load target에는 unique key를 설계한다.
  • checkpoint는 validate 성공 후 갱신한다.

위험 신호!

  • runPipeline() 하나가 extract, transform, load를 모두 숨긴다.
  • source count와 target count가 맞지 않아도 원인을 설명하지 못한다.
  • transform 오류 row가 로그에만 남고 재처리할 수 없다.
  • load가 중복을 DB 제약으로 막지 않는다.
  • pipeline 성공 로그가 검증 성공보다 먼저 찍힌다.

확인 질문

확인 질문

  • Extract, Transform, Load를 분리해야 하는 이유는 무엇인가?
    • 실패 지점과 재처리 범위, 검증 기준이 단계마다 다르기 때문이다.
  • Load 단계에서 멱등성이 필요한 이유는 무엇인가?
    • 같은 range를 다시 적재해도 target row가 중복되거나 잘못 누적되지 않게 하기 위해서다.
  • checkpoint는 언제 갱신해야 하는가?
    • load와 품질 검증이 성공한 뒤 갱신해야 한다.

참고 문서