0. 들어가며: 데이터 엔지니어는 왜 데이터베이스 내부를 알아야 할까?
데이터 엔지니어링 업무를 하다 보면 다음과 같은 상황을 자주 경험한다.
- 똑같은 SQL인데 Oracle에서는 빠르고 Impala에서는 느리다.
- 수억 건의 데이터에서 특정 고객을 조회하는 것은 빠른데, 월별 매출을 집계하면 시간이 오래 걸린다.
- 데이터가 계속 적재되는 테이블에서 조회 성능이 점점 나빠진다.
- ETL 파이프라인을 수정하지 않았는데 컬럼이 추가되면서 오류가 발생한다.
- 운영 중인 시스템을 중단하지 않고 새로운 버전으로 변경해야 한다.
이런 문제를 해결하려면 SQL 문법뿐 아니라 데이터가 저장되고 검색되는 구조, 시스템 간 데이터를 주고받는 방식까지 이해해야 한다.
이번 글에서는 《데이터 중심 애플리케이션 설계》의 3장과 4장을 중심으로 데이터 엔지니어가 알아야 할 개념을 정리한다.
핵심은 두 가지다.
- 데이터를 어떻게 저장하고 검색하느냐에 따라 시스템의 성능이 달라진다.
- 데이터를 어떤 형식으로 주고받느냐에 따라 시스템의 확장성과 호환성이 달라진다.
PART 1. 데이터 저장소와 검색
1. OLTP와 OLAP는 무엇이 다를까?
데이터베이스를 이해할 때 가장 먼저 알아야 할 개념이 OLTP와 OLAP다.
두 시스템은 모두 데이터를 다루지만 목적이 다르다.
1-1. OLTP (Online Transaction Processing)
OLTP는 온라인 트랜잭션 처리 시스템이다.
쉽게 말해 사용자가 서비스를 이용할 때 발생하는 데이터를 빠르고 정확하게 처리하는 시스템이다.
예를 들어 쇼핑몰을 생각해 보자.
고객이 상품을 주문하면 다음 작업이 발생한다.
- 주문 정보를 저장한다.
- 결제 상태를 변경한다.
- 재고를 차감한다.
- 주문 상태를 조회한다.
이때 필요한 데이터는 전체 주문 데이터 중 극히 일부다.
SELECT *
FROM ORD_ORD_BSC_M
WHERE ORD_ID = 123456;
위 SQL은 특정 주문번호에 해당하는 데이터를 찾는다.
OLTP 시스템에서 중요한 것은 다음과 같다.
- 특정 행을 빠르게 검색하는 것
- 소량의 데이터를 빠르게 추가하거나 수정하는 것
- 여러 사용자의 동시 요청을 처리하는 것
- 트랜잭션의 정확성을 보장하는 것
대표적인 관계형 OLTP 데이터베이스로 Oracle, PostgreSQL, MySQL 등이 있다. 다만 실제 사용 목적은 데이터베이스 제품 자체보다 구성과 워크로드에 따라 달라진다.
1-2. OLAP (Online Analytical Processing)
OLAP는 온라인 분석 처리 시스템이다.
OLTP가 개별 주문을 처리한다면 OLAP는 수많은 주문 데이터를 분석해 의미 있는 정보를 도출하는 것에 집중한다.
예를 들어 데이터분석팀에서는 다음과 같은 질문을 받는다.
- 최근 12개월간 월별 매출은 얼마인가?
- 신규회원의 첫 구매 전환율은 어떻게 변화했는가?
- 상품 대분류별 매출 비중은 얼마인가?
- 8월 대비 9월 매출이 감소한 원인은 무엇인가?
이런 질문은 몇 개의 행만 읽어서 해결할 수 없다.
SELECT
FROM_TIMESTAMP(ORD_ACP_DTM, 'yyyyMM') AS BASE_YM,
SUM(ORD_AMT) AS SALES_AMT
FROM CCIP.ORD_ORD_BSC_M
WHERE ORD_ACP_DTM >= CAST('2026-01-01' AS TIMESTAMP)
AND ORD_ACP_DTM < CAST('2027-01-01' AS TIMESTAMP)
GROUP BY FROM_TIMESTAMP(ORD_ACP_DTM, 'yyyyMM');
Impala 문법을 사용한 개념 설명용 예시이며, 실제 매출 집계에는 주문·상세 상태와 금액 산식 등의 업무 조건을 추가해야 한다.
이 쿼리는 특정 주문 한 건이 아니라 1년 동안의 주문 데이터를 집계한다.
이처럼 OLAP는 대량의 데이터를 읽고, 필터링하고, 집계하는 작업에 최적화된다.
1-3. OLTP vs OLAP 비교
구분OLTPOLAP
| 주요 목적 | 서비스 운영 | 데이터 분석 |
| 사용자 | 고객, 운영 시스템 | 분석가, BI, 데이터 엔지니어 |
| 작업 형태 | INSERT, UPDATE, 단건 SELECT | SELECT, JOIN, GROUP BY |
| 조회 범위 | 소량의 행 | 대량의 행 |
| 주요 관심사 | 낮은 지연시간, 정확성 | 처리량, 대량 집계 성능 |
| 저장 방식 | 주로 행 지향 | 주로 컬럼 지향 |
| 예시 | 주문·결제 시스템 | 매출 분석·KPI 리포트 |
1-4. 실무에서는 어떻게 연결될까?
쇼핑몰의 전체 데이터 흐름을 예로 들어보자.
고객이 상품 주문
↓
운영 DB(OLTP)
↓
CDC / 배치 ETL
↓
데이터 레이크 또는 데이터 웨어하우스
↓
분석 엔진(OLAP)
↓
월별 매출 / 고객 분석 / Power BI
운영 DB는 실시간 주문을 처리하고, 분석 시스템은 누적된 주문 데이터를 분석한다.
이 둘을 분리하는 중요한 이유는 대량 분석 쿼리가 운영 서비스의 성능에 영향을 주지 않도록 하기 위해서다.
실무 포인트
운영 데이터베이스의 모든 데이터를 분석하려면 운영 DB에 무거운 SQL을 실행하기보다 분석용 저장소로 데이터를 복제하거나 적재하는 구조가 일반적으로 적합하다.
2. 데이터베이스는 데이터를 어떻게 찾을까?
데이터베이스에 1억 건의 데이터가 있다고 가정하자.
우리는 다음 쿼리를 실행한다.
SELECT *
FROM CUSTOMER
WHERE CST_ID = 10001;
데이터베이스는 어떻게 1억 건 중 고객 한 명을 찾을까?
가장 단순한 방법은 모든 데이터를 처음부터 읽는 것이다.
이 방식을 **전체 스캔(Full Scan)**이라고 한다.
하지만 데이터를 매번 전부 읽으면 많은 시간이 소요될 수 있다.
이 문제를 해결하기 위해 사용하는 대표적인 기술이 인덱스(Index)다.
2-1. 인덱스란?
인덱스는 책의 맨 뒤에 있는 찾아보기와 비슷하다.
책에서 특정 단어를 찾을 때 모든 페이지를 읽는 대신 찾아보기에서 페이지 번호를 확인한다.
데이터베이스 인덱스도 같은 목적을 가진다.
인덱스
10001 → 데이터 위치 A
10002 → 데이터 위치 B
10003 → 데이터 위치 C
특정 고객을 찾을 때 전체 데이터를 읽지 않고 인덱스를 이용해 해당 위치로 접근할 수 있다.
다만 인덱스는 무료가 아니다.
인덱스를 저장하기 위한 추가 공간이 필요하고, 새로운 데이터가 입력될 때 인덱스도 관리해야 한다.
즉, 조회 성능을 개선하는 대신 저장 공간과 쓰기 비용을 추가로 사용하는 구조다.
3. Hash Index와 B-Tree
3-1. Hash Index
Hash Index는 키를 이용해 데이터 위치를 찾는 방식이다.
예를 들어 다음과 같은 정보를 저장한다.
Key Value
10001 → 저장 위치 A
10002 → 저장 위치 B
10003 → 저장 위치 C
Hash Index는 특정 키를 정확히 찾는 작업에 적합하다.
SELECT *
FROM CUSTOMER
WHERE CST_ID = 10001;
반면 다음과 같은 범위 검색에는 일반적인 해시 인덱스가 적합하지 않다.
SELECT *
FROM CUSTOMER
WHERE CST_ID BETWEEN 10000 AND 20000;
해시값이 원래 키의 정렬 순서를 보존하지 않기 때문이다.
이때 활용할 수 있는 것이 B-Tree다.
3-2. B-Tree란?
B-Tree는 데이터를 정렬된 상태로 관리하는 균형 잡힌 트리 자료구조다.
예를 들어 고객 ID가 다음과 같이 존재한다고 가정하자.
[50]
/ \
[20, 35] [70, 90]
실제 B-Tree 노드는 보통 여러 개의 키와 자식 포인터를 가지며, 위 그림은 검색 원리를 단순화한 것이다.
60이라는 키를 찾는다면 루트에서 어느 방향으로 이동해야 하는지 판단할 수 있다.
B-Tree는 한 번에 모든 데이터를 확인하지 않고 필요한 부분을 찾아간다.
B-Tree의 장점
- 특정 키 검색
- 범위 검색
- 정렬된 데이터 접근
- 디스크 기반 데이터베이스에서 효율적인 탐색
관계형 데이터베이스에서는 B-Tree 계열 인덱스가 널리 사용된다.
실무 예시
SELECT *
FROM ORDERS
WHERE ORD_DATE >= DATE '2026-09-01'
AND ORD_DATE < DATE '2026-10-01';
주문일에 적절한 B-Tree 인덱스가 있다면 특정 기간의 데이터를 효율적으로 찾는 데 도움이 될 수 있다.
하지만 인덱스가 항상 좋은 것은 아니다.
조회 대상이 전체 데이터의 상당 부분을 차지한다면 인덱스를 여러 번 따라가는 것보다 전체 스캔이 더 효율적일 수도 있다.
4. LSM Tree는 무엇일까?
LSM Tree(Log-Structured Merge Tree)는 많은 양의 쓰기 작업을 효율적으로 처리하기 위한 저장 구조다.
책에서 설명하는 중요한 아이디어는 다음과 같다.
데이터를 매번 디스크의 정해진 위치에 수정하는 대신, 순차적으로 기록하고 나중에 정리한다.
4-1. 왜 순차 쓰기가 중요할까?
디스크에 데이터를 저장할 때 무작위 위치를 반복해서 수정하면 많은 I/O 비용이 발생할 수 있다.
반대로 데이터를 연속적인 위치에 기록하면 효율적으로 처리할 수 있다.
LSM 계열 저장소는 이런 특성을 활용한다.
4-2. LSM Tree의 기본 구조
새로운 데이터 입력
↓
WAL에 기록
↓
MemTable(메모리)
↓
Flush 발생
↓
SSTable(디스크)
↓
Compaction
↓
정리된 SSTable
각 구성요소를 하나씩 이해해 보자.
① WAL (Write-Ahead Log)
WAL은 변경사항을 먼저 기록하는 로그다.
메모리에만 데이터를 저장하면 서버 장애가 발생했을 때 데이터가 사라질 수 있다.
따라서 변경사항을 로그에 남겨 장애 이후 복구할 수 있도록 한다.
② MemTable
MemTable은 메모리에 데이터를 보관하는 자료구조다.
새로운 데이터가 들어오면 메모리에서 관리하고, 일정 크기에 도달하면 디스크에 기록한다.
③ SSTable (Sorted String Table)
SSTable은 키 기준으로 정렬된 데이터를 저장하는 파일이다.
기존 파일을 계속 직접 수정하는 대신 새로운 파일을 생성하는 방식으로 동작할 수 있다.
④ Compaction
시간이 지나면 SSTable 파일이 여러 개 생긴다.
Compaction은 여러 파일을 병합하고 불필요한 이전 버전의 데이터를 정리하는 과정이다.
예를 들어 다음과 같은 기록이 있다고 하자.
SSTable 1 : 고객 A → 일반회원
SSTable 2 : 고객 A → VIP회원
SSTable 3 : 고객 A → 우수회원
새로운 버전이 이전 버전을 대체한다는 가정하에, 병합 과정에서 더 이상 필요하지 않은 과거 버전을 제거할 수 있다.
다만 실제 저장소에서는 동시 읽기, 스냅샷, 삭제 마커 등에 따라 유지 여부가 달라진다.
4-3. LSM Tree의 장단점
장점단점
| 대량 쓰기 처리에 유리 | Compaction에 추가 I/O 발생 |
| 순차적 디스크 쓰기 활용 | 읽기 시 여러 파일을 확인할 수 있음 |
| 파일 기반 데이터 관리 | 쓰기·읽기·공간 증폭 관리 필요 |
| 높은 쓰기 처리량 구현 가능 | 성능 튜닝이 복잡할 수 있음 |
LSM Tree 계열 구조는 RocksDB, LevelDB, HBase 등의 저장소 기술을 이해할 때 중요하다.
실무 포인트
HBase 계열 저장소에서 쓰기 성능이 좋다가 특정 시점에 I/O 부하가 증가한다면 Compaction이나 Flush 작업이 원인 중 하나일 수 있다.
5. Bloom Filter는 무엇일까?
LSM Tree에서는 데이터가 여러 SSTable에 분산되어 있을 수 있다.
특정 키를 찾을 때 모든 SSTable을 확인하면 비효율적이다.
이때 활용하는 기술 중 하나가 Bloom Filter다.
Bloom Filter는 특정 데이터가 집합에 존재하지 않는지를 빠르게 판단하기 위한 확률적 자료구조다.
예를 들어 다음과 같이 동작한다.
고객 10001이 SSTable A에 존재하는가?
Bloom Filter 확인
↓
없음 → 해당 SSTable 탐색 생략
있을 가능성 → 실제 SSTable 확인
일반적인 Bloom Filter에서는 다음 특성이 중요하다.
- 없다고 판단하면 실제로도 없다.
- 있을 수 있다고 판단해도 실제로는 없을 수 있다.
두 번째 경우를 False Positive(거짓 양성)라고 한다.
Bloom Filter는 디스크 접근을 줄여 읽기 성능을 개선하는 데 사용된다.
데이터 엔지니어 입장에서는 Parquet 같은 분석용 파일 형식이나 LSM 기반 저장소에서 활용되는 필터 최적화를 이해하는 데 도움이 된다. 다만 모든 Parquet 파일이나 조회 엔진이 Bloom Filter를 사용하는 것은 아니다.
6. B-Tree와 LSM Tree는 언제 사용할까?
두 방식의 가장 큰 차이는 데이터를 관리하는 철학이다.
구분B-TreeLSM Tree
| 핵심 구조 | 정렬된 페이지 기반 트리 | 메모리와 정렬된 파일 |
| 데이터 수정 | 기존 페이지 갱신 가능 | 새로운 기록과 병합 중심 |
| 쓰기 처리 | 무작위 쓰기가 발생할 수 있음 | 순차 쓰기 활용 |
| 읽기 | 트리 탐색 | 메모리·여러 파일 탐색 |
| 추가 관리 | 페이지 분할·병합 등 | Flush·Compaction |
| 대표 예시 | Oracle·PostgreSQL의 B-Tree 계열 인덱스 | RocksDB·LevelDB |
어느 한쪽이 무조건 빠르지는 않다.
데이터의 크기, 읽기와 쓰기의 비율, 저장 장치, 캐시, 구현 방식에 따라 결과가 달라진다.
따라서 시스템을 설계할 때는 다음과 같은 질문이 필요하다.
우리 시스템은 데이터를 자주 읽는가, 자주 쓰는가? 어떤 방식으로 읽고 쓰는가?
7. Row-oriented Storage와 Column-oriented Storage
데이터 분석 업무에서 매우 중요한 개념이다.
데이터를 행(Row) 중심으로 저장하는지, 열(Column) 중심으로 저장하는지에 따라 쿼리 성능이 크게 달라진다.
7-1. 행 지향 저장소
다음 데이터가 있다고 가정하자.
고객ID상품명수량매출액
| 1001 | 우유 | 2 | 4,000 |
| 1002 | 계란 | 1 | 6,000 |
| 1003 | 커피 | 3 | 9,000 |
행 지향 저장소에서는 개념적으로 다음과 같이 저장된다.
[1001, 우유, 2, 4000]
[1002, 계란, 1, 6000]
[1003, 커피, 3, 9000]
고객 1001의 주문 전체를 조회한다면 필요한 값들이 함께 저장되어 있어 효율적이다.
7-2. 컬럼 지향 저장소
컬럼 지향 저장소는 같은 컬럼의 값을 모아서 저장한다.
고객ID : [1001, 1002, 1003]
상품명 : [우유, 계란, 커피]
수량 : [2, 1, 3]
매출액 : [4000, 6000, 9000]
이 구조는 다음과 같은 분석에 유리하다.
SELECT SUM(SALES_AMT)
FROM SALES;
매출액만 필요하기 때문에 다른 컬럼을 읽지 않아도 된다.
7-3. 컬럼 지향 저장소가 분석에 유리한 이유
첫 번째는 필요한 컬럼만 읽을 수 있다는 점이다.
테이블에 컬럼이 100개 있어도 쿼리가 사용하는 컬럼이 3개라면, 적절한 저장 형식과 엔진에서는 필요한 컬럼만 읽을 수 있다.
두 번째는 압축 효율이다.
같은 컬럼에는 유사한 데이터가 많이 존재한다.
예를 들어 다음과 같다.
회원구분 : 신규, 신규, 신규, 기존, 기존
연속적인 동일 값이나 반복 패턴은 효율적으로 압축할 수 있다.
이때 Run-Length Encoding(RLE), Dictionary Encoding 등의 기법이 활용될 수 있다.
RLE는 같은 값이 연속해서 나오는 경우 값과 반복 횟수를 저장하는 방법이다.
원본 : A A A A B B C C C
압축 : A×4, B×2, C×3
Dictionary Encoding은 반복되는 값을 작은 코드로 치환한다.
신규 → 1
기존 → 2
원본 : 신규, 기존, 신규
저장 : 1, 2, 1
데이터 크기가 줄어들면 디스크에서 읽어야 할 데이터도 감소할 수 있다.
7-4. Impala와 Parquet 실무 예시
Cloudera 환경에서 Impala와 Parquet를 사용하는 상황을 생각해 보자.
SELECT
BSS_YM,
SUM(SALES_AMT)
FROM DM.MONTHLY_SALES
WHERE BSS_YM = '202609'
GROUP BY BSS_YM;
Parquet 기반 테이블이라면 컬럼 단위 읽기와 압축이 집계 성능에 도움이 된다.
다만 컬럼 지향 저장소라고 해서 모든 SQL이 자동으로 빨라지는 것은 아니다.
파티션 설계, 파일 크기, 통계정보, 조인 방식, 실제 스캔량도 중요하다.
특히 Parquet처럼 대량 분석에 적합한 저장 형식과 Impala 같은 분석 엔진을 함께 사용하는 이유를 이해할 수 있다.
8. Data Warehouse와 데이터 모델링
데이터 웨어하우스(Data Warehouse)는 여러 시스템에 존재하는 데이터를 분석하기 위해 통합하는 저장 환경이다.
예를 들어 기업에는 다음과 같은 데이터가 존재한다.
CRM → 고객 정보
주문 DB → 주문 정보
결제 DB → 결제 정보
상품 DB → 상품 정보
각 시스템을 따로 조회하면 고객별 매출을 분석하기 어렵다.
따라서 데이터를 수집하고 변환하여 분석하기 쉬운 형태로 저장한다.
원천 시스템
↓
데이터 수집
↓
데이터 정제 및 변환
↓
데이터 웨어하우스
↓
BI / 통계 / KPI 분석
8-1. Fact Table과 Dimension Table
데이터 웨어하우스에서는 Fact와 Dimension을 구분하는 모델링 방식이 자주 사용된다.
**Fact Table(사실 테이블)**은 측정 가능한 실적 데이터를 저장한다.
예시:
- 주문건수
- 매출액
- 주문수량
- 할인금액
**Dimension Table(차원 테이블)**은 실적을 어떤 기준으로 분석할지 정의한다.
예시:
- 고객
- 상품
- 날짜
- 지점
- 판매채널
상품 차원
|
고객 차원 — 매출 Fact — 날짜 차원
|
채널 차원
이런 구조를 Star Schema(스타 스키마)라고 한다.
8-2. Grain이란?
Fact Table을 설계할 때 반드시 알아야 할 개념이다.
Grain은 테이블에서 한 행이 무엇을 의미하는지를 뜻한다.
예를 들어 다음 두 테이블은 서로 다르다.
주문 상세 Fact
1행 = 주문번호 + 상품번호
월별 매출 Fact
1행 = 기준월 + 상품번호
Grain이 다르면 같은 고객수를 계산하더라도 중복 여부가 달라질 수 있다.
실무에서 자주 발생하는 문제는 서로 다른 Grain의 테이블을 조인하여 매출액이 중복 집계되는 것이다.
따라서 분석 테이블을 설계할 때는 먼저 다음 질문을 해야 한다.
이 테이블에서 한 행은 정확히 무엇을 의미하는가?
9. Data Cube는 무엇일까?
Data Cube(데이터 큐브)는 여러 차원으로 집계한 데이터를 효율적으로 조회하기 위한 개념이다.
예를 들어 매출 데이터를 다음 기준으로 분석한다고 하자.
- 날짜
- 상품
- 지역
원본 주문 데이터를 매번 읽는 대신 미리 집계된 결과를 활용할 수 있다.
2026년 9월 / 우유 / 서울 → 1,000만 원
2026년 9월 / 계란 / 서울 → 800만 원
2026년 9월 / 우유 / 부산 → 600만 원
이를 이용하면 날짜별, 상품별, 지역별 집계에 빠르게 접근할 수 있다.
9-1. ROLLUP과 CUBE
SQL에서는 ROLLUP, CUBE, GROUPING SETS 등을 사용해 다양한 집계 조합을 생성할 수 있다.
ROLLUP은 계층적인 소계와 총계를 생성하는 데 사용된다.
SELECT
BASE_YM,
PRD_LCLS_NM,
SUM(SALES_AMT)
FROM SALES
GROUP BY ROLLUP(BASE_YM, PRD_LCLS_NM);
반면 CUBE는 지정한 차원들의 가능한 집계 조합을 생성한다.
단, 지원 여부와 문법은 데이터베이스마다 다르다.
실무 포인트
일별·월별 KPI를 반복해서 산출한다면 원천 데이터 전체를 매번 집계하기보다 미리 계산한 집계 테이블을 활용하는 것이 효율적일 수 있다.
다만 데이터 큐브는 모든 지표에 적합하지 않다.
특히 고객수처럼 COUNT(DISTINCT)가 필요한 지표는 월별 값을 단순 합산하면 중복 고객이 발생할 수 있으므로 주의해야 한다.
PART 2. 데이터 부호화와 발전
10. 데이터 부호화(Encoding)는 무엇일까?
서로 다른 시스템이 데이터를 주고받으려면 데이터를 일정한 형식으로 표현해야 한다.
예를 들어 Python에서 다음과 같은 데이터가 있다고 하자.
customer = {
"cst_id": 10001,
"name": "Kim",
"grade": "VIP"
}
이 객체를 다른 서버로 전달하거나 파일에 저장하려면 공통 형식으로 변환해야 한다.
이 과정을 직렬화(Serialization) 또는 부호화(Encoding)라고 한다.
반대로 저장된 데이터를 다시 프로그램에서 사용할 수 있는 객체로 복원하는 과정을 역직렬화(Deserialization) 또는 복호화(Decoding)라고 한다.
Python 객체
↓ 직렬화
JSON / Avro / Protobuf
↓ 전송 또는 저장
다른 서버
↓ 역직렬화
프로그램 객체
여기서 말하는 부호화는 데이터를 표현 형식으로 바꾸는 것이며, 비밀을 보호하기 위한 암호화(Encryption)와는 다르다.
11. JSON, XML, CSV의 차이
데이터 엔지니어가 가장 자주 접하는 형식이다.
11-1. JSON
JSON은 키와 값으로 데이터를 표현하는 텍스트 형식이다.
{
"cst_id": 10001,
"name": "Kim",
"grade": "VIP"
}
장점은 사람이 읽기 쉽고 웹 API에서 널리 사용된다는 것이다.
다만 숫자 정밀도, 날짜·시간, 바이너리 값 등을 표현할 때는 생산자와 소비자 간 별도 규칙이 필요할 수 있다.
11-2. XML
XML은 태그를 이용해 데이터를 표현한다.
<customer>
<cst_id>10001</cst_id>
<name>Kim</name>
<grade>VIP</grade>
</customer>
문서 중심 데이터나 일부 기업 시스템에서 사용된다.
구조와 메타데이터를 상세하게 표현할 수 있지만 표현량이 많아질 수 있다.
11-3. CSV
CSV는 행과 열 형태의 데이터를 구분자로 저장한다.
cst_id,name,grade
10001,Kim,VIP
10002,Lee,NORMAL
Excel이나 데이터 분석 도구와 쉽게 연동할 수 있다.
하지만 컬럼의 데이터 타입이나 복잡한 중첩 구조를 자체적으로 명확히 표현하기 어렵다.
예를 들어 "00123"을 숫자로 해석하면 앞의 0이 사라질 수 있다.
또한 쉼표, 따옴표, 줄바꿈을 포함하는 값을 처리할 때는 올바른 CSV 규칙과 파서가 필요하다.
11-4. 비교
구분JSONXMLCSV
| 기본 구조 | 키-값 | 태그 | 행-열 |
| 중첩 데이터 | 지원 | 지원 | 기본적으로 부적합 |
| 사람이 읽기 | 쉬움 | 비교적 쉬움 | 쉬움 |
| 스키마 정의 | 별도 규칙 가능 | 별도 규칙 가능 | 별도 규칙 필요 |
| 주요 활용 | API·로그 | 시스템 연동·문서 | 파일 교환·분석 |
12. Avro와 Protocol Buffers는 왜 사용할까?
JSON은 사용하기 쉽지만 데이터 크기나 스키마 관리 측면에서 한계가 있을 수 있다.
이 문제를 해결하기 위해 스키마 기반의 바이너리 직렬화 방식이 사용된다.
대표적으로 다음 두 가지가 있다.
- Apache Avro
- Protocol Buffers(Protobuf)
12-1. 스키마란?
스키마는 데이터의 구조를 정의하는 규칙이다.
예를 들어 다음 데이터가 있다고 하자.
{
"cst_id": 10001,
"age": 30
}
이 데이터의 스키마는 다음과 같이 설명할 수 있다.
cst_id : 정수형
age : 정수형
즉, 데이터를 어떤 구조와 타입으로 해석해야 하는지 알려준다.
12-2. Avro
Avro는 스키마 기반 직렬화 형식이다.
예를 들어 다음과 같이 스키마를 정의할 수 있다.
{
"type": "record",
"name": "Customer",
"fields": [
{"name": "cst_id", "type": "long"},
{"name": "name", "type": "string"}
]
}
Avro는 데이터 구조를 스키마로 정의하고, 읽는 쪽과 쓰는 쪽의 스키마를 비교하여 호환성을 처리할 수 있다는 특징이 있다.
Kafka와 같은 이벤트 기반 데이터 파이프라인에서 활용되는 경우가 많다.
12-3. Protocol Buffers
Protobuf는 필드 번호와 데이터 타입을 정의해 데이터를 직렬화한다.
syntax = "proto3";
message Customer {
int64 cst_id = 1;
string name = 2;
}
필드 번호는 메시지의 바이너리 표현에서 중요한 역할을 한다.
Protobuf는 서비스 간 통신이나 gRPC에서 자주 활용된다.
12-4. 왜 바이너리 형식을 사용할까?
JSON에서는 데이터의 필드 이름이 반복적으로 등장한다.
반면 스키마 기반 바이너리 형식은 미리 정의된 구조를 활용하여 필드 이름을 반복해서 기록할 필요를 줄일 수 있다.
따라서 데이터 크기와 직렬화 처리 비용을 줄이는 데 도움이 될 수 있다.
단, 실제 크기와 속도는 데이터의 특성, 압축 여부, 구현 방식에 따라 달라진다.
13. 스키마 발전(Schema Evolution)
데이터 엔지니어가 반드시 이해해야 하는 개념이다.
서비스가 처음 만들어졌을 때 고객 정보가 다음과 같다고 하자.
{
"cst_id": 10001,
"name": "Kim"
}
이후 서비스가 확장되면서 회원등급을 추가한다.
{
"cst_id": 10001,
"name": "Kim",
"grade": "VIP"
}
그런데 운영 환경에는 이전 버전과 새로운 버전의 애플리케이션이 동시에 존재할 수 있다.
새로운 필드가 생겼다고 해서 모든 시스템을 한순간에 변경할 수 있는 것은 아니다.
이 때문에 스키마 변경 시 호환성을 고려해야 한다.
14. 하위 호환성과 상위 호환성
두 용어는 자주 혼동되므로 정확히 이해해야 한다.
14-1. 하위 호환성(Backward Compatibility)
새로운 버전의 프로그램이 이전 버전의 데이터를 읽을 수 있는 것이다.
과거 데이터
↓
새로운 프로그램
↓
정상 처리
예를 들어 과거에는 고객등급 컬럼이 없었다.
{
"cst_id": 10001
}
새로운 프로그램에서는 고객등급을 사용하지만, 이 값이 없으면 기본값을 적용하도록 설계할 수 있다.
grade = customer.get("grade", "NORMAL")
이렇게 하면 이전 데이터도 처리할 수 있다.
14-2. 상위 호환성(Forward Compatibility)
이전 버전의 프로그램이 새로운 버전의 데이터를 읽을 수 있는 것이다.
새로운 데이터
↓
이전 프로그램
↓
정상 처리
예를 들어 새로운 데이터에 grade 필드가 추가되었다고 하자.
이전 버전의 프로그램이 알 수 없는 필드를 무시하도록 구현되어 있다면 정상적으로 처리할 수 있다.
다만 모든 형식과 라이브러리가 이러한 동작을 자동으로 보장하지는 않는다.
14-3. 비교
구분의미예시
| Backward | 새 코드가 옛 데이터 읽기 | 신규 ETL이 과거 파일 처리 |
| Forward | 옛 코드가 새 데이터 읽기 | 기존 ETL이 신규 컬럼을 무시 |
실무 포인트
신규 컬럼 추가는 상대적으로 호환성을 유지하기 쉬운 변경이다.
반면 컬럼 삭제, 데이터 타입 변경, 기존 필드의 의미 변경은 기존 시스템에 문제를 일으키기 쉽다.
15. Rolling Upgrade(순차적 업그레이드)
서비스가 여러 서버에서 실행되고 있다고 하자.
Server A : Version 1
Server B : Version 1
Server C : Version 1
새로운 버전을 배포하기 위해 모든 서버를 동시에 종료하면 서비스가 중단될 수 있다.
따라서 일부 서버부터 차례대로 업데이트하는 방식을 사용한다.
1단계
A : V2
B : V1
C : V1
2단계
A : V2
B : V2
C : V1
3단계
A : V2
B : V2
C : V2
이것을 순차적 업그레이드라고 한다.
핵심 문제는 전환 기간에 서로 다른 버전이 공존한다는 점이다.
따라서 V1과 V2가 서로의 데이터를 읽을 수 있어야 정상적으로 서비스가 동작할 가능성이 높아진다.
15-1. ETL 파이프라인에서도 비슷한 문제가 발생한다
예를 들어 다음과 같은 파이프라인이 있다고 하자.
주문 시스템
↓
Kafka
↓
ETL Consumer
↓
데이터 웨어하우스
주문 시스템에서 이벤트 스키마를 변경했는데 ETL Consumer는 기존 스키마만 처리할 수 있다면 적재 오류가 발생한다.
이를 예방하기 위해 다음과 같은 접근을 사용할 수 있다.
- 새로운 필드를 선택적으로 처리할 수 있도록 소비자 코드를 변경한다.
- 이전 데이터에 대해서는 기본값이나 NULL 처리를 준비한다.
- 소비자 배포 및 검증을 완료한다.
- 생산자에서 새로운 데이터 형식을 사용한다.
- 모든 시스템이 전환된 이후 필요하다면 오래된 필드를 제거한다.
이런 방식을 흔히 확장 후 축소(Expand and Contract) 방식이라고 부른다.
PART 3. 시스템 간 데이터 교환
16. REST API와 RPC
데이터 시스템은 서로 독립적으로 존재하지 않는다.
주문 시스템, 회원 시스템, 결제 시스템, 분석 시스템은 데이터를 주고받는다.
이때 많이 사용하는 방식이 API와 RPC다.
16-1. REST API
REST는 네트워크 애플리케이션 설계를 위한 아키텍처 스타일이다.
REST API에서는 보통 HTTP를 이용하여 리소스에 접근한다.
예를 들어 특정 고객의 정보를 조회한다고 하자.
GET /customers/10001
서버가 JSON 형식으로 데이터를 응답할 수 있다.
{
"cst_id": 10001,
"grade": "VIP"
}
REST API는 다른 시스템과 데이터를 연동하거나 외부 데이터를 수집하는 데 많이 사용된다.
16-2. RPC(Remote Procedure Call)
RPC는 다른 서버에 있는 함수를 마치 호출하듯 요청하는 방식이다.
서비스 A
↓ getCustomer(10001)
서비스 B
↓
고객정보 반환
대표적인 기술로 gRPC가 있으며, Protobuf를 사용하는 구성이 흔하다.
RPC의 핵심은 원격에 있는 로직을 호출할 수 있게 한다는 것이다.
그러나 로컬 함수 호출과 달리 네트워크 지연, 연결 실패, 타임아웃, 중복 요청 등의 문제가 발생할 수 있다.
따라서 RPC를 설계할 때는 실패와 재시도 정책이 중요하다.
17. 메시지 브로커와 비동기 처리
실시간 데이터 파이프라인을 구성할 때 중요한 개념이다.
예를 들어 고객이 주문하면 다음 시스템이 주문 정보를 사용해야 한다고 하자.
- 결제 시스템
- 재고 시스템
- 배송 시스템
- 데이터 분석 시스템
모든 시스템이 주문 서버를 동시에 호출하면 서버 간 의존성이 커질 수 있다.
이를 줄이기 위해 메시지 브로커를 사용할 수 있다.
주문 시스템
↓
메시지 브로커
/ | \
↓ ↓ ↓
재고 배송 데이터 분석
메시지 브로커는 생산자(Producer)가 발행한 메시지를 소비자(Consumer)에게 전달하는 역할을 한다.
17-1. Producer
메시지를 생성하여 전달하는 주체다.
예를 들어 주문 시스템이 주문 완료 이벤트를 생성한다.
{
"event_type": "ORDER_COMPLETED",
"ord_id": 123456,
"cst_id": 10001
}
17-2. Consumer
메시지를 읽고 처리하는 주체다.
예를 들어 데이터 적재 프로그램이 메시지를 읽어 분석용 테이블에 저장한다.
17-3. 비동기 처리
동기 처리는 일반적으로 요청 후 응답을 기다리는 방식이다.
비동기 메시지 처리는 생산자가 메시지를 발행한 다음 모든 후속 작업이 완료되기를 기다리지 않고 다른 작업을 진행할 수 있게 해준다.
이를 통해 시스템 간 결합도를 줄일 수 있다.
단, 비동기 방식에서도 메시지 중복, 순서, 유실, 재처리 등의 문제를 고려해야 한다.
17-4. Kafka 실무 예시
고객 주문 발생
↓
주문 DB
↓
CDC
↓
Kafka Topic
↓
Streaming Consumer
↓
데이터 레이크 / 분석 테이블
여기서 CDC(Change Data Capture)는 원천 데이터베이스의 변경사항을 포착하여 다른 시스템에 전달하는 기술이다.
이 구조에서는 원천 데이터의 변경사항을 분석 시스템으로 전달할 수 있다.
다만 CDC의 정확한 전달 방식과 보장 수준은 사용하는 도구 및 구성에 따라 달라진다.
PART 4. 데이터 엔지니어 실무에 적용하기
18. 사례 1: 월별 매출 쿼리가 너무 느리다면?
상황을 가정해 보자.
- 주문 데이터 5억 건
- 매월 상품별 매출 집계
- Impala에서 전체 기간 조회
- 실행 시간이 지나치게 오래 걸림
이때 단순히 서버 메모리만 늘리는 것은 근본적인 해결책이 아닐 수 있다.
확인해야 할 사항
첫 번째는 저장 형식이다.
텍스트 기반 파일보다 Parquet 같은 컬럼 지향 저장 형식이 분석 쿼리에 적합할 수 있다.
두 번째는 파티션이다.
월별 조회가 많다면 기준월을 파티션 키로 사용하는 방안을 검토할 수 있다.
SALES/
bss_ym=202607/
bss_ym=202608/
bss_ym=202609/
이 구조에서 bss_ym = '202609' 조건으로 검색하면 필요한 파티션만 읽는 최적화가 가능할 수 있다.
세 번째는 사전 집계다.
매번 주문 원천을 읽지 않고 월별 매출 집계 테이블을 생성할 수 있다.
원천 주문 테이블
↓ 일별/월별 ETL
월별 매출 집계 테이블
↓
월별 KPI 보고서
단, 집계 기준이 변경되거나 과거 데이터가 수정되는 경우를 고려해 재처리 정책을 설계해야 한다.
19. 사례 2: 신규 컬럼을 추가한 후 ETL이 실패한다면?
상황은 다음과 같다.
기존 회원 데이터:
MEM_ID
MEM_NM
MEM_JN_DTM
변경 후:
MEM_ID
MEM_NM
MEM_JN_DTM
MEM_GRADE
원천 시스템에서는 정상적으로 컬럼이 추가되었다.
그런데 ETL 코드가 기존 컬럼 구조를 전제로 작성되어 있다면 적재 오류가 발생할 수 있다.
예를 들어 다음과 같은 코드다.
INSERT INTO TARGET_TABLE
SELECT *
FROM SOURCE_TABLE;
원천 컬럼이 늘어난 경우 대상 테이블과 컬럼 수가 일치하지 않을 수 있다.
이 문제를 예방하려면 컬럼을 명시하는 방식이 더 안전하다.
INSERT INTO TARGET_TABLE (
MEM_ID,
MEM_NM,
MEM_JN_DTM
)
SELECT
MEM_ID,
MEM_NM,
MEM_JN_DTM
FROM SOURCE_TABLE;
이는 스키마 변경을 견디는 데 도움이 되는 방법 중 하나다.
그러나 컬럼 이름만 같고 의미나 타입이 바뀌었다면 별도의 검증이 필요하다.
핵심은 신규 데이터 구조와 기존 데이터 구조의 호환성을 관리하는 것이다.
20. 사례 3: 동일한 쿼리인데 결과가 달라진다면?
데이터 분석에서 흔히 발생하는 상황이다.
어제 실행한 월별 매출 쿼리와 오늘 실행한 동일한 쿼리의 결과가 다르다고 가정하자.
가능한 원인은 다양하다.
- 원천 데이터가 수정됨
- 과거 주문 데이터가 추가 적재됨
- 주문 취소나 환불이 뒤늦게 반영됨
- ETL이 과거 파티션을 재처리함
- 고객 또는 상품 기준정보가 변경됨
이 문제는 단순히 SQL이 잘못되었다는 의미는 아니다.
데이터가 시간에 따라 변할 수 있다는 사실을 고려해야 한다.
이때 중요한 개념이 스냅샷(Snapshot)이다.
스냅샷은 특정 시점의 데이터 상태를 보존한 것이다.
2026-10-21 기준 데이터
↓
Snapshot A
2026-10-22 기준 데이터
↓
Snapshot B
스냅샷을 보관하면 두 날짜의 데이터 차이를 비교할 수 있다.
실무에서 과거 보고서의 숫자를 재현해야 한다면 단순히 SQL만 보관하는 것으로는 부족할 수 있다.
다음과 같은 정보가 필요하다.
- 집계 SQL과 파라미터
- 데이터 추출 기준일시
- 원천 데이터 버전 또는 스냅샷
- 배치 실행 이력
- 사용한 업무 정의와 코드 버전
이렇게 관리하면 과거 수치가 변경되었을 때 원인을 추적하기가 쉬워진다.
PART 5. 핵심 용어 최종 정리
개념쉽게 설명하면실무 활용
| OLTP | 운영 업무를 빠르게 처리 | 주문·고객·결제 |
| OLAP | 많은 데이터를 분석 | 매출·KPI·통계 |
| Index | 데이터의 위치를 빠르게 찾는 구조 | 특정 조건 검색 |
| Hash Index | 키로 위치를 찾는 인덱스 | 정확한 키 검색 |
| B-Tree | 정렬된 트리 구조 | 범위 검색 |
| LSM Tree | 쓰기를 모아서 저장하고 병합하는 구조 | 대량 쓰기 저장소 |
| WAL | 장애 복구를 위한 선행 로그 | 변경사항 복구 |
| MemTable | 메모리의 데이터 관리 구조 | LSM 쓰기 |
| SSTable | 정렬된 디스크 파일 | LSM 저장 |
| Compaction | 저장 파일 병합·정리 | 읽기 효율·공간 관리 |
| Bloom Filter | 데이터 부재를 빠르게 판단 | 불필요한 디스크 읽기 감소 |
| Column Storage | 열 단위 저장 | 대량 분석 |
| Data Warehouse | 분석용 통합 데이터 저장 환경 | BI·KPI |
| Fact Table | 측정할 실적 데이터 | 매출액·수량 |
| Dimension | 실적을 나누는 기준 | 고객·상품·날짜 |
| Grain | 테이블 한 행의 의미 | 중복 집계 방지 |
| Data Cube | 여러 차원의 집계 구조 | 다차원 분석 |
| Serialization | 데이터를 저장·전송 형식으로 변환 | API·파일·이벤트 |
| Avro | 스키마 기반 직렬화 | 데이터 파이프라인 |
| Protobuf | 필드 번호 기반 바이너리 직렬화 | 서비스 간 통신 |
| Schema Evolution | 데이터 구조의 변경 관리 | ETL 유지보수 |
| Backward Compatibility | 새 코드가 옛 데이터 처리 | 신규 ETL 배포 |
| Forward Compatibility | 옛 코드가 새 데이터 처리 | 구버전 시스템 유지 |
| Rolling Upgrade | 서버를 차례대로 업데이트 | 무중단 배포 |
| REST API | HTTP 기반 리소스 인터페이스 | 외부 시스템 연동 |
| RPC | 원격 서비스 호출 | 내부 서비스 통신 |
| Message Broker | 메시지를 전달하는 중간 시스템 | 비동기 데이터 처리 |
| CDC | 원천 DB 변경사항 포착 | 실시간 데이터 수집 |
| Snapshot | 특정 시점의 데이터 상태 | 과거 수치 재현 |
마무리: 데이터 엔지니어가 반드시 기억해야 할 5가지
첫째, 모든 데이터베이스가 같은 목적을 위해 설계된 것은 아니다.
운영 시스템과 분석 시스템은 처리하는 데이터의 양과 접근 방식이 다르다. OLTP와 OLAP의 차이를 이해해야 적합한 데이터 아키텍처를 선택할 수 있다.
둘째, 저장 구조를 이해하면 쿼리가 느린 이유를 더 정확하게 판단할 수 있다.
B-Tree, LSM Tree, 컬럼 지향 저장소는 각각 다른 문제를 해결한다. 데이터베이스 성능을 이해하려면 데이터가 어떻게 저장되고 검색되는지 알아야 한다.
셋째, 데이터 모델의 Grain을 먼저 정의해야 한다.
월별 실적을 계산할 때 한 행이 주문인지, 주문 상세인지, 고객별 집계인지에 따라 결과가 달라진다. 데이터 모델링은 SQL 작성 이전에 결정해야 할 중요한 작업이다.
넷째, 데이터 구조는 계속 변한다.
신규 컬럼 추가, 데이터 타입 변경, 필드 삭제는 ETL 파이프라인에 영향을 줄 수 있다. 하위 호환성과 상위 호환성을 고려해야 시스템 변경을 안전하게 처리할 수 있다.
다섯째, 데이터 파이프라인은 단순히 데이터를 옮기는 프로그램이 아니다.
수집, 저장, 변환, 호환성 관리, 재처리, 장애 복구, 결과 재현까지 포함하는 시스템이다.
결국 이 책에서 배울 수 있는 중요한 관점은 다음과 같다.
좋은 데이터 시스템을 설계하려면 어떤 기술을 사용할지보다 먼저, 데이터가 어떻게 저장되고 이동하며 변경되는지 이해해야 한다.
주니어 데이터 엔지니어라면 처음부터 모든 저장소 엔진의 세부 구현을 외우기보다는, 각 기술이 어떤 문제를 해결하기 위해 만들어졌는지를 중심으로 이해하는 것이 좋다.
그렇게 공부하면 SQL 최적화, ETL 설계, 데이터 웨어하우스 구축, 실시간 파이프라인 개발에서 기술을 선택하는 이유를 점차 설명할 수 있게 된다.
