데이터 엔지니어링
흩어진 원천 데이터를 모아 믿고 쓸 수 있는 표로 만드는 일을 다룬다. ETL과 ELT, 웨어하우스와 레이크하우스, Parquet와 테이블 포맷의 속, DAG 오케스트레이션, 차원 모델링, 데이터 품질과 계약, 배치·스트리밍·CDC를 거쳐, 그 표를 읽는 쪽에 필요한 통계 기초까지 숫자로 계산해 가며 정리한다.
목차 (10)
스트림 처리의 시간 의미와 정확히 한 번 처리는 시스템 설계 › 배치와 스트림 처리에, 로그·지표를 모으는 설계 사례는 시스템 설계 › 데이터 사례에, 열 지향 저장과 분석 질의 엔진은 데이터베이스 › 분석과 검색에 있다. 이 노트는 그 부품을 엮어 분석용 데이터를 만들고 지키는 쪽을 다룬다.
ETL과 ELT
데이터 파이프라인의 뼈대는 세 단계다. 원천(서비스 DB, 외부 API, 로그)에서 추출(Extract)하고, 쓸 모양으로 변환(Transform)하고, 분석 저장소에 적재(Load)한다. 순서를 어떻게 두느냐로 두 방식이 갈린다.
- ETL: 저장소가 비싸고 계산이 약하던 시절의 방식이다. 별도 서버에서 정제·조인한 뒤 결과만 싣는다. 개인 정보처럼 저장소에 들어가면 안 되는 값을 미리 걸러 낼 때는 지금도 쓴다.
- ELT: 클라우드 웨어하우스가 저장과 계산을 따로 늘릴 수 있게 되면서 기본이 됐다. 원본(raw)을 그대로 싣고, 변환은 저장소 안에서 SQL로 한다. dbt 같은 도구가 이 SQL 변환을 버전 관리·테스트·의존 순서와 함께 다룬다.
- 층 나누기: 흔히 원본 → 정제(staging) → 모델(mart) 순으로 층을 둔다. 원본 층은 손대지 않고 남겨 두기 때문에, 변환에 버그가 있으면 고친 뒤 처음부터 다시 만들 수 있다. 레이크하우스에서는 같은 생각을 메달리온 구조라 부르며 브론즈(원본)·실버(검증)·골드(보강·집계) 층으로 이름 붙인다(Azure Databricks 문서). 층을 지날수록 구조와 품질이 나아진다는 뜻이다.
웨어하우스, 레이크, 레이크하우스
데이터 웨어하우스는 분석 질의에 맞춘 데이터베이스다. 들어올 때 스키마를 강제하고(schema-on-write), 저장 형식은 제품 안에 감춰져 있다. 데이터 레이크는 객체 저장소에 파일을 그대로 쌓는 곳이다. 싸고 무엇이든 넣을 수 있지만, 트랜잭션이 없어 쓰는 도중의 파일을 읽거나 같은 데이터를 두 번 넣는 일이 생기고, 관리하지 않으면 아무도 믿지 못하는 ‘데이터 늪’이 된다.
Armbrust 등(CIDR 2021)이 이름 붙인 레이크하우스는 둘을 합친다. 데이터는 객체 저장소의 Parquet 파일로 두고, 그 위의 테이블 포맷이 “지금 이 표는 어떤 파일들인가”를 원자적으로 관리한다. 파일 형식이 열려 있으니 Spark·Trino·DuckDB·웨어하우스 엔진이 같은 사본을 함께 읽고, ML 학습도 복사 없이 같은 파일을 쓴다.
열 지향 파일: Parquet
분석 질의는 행은 많이, 열은 몇 개만 읽는다. 그래서 같은 열의 값을 붙여 저장하는 열 지향 형식이 유리하다. Parquet는 Google Dremel의 중첩 열 저장 방식을 바탕으로 한 열린 형식이다. 파일은 행을 묶은 행 그룹(흔히 수십~수백 MB)으로 나뉘고, 행 그룹 안에서 열마다 열 청크가 이어지며, 열 청크는 다시 페이지로 나뉜다. 파일 끝의 푸터에 스키마와 행 그룹·열마다의 min/max 같은 통계가 있다.
그림의 질의는 행 그룹 세 개 중 1개, 열 세 개 중 두 개만 읽는다. 이 건너뛰기는 데이터가 조건 열로 정렬·군집돼 있을 때 효과가 크다. 날짜가 뒤섞여 있으면 모든 행 그룹의 min/max가 넓어져 하나도 건너뛰지 못한다.
열 하나에는 비슷한 값이 몰려 있어 인코딩이 잘 먹힌다. 국가 코드 열 1,000,000행(값 다섯 종류)으로 계산해 보자.
- PLAIN: 문자열마다 길이 4바이트 + 글자 2바이트라 6.0MB다.
- 사전 인코딩: 서로 다른 값 다섯 개를 사전 페이지에 한 번만 적고, 각 행에는 사전 번호만 둔다. 다섯 개를 구별하는 데 3비트면 되니 번호를 비트 단위로 빽빽하게 채우면(bit-packing) 약 375KB다. 사전이 너무 커지면 작성기는 PLAIN으로 되돌아간다.
- RLE: 같은 번호가 이어지면 “번호 × 반복 횟수”로 적는다. 국가별로 정렬해 두면 구간이 다섯 개뿐이라 번호 부분이 50바이트 남짓으로 준다.
인코딩 뒤에는 페이지 단위로 Snappy·ZSTD 같은 범용 압축을 한 번 더 건다. 숫자 열에는 이웃한 값의 차이만 적는 DELTA_BINARY_PACKED가 쓰인다. 같은 데이터라도 정렬 순서와 행 그룹 크기에 따라 파일 크기와 질의 속도가 몇 배씩 달라지는 이유가 여기 있다. 작은 파일이 수만 개 쌓이면 푸터를 여는 비용이 커지므로, 주기적으로 파일을 합치는 컴팩션도 필요하다.
열린 테이블 포맷: Iceberg, Delta Lake, Hudi
객체 저장소에는 “파일 여러 개를 한꺼번에 바꾸기”가 없다. 테이블 포맷은 데이터 파일을 고쳐 쓰지 않고, 표를 이루는 파일 목록을 담은 메타데이터를 새로 쓴 뒤 “현재 버전” 포인터 하나만 원자적으로 바꿔 이 문제를 푼다. 커밋 하나가 스냅숏 하나다. 독자는 시작할 때 본 스냅숏만 읽으므로 쓰는 도중의 파일을 보지 않는다.
- 시간 여행: 옛 스냅숏을 지정해 읽는다. Iceberg와 Delta Lake 모두 Spark SQL에서
TIMESTAMP AS OF·VERSION AS OF를 지원한다. 어제 보고서를 그때 데이터로 다시 돌리거나, 잘못된 적재를 이전 스냅숏으로 되돌릴 때 쓴다. - 만료와 정리: 스냅숏을 무한히 두면 옛 파일도 무한히 남는다. 보존 기간을 정해 스냅숏을 만료하고, 어떤 스냅숏도 가리키지 않는 파일을 지운다(Iceberg의
expire_snapshots, Delta의VACUUM). 지운 시점 이전으로는 시간 여행이 안 된다. - 스키마·파티션 진화: 열을 이름이 아니라 ID로 추적해 열 이름을 바꾸거나 순서를 바꿔도 옛 파일을 다시 쓰지 않는다. Iceberg는 파티션 규칙도 바꿀 수 있고, 질의는 파티션 열을 몰라도 된다(숨은 파티셔닝).
세 포맷은 출발점이 다르다. Iceberg는 Netflix에서 시작해 메타데이터 파일 → 매니페스트 목록 → 매니페스트의 나무 구조로 큰 표의 계획을 빠르게 하며, 명세 v3에서 삭제 벡터와 반정형 variant 타입이 더해졌다. Delta Lake는 Databricks에서 시작해 _delta_log 폴더에 커밋마다 JSON 로그를 쌓고 주기적으로 체크포인트를 남긴다. Hudi는 Uber에서 잦은 갱신(upsert)과 증분 읽기를 위해 시작했고, 쓸 때 다시 쓰는 copy-on-write와 변경 로그를 따로 쌓았다가 읽을 때 합치는 merge-on-read를 고를 수 있다.
오케스트레이션: DAG와 재시도
파이프라인은 “주문 추출과 사용자 추출이 끝나면 조인하고, 그다음 적재하고, 그다음 보고서를 만든다” 같은 의존 관계로 이뤄진다. 이를 방향 있고 순환 없는 그래프, DAG로 적고 스케줄러가 순서대로 돌린다. Apache Airflow가 대표적이며 2025년에 3.0이 나왔다.
DAG 실행이 만들어졌다. 아직 아무 태스크도 돌지 않았다.
- 태스크 상태: Airflow의 태스크 인스턴스는 실행 중(running)·성공(success)·재시도 대기(up_for_retry)·실패(failed)·위쪽 실패(upstream_failed) 같은 상태를 지난다. 네트워크 끊김 같은 일시적 실패는
retries와retry_delay로 흡수한다. - 멱등성: 재시도는 같은 태스크를 다시 돌린다는 뜻이다. “어제 데이터를 덧붙인다”는 두 번 돌면 중복이 생기고, “어제 파티션을 통째로 덮어쓴다”는 몇 번 돌아도 결과가 같다. 태스크는 덮어쓰기나 MERGE처럼 다시 돌려도 안전하게 짠다.
- 논리 날짜와 백필: 각 실행은 “어느 기간의 데이터를 처리하는가”를 나타내는 논리 날짜를 받는다. 지난 석 달을 새 로직으로 다시 만들 때(백필) 같은 DAG를 날짜만 바꿔 돌릴 수 있으려면, 태스크가 현재 시각이 아니라 이 날짜를 기준으로 동작해야 한다.
- 자산 중심: Dagster는 “어떤 작업을 돌리나”보다 “어떤 표(자산)가 무엇에서 만들어지나”를 먼저 적게 한다. 자산 그래프에서 실행 순서가 나오고, 어떤 표가 오래됐는지를 바로 볼 수 있다.
데이터 모델링: 스타 스키마와 SCD
분석용 표는 서비스 DB처럼 정규화하지 않는다. Kimball의 차원 모델링은 일어난 일을 숫자로 적는 사실 표와, 그 일을 설명하는 차원 표로 나눈다. 사실 표 가운데 차원 표가 둘러싼 모양이라 스타 스키마라 부른다.
- 그레인부터: 사실 표 한 행이 무엇인지(주문 한 건? 주문의 상품 한 줄? 하루 매장 합계?)를 가장 먼저 정한다. 그레인이 섞이면 합계가 두 번 세어진다.
- 적합 차원: 날짜·고객 같은 차원을 여러 사실 표가 같은 정의로 함께 쓰면, 매출과 반품을 같은 축으로 나란히 볼 수 있다.
- 넓은 표 하나: 열 지향 엔진은 조인보다 넓은 표 스캔이 싼 경우가 많아, 차원을 미리 붙인 큰 표(One Big Table)를 따로 만들기도 한다. 다만 원천 모델은 스타 스키마로 두고 그 결과로 만든다.
차원 값은 천천히 바뀐다. 고객이 이사하면 지난 매출은 어느 도시로 셀까? SCD(slowly changing dimension) 유형이 그 답이다. 1형은 값을 덮어써 역사를 버리고, 2형은 행을 새로 더해 역사를 남긴다.
데이터 품질과 데이터 계약
파이프라인 오류는 대개 조용하다. 작업은 성공했는데 표가 비어 있거나, 원천 팀이 열 이름을 바꿔 값이 모두 null이 되거나, 단위가 원에서 센트로 바뀐다. 그래서 데이터 자체를 검사한다. 흔히 보는 축은 신선도(마지막 갱신이 언제인가), 양(행 수가 평소 범위인가), 스키마(열과 타입이 그대로인가), 값(null·중복·허용 값·범위·참조 무결성)이다.
- 도구: dbt 데이터 테스트는
unique,not_null,accepted_values,relationships를 SQL 모델 옆에 적는다. Great Expectations는 기대값 묶음을 코드로 정의해 검증 보고서를 낸다. - 쓰고 검사하고 공개(write-audit-publish): 새 데이터를 숨은 곳(스테이징 표, 테이블 포맷의 브랜치)에 먼저 쓰고, 검사가 통과하면 그때 공개한다. 잘못된 데이터가 대시보드에 잠깐이라도 보이는 일을 막는다.
- 데이터 계약: 품질 검사가 쓰는 쪽에서 문제를 잡는다면, 계약은 만드는 쪽과 미리 약속한다. 스키마·의미·허용 값·갱신 주기·소유자를 문서로 적고, 원천 팀이 바꾸려면 버전을 올리고 알린다. Open Data Contract Standard는 이 문서를 YAML로 적는 공개 형식이다.
- 리니지: 대시보드 숫자가 이상할 때 가장 먼저 묻는 것은 "이 표는 어느 원천에서 어떤 작업을 거쳐 왔나"다. 표와 작업 사이의 이 계보를 데이터 리니지라 한다. 거꾸로 따라가면 원인을 찾고, 앞으로 따라가면 원천 열 하나를 바꿀 때 영향받는 표와 대시보드를 미리 안다. 오케스트레이터·엔진이 실행할 때마다 입력과 출력을 이벤트로 내보내게 하는 열린 명세가 OpenLineage다.
배치, 스트리밍, CDC
처리 주기는 데이터가 얼마나 늦게 보여도 되는지로 정한다. 일정 간격으로 도는 배치에서 이벤트가 아무 때나 들어온다고 보면, 평균 지연은 주기의 절반 + 실행 시간, 최악은 주기 + 실행 시간이다. 매일 한 번 한 시간짜리 배치면 평균 13시간, 최악 25시간이고, 15분마다 3분짜리 마이크로 배치면 평균 10.5분, 최악 18분이다.
보고서 대부분은 매일·매시간 배치로 충분하다. 사기 탐지나 실시간 재고처럼 초 단위가 필요할 때만 스트리밍을 고른다. 윈도, 워터마크, 정확히 한 번 처리 같은 스트림 처리의 속은 배치와 스트림 처리 노트에서 다룬다.
원천 DB에서 데이터를 가져오는 가장 쉬운 방법은 “updated_at이 지난번 이후인 행”을 주기적으로 질의하는 것이지만, 삭제된 행과 두 질의 사이의 중간 변경을 놓치고 원천 DB에 부하를 준다. CDC(change data capture)는 DB가 복제를 위해 이미 쓰는 트랜잭션 로그(PostgreSQL WAL, MySQL binlog)를 읽어 모든 변경을 순서대로 이벤트로 만든다. Debezium이 널리 쓰는 오픈 소스 커넥터다.
분석가를 위한 통계 기초
잘 만든 표도 잘못 읽으면 소용없다. 먼저 평균과 중앙값이다. 값 열 개 2, 3, 3, 4, 4, 4, 5, 5, 6, 40의 평균은 7.6이지만 중앙값은 4이다. 이상값 하나가 평균을 끌어올렸다. 응답 시간·매출·체류 시간처럼 꼬리가 긴 분포에서는 평균보다 중앙값과 백분위수가 ‘보통’을 더 잘 말한다.
- 분산과 표준편차: 평균에서 얼마나 퍼졌나를 잰다. 표본 분산은 편차 제곱의 합을 n이 아니라 n − 1로 나눈다(표본 평균을 이미 데이터로 맞춘 만큼 자유도가 하나 준다). 위 값들의 표본 표준편차는 11.4로, 이상값을 빼면 1.2이다.
- 표본: 전체를 다 볼 수 없으면 일부를 뽑아 추정한다. 무작위로 뽑아야 하고, “앱을 연 사람만”처럼 뽑는 방식이 결과와 얽히면 표본이 아무리 커도 치우친다(선택 편향). 집단 비율이 크게 다르면 층마다 따로 뽑는 층화 추출이 오차를 줄인다.
- 신뢰 구간: 표본 평균은 뽑을 때마다 흔들린다. 그 흔들림의 크기가 표준 오차 s / √n이고, 95% 신뢰 구간은 평균 ± 1.96 × 표준 오차다. 400명의 평균 체류가 52초, 표준편차가 30초라면 구간은 52 ± 2.9초다. 오차를 절반으로 줄이려면 표본이 네 배 필요하다(1,600명이면 ± 1.5초). 비율도 같다. 1만 명 중 500명이 전환했다면 5.0% ± 0.43%다.
A/B 테스트는 사용자를 무작위로 둘로 나눠 한쪽에만 변경을 보여 주고 차이를 잰다. 관찰된 차이가 우연으로 생길 만한 크기인지를 두 비율 z 검정으로 따진다. 두 그룹의 합동 비율로 표준 오차를 구하고, 차이를 그것으로 나눈 z에서 p값(차이가 없는데도 이만큼 이상 벌어질 확률)을 얻는다.
- 계산: 그룹당 1만 명에서 5.0% 대 5.7%(상대 상승 14%)이면 z = 2.20, p = 0.028로 5% 기준에서 유의하다. 같은 비율을 1천 명씩으로 재면 z = 0.70, p = 0.49라 차이가 있다고 말할 수 없다.
- 표본 크기는 미리: 기준 전환율 5%에서 10% 상대 상승(5.5%)을 유의 수준 5%, 검정력 80%로 잡아내려면 그룹당 약 31,231명이 필요하다. 작은 효과일수록 필요한 표본은 효과 크기의 제곱에 반비례해 늘어난다.
- 중간에 엿보지 않기: 매일 p값을 보다가 0.05 아래로 내려간 날 멈추면, 실제 효과가 없어도 ‘유의’를 얻을 확률이 5%를 훨씬 넘는다. 정한 표본에 도달할 때까지 기다리거나, 순차 검정처럼 중간 확인을 전제한 방법을 쓴다.
- 그 밖의 함정: 지표를 스무 개 보면 하나쯤은 우연히 유의하다(다중 비교). 두 그룹 인원이 설계한 비율(50:50)에서 크게 어긋나면 배정 자체가 고장 난 것이니 결과를 믿지 않는다(표본 비율 불일치). 이런 함정은 Kohavi 등의 책에 사례와 함께 정리돼 있다.
- 상관과 인과, 심슨의 역설: 무작위 배정이 없는 관찰 데이터에서 두 지표가 함께 움직여도 원인이라고 말할 수 없다. 둘 다를 움직이는 제3의 요인이 있을 수 있기 때문이다. 극단적인 경우가 심슨의 역설이다. 집단마다 나눠 보면 모두 한 방향인 차이가, 합쳐 보면 반대 방향으로 뒤집힌다. 집단마다 크기와 기준값이 다르기 때문이다. 잘 알려진 예가 1973년 가을 UC 버클리 대학원 입학 자료로, 전체로는 남성 합격률이 뚜렷이 높았지만, 학과별로 보정하면 오히려 여성 쪽으로 작은 차이가 나타났다. 여성이 합격률이 낮은 경쟁 학과에 더 많이 지원했기 때문이다. 합계를 보기 전에 집단별로 한 번 나눠 본다. A/B 테스트가 무작위 배정을 고집하는 이유도 이 제3의 요인을 양쪽에 고르게 나누려는 것이다.
핵심 자료
요지는 직접 정리한 한 줄이다.
| 자료 | 종류 | 한 줄 요지 |
|---|---|---|
| Apache Parquet 파일 형식 문서 | 공식 문서 | 행 그룹·열 청크·페이지와 푸터 메타데이터, 인코딩 목록을 정의한다. |
| Dremel (Melnik 등) | 논문 | 중첩 레코드를 열로 쪼개 저장하는 방법을 보였다. Parquet의 바탕이다. |
| Lakehouse (Armbrust 등, CIDR 2021) | 논문 | 열린 파일 위에 트랜잭션 메타데이터 층을 얹어 웨어하우스와 레이크를 합치자고 제안했다. |
| Apache Iceberg 테이블 명세 | 명세 | 스냅숏·매니페스트·메타데이터 파일의 구조와 버전별(v1~v3) 기능을 정한다. |
| Airflow 핵심 개념: DAG·태스크 | 공식 문서 | DAG 정의, 태스크 인스턴스 상태, 재시도와 의존 규칙을 설명한다. |
| Kimball 차원 모델링 기법 | 해설 | 그레인, 사실·차원 표, 적합 차원, SCD 유형을 짧은 항목으로 정리했다. |
| Designing Data-Intensive Applications (Kleppmann) | 책 | 저장 엔진, 인코딩, 배치·스트림 처리를 한 흐름으로 설명한다. |
| OpenIntro Statistics | 무료 교재 | 표본, 신뢰 구간, 비율 검정을 예제로 풀어 주는 공개 통계 교과서다. |
| Trustworthy Online Controlled Experiments (Kohavi 등) | 책 | 온라인 A/B 테스트를 설계·분석할 때의 함정(중간 확인, 표본 비율 불일치)을 다룬다. |
- Apache Parquet 파일 형식 문서
- Dremel (Melnik 등)
- Lakehouse (Armbrust 등, CIDR 2021)
- Apache Iceberg 테이블 명세
- Airflow 핵심 개념: DAG·태스크
- Kimball 차원 모델링 기법
- Designing Data-Intensive Applications (Kleppmann)
- OpenIntro Statistics
- Trustworthy Online Controlled Experiments (Kohavi 등)
- Parquet 인코딩 목록
- Iceberg — Spark 쿼리와 시간 여행
- Delta Lake — 테이블 읽고 쓰기·시간 여행
- Apache Hudi 개요
- Airflow 핵심 개념: DAG
- Apache Airflow 3.0 출시 글
- Dagster — 자산(asset)
- Kimball — 2형 천천히 변하는 차원
- dbt — 데이터 테스트
- Great Expectations 문서
- Open Data Contract Standard (Bitol)
- Debezium 문서
- Azure Databricks — 메달리온 레이크하우스 구조
- OpenLineage 문서
- Simpson's paradox — Wikipedia