본문 바로가기
C.W.K.
Stream
Lesson 04 of 06 · published

DE Mental Model — Schema, Lineage, Idempotency, Contract

~14 min · foundation, mental-model, principles

Level 0구경꾼
0 XP0/47 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

한 번 돌아간 스크립트와 2년째 매일 밤 도는 파이프라인을 가르는 건 아이디어 네 개야. 지금 외워 둬 — 앞으로 모든 트랙에서 계속 마주칠 테니까.

1. Schema

Schema 는 데이터가 어떤 모양이어야 하는지를 선언해 둔 기술이야 — 컬럼 이름, 타입, 제약 ("unique", "음수 금지", "이 정규식에 맞을 것"). 탐색할 땐 auto-detect 도 괜찮아. 하지만 사람 없이 도는 파이프라인에선 schema 가 명시적이어야 하고, version control 에 있어야 하고, 도구 경계를 넘을 때마다 검증돼야 해.

2. Lineage

Lineage 는 "이 숫자 어디서 왔어?" 에 답해 줘 — 테이블 수준에서, 컬럼 수준에서, 이상적으론 row 수준에서. CFO 가 Q1 매출이 0.3% 움직인 이유를 물으면 거꾸로 걸어갈 수 있어야 해: 대시보드 → 최종 mart → 중간 join → 소스 extract. Modern 도구 (dbt, Dagster, OpenLineage) 는 이 그래프를 자동으로 그려 줘 — 도구가 깔아 준 길을 따라 쌓았다면.

3. Idempotency

Idempotent 파이프라인은 같은 입력 window 에 대해 한 번을 돌리든, 두 번이든, 스무 번이든 같은 결과를 내. 사소해 보이지? 안 사소해. 순진한 append 는 retry 한 번에 row 가 두 배가 되고, 순진한 incremental load 는 backfill 뒤에 sync 가 어긋나. 규율은 write 단계를 이렇게 설계하는 거야: partition 을 다시 돌리면 깔끔하게 통째로 교체되도록. 이걸 한번 몸에 익히면 디버깅이 더는 무섭지 않아 — "그냥 다시 돌려" 가 진짜 답이 되거든.

한 가지는 조심해. 아래 stage-and-swap 패턴을 두고 atomic 이라고들 하는데 정확히는 아냐. 파일시스템이 한 번에 원자적으로 갈아 주는 건 경로 하나뿐이고, 이미 내용물이 있는 디렉토리를 한 걸음에 바꿔 끼우진 못해. 이 패턴이 실제로 주는 건 다시 돌려도 안전한 write, 그리고 partition 이 잠깐 공중에 떠 있는 아주 짧은 구간이야. 그 정도면 충분해 — 거기서 죽어도 되돌릴 수 있게 순서만 잡아 두면. Track 4 에서 제대로 뜯어볼 거야.

4. Contract

Data contract 는 데이터를 만드는 팀과 쓰는 팀 사이의 살아 있는 합의야: schema, freshness, 소유권, breaking change 정책. Contract 가 없으면 모든 upstream 변경이 장애 예약이야. 있으면 producer 는 뭘 깨면 안 되는지 알고, consumer 는 뭘 믿어도 되는지 알아.

네 개가 어떻게 맞물리는지

Schema 는 지역적이야 — 단계마다, 여기서 이게 참이어야 해. Lineage 는 전역적이고 — 전 단계에 걸쳐, 데이터가 어떻게 흘렀나. Idempotency 는 시간적이야 — 재실행을 거듭해도 결과가 흔들리지 않나. Contract 는 사회적이고 — 팀 사이에, 우리가 서로 뭘 약속했나. 진지한 파이프라인은 넷 다 갖고 있어. 일회성 스크립트는 하나도 없고.

Code

Idempotent partition write — canonical 패턴·python
from pathlib import Path
import shutil
import pandas as pd

def write_partition(df: pd.DataFrame, root: Path, partition: str) -> Path:
    '''Partition 교체. 다시 돌려도 안전 — 같은 결과.

    Atomic 아니고, docstring 이 그렇다고 우기면 안 돼. 이미 내용물이 있는
    디렉토리 위로 rename 하면 OSError(ENOTEMPTY) 가 나거든. 이런 디렉토리
    교체는 언제나 두 걸음이야. 사이에서 죽어도 되돌릴 수 있게 순서를 잡아.
    '''
    target = root / f'date={partition}'
    staging = root / f'.tmp_{partition}'
    retired = root / f'.retired_{partition}'

    # 1. 아무도 안 읽는 staging 디렉토리에 쓰기
    if staging.exists():
        shutil.rmtree(staging)
    staging.mkdir(parents=True)
    df.to_parquet(staging / 'data.parquet', index=False)

    # 2. 옛 partition 을 옆으로 치우고, 그 다음에 새 걸 넣어. 이 두 rename
    #    사이에서 죽어도 옛 데이터는 .retired_* 에 남아 있어 — 되돌릴 수
    #    있다는 뜻이야. 먼저 지워 버리면 되돌릴 방법이 없어.
    if target.exists():
        if retired.exists():
            shutil.rmtree(retired)
        target.rename(retired)
    staging.rename(target)

    # 3. 여기까지 왔으면 옛 partition 을 버려도 안전해
    if retired.exists():
        shutil.rmtree(retired)
    return target

# 같은 partition 에 다시 돌려? 같은 결과. Double-count 도 orphan row 도 없음
write_partition(today_df, Path('warehouse/orders'), '2026-04-30')

External links

Exercise

과거에 만든 데이터 생산 스크립트 — CSV exporter, daily 리포트, 뭐든 — 하나 골라. 이 네 가지 (explicit schema, 추적 가능한 lineage, idempotent 재실행, 선언된 contract) 기준으로 0–4 점을 매겨 봐. 일회성 스크립트는 보통 0/4 야 — 그게 정상이야. 근데 이제 과녁이 생긴 거야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.