0. 들어가며: 데이터 엔지니어는 왜 데이터베이스 내부를 알아야 할까?

데이터 엔지니어링 업무를 하다 보면 다음과 같은 상황을 자주 경험한다.

  • 똑같은 SQL인데 Oracle에서는 빠르고 Impala에서는 느리다.
  • 수억 건의 데이터에서 특정 고객을 조회하는 것은 빠른데, 월별 매출을 집계하면 시간이 오래 걸린다.
  • 데이터가 계속 적재되는 테이블에서 조회 성능이 점점 나빠진다.
  • ETL 파이프라인을 수정하지 않았는데 컬럼이 추가되면서 오류가 발생한다.
  • 운영 중인 시스템을 중단하지 않고 새로운 버전으로 변경해야 한다.

이런 문제를 해결하려면 SQL 문법뿐 아니라 데이터가 저장되고 검색되는 구조, 시스템 간 데이터를 주고받는 방식까지 이해해야 한다.

이번 글에서는 《데이터 중심 애플리케이션 설계》의 3장과 4장을 중심으로 데이터 엔지니어가 알아야 할 개념을 정리한다.

핵심은 두 가지다.

  1. 데이터를 어떻게 저장하고 검색하느냐에 따라 시스템의 성능이 달라진다.
  2. 데이터를 어떤 형식으로 주고받느냐에 따라 시스템의 확장성과 호환성이 달라진다.

PART 1. 데이터 저장소와 검색

1. OLTP와 OLAP는 무엇이 다를까?

데이터베이스를 이해할 때 가장 먼저 알아야 할 개념이 OLTP와 OLAP다.

두 시스템은 모두 데이터를 다루지만 목적이 다르다.

1-1. OLTP (Online Transaction Processing)

OLTP는 온라인 트랜잭션 처리 시스템이다.

쉽게 말해 사용자가 서비스를 이용할 때 발생하는 데이터를 빠르고 정확하게 처리하는 시스템이다.

예를 들어 쇼핑몰을 생각해 보자.

고객이 상품을 주문하면 다음 작업이 발생한다.

  1. 주문 정보를 저장한다.
  2. 결제 상태를 변경한다.
  3. 재고를 차감한다.
  4. 주문 상태를 조회한다.

이때 필요한 데이터는 전체 주문 데이터 중 극히 일부다.

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는 기존 스키마만 처리할 수 있다면 적재 오류가 발생한다.

이를 예방하기 위해 다음과 같은 접근을 사용할 수 있다.

  1. 새로운 필드를 선택적으로 처리할 수 있도록 소비자 코드를 변경한다.
  2. 이전 데이터에 대해서는 기본값이나 NULL 처리를 준비한다.
  3. 소비자 배포 및 검증을 완료한다.
  4. 생산자에서 새로운 데이터 형식을 사용한다.
  5. 모든 시스템이 전환된 이후 필요하다면 오래된 필드를 제거한다.

이런 방식을 흔히 확장 후 축소(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 설계, 데이터 웨어하우스 구축, 실시간 파이프라인 개발에서 기술을 선택하는 이유를 점차 설명할 수 있게 된다.

"원천 시스템에선 분명 삭제했는데, 데이터 분석 쪽에서는 그 데이터가 그대로 보인다." 데이터 엔지니어가 자주 마주치는 문제 중 하나입니다. 오늘은 이 현상이 왜 일어나는지, 어떻게 해결하는지 정리하고, 끝에 면접 단골 질문도 같이 넣었습니다.

 


1. 데이터는 보통 두 곳에 저장됩니다

회사에서 데이터는 크게 두 종류의 시스템에 나뉘어 저장됩니다.

  • 원천(Source) DB: 실제 업무 처리용. 주문 등록, 회원 가입, 결제 같은 작업이 실시간으로 일어나는 시스템입니다. Oracle, MySQL, PostgreSQL 같은 RDBMS가 흔히 쓰입니다.
  • 데이터 웨어하우스 / 데이터 레이크: 분석·통계·리포트용. 원천 시스템에서 데이터를 주기적으로 가져와 쌓아두고, 마케팅·추천·BI 같은 작업에 씁니다. Hadoop 생태계가 대표적입니다.

업무 처리와 분석을 분리하는 이유는 단순합니다. 분석 쿼리는 무겁고 오래 걸리는데, 그걸 업무 시스템에서 돌리면 실제 사용자가 느려지거나 장애로 이어질 수 있기 때문입니다. 그래서 분석은 별도 환경에서 합니다.

 

문제는 두 시스템의 데이터를 어떻게 일치시킬 것인가입니다.

 

2. 데이터 동기화 배치는 보통 INSERT/UPDATE만 합니다

원천 DB의 데이터를 Hadoop 쪽으로 옮기는 작업을 배치(Batch)라고 부릅니다. 보통 새벽에 돌면서 "어제 새로 생긴 데이터", "어제 바뀐 데이터"를 가져와 쌓습니다.

 

여기서 처리하는 작업은 대부분 두 가지입니다.

  • INSERT: 새로 생긴 행 추가
  • UPDATE: 변경된 행 갱신

빠진 게 있죠. DELETE(삭제)입니다.

원천에서 누가 데이터를 지웠을 때, 배치가 그걸 인지하지 못하면 타겟 쪽엔 그 행이 그대로 남게 됩니다.

 

3. 실제 사례로 보기

다음과 같은 테이블이 있다고 해봅시다. PK는 고객ID + 주소순번입니다.

 

  • 원천 DB 조회 결과
고객ID 주소순번 주소
1234 9 서울시 강남구

 

  • Hadoop 조회 결과 (같은 SQL)
고객ID 주소순번 주소
1234 4 서울시 마포구 ← 원천엔 없음
1234 9 서울시 강남구

 

원천에서는 누가 옛날 주소(순번 4번)를 깔끔하게 삭제했는데, Hadoop엔 여전히 남아있습니다. 이 상태를 데이터 정합성이 깨졌다고 표현합니다.

 

이대로 분석을 돌리면 이 고객이 두 군데 사는 사람으로 잡힙니다. 마케팅 메일이 두 번 발송되거나, 통계가 부풀려지거나, 리포트 신뢰도가 떨어지는 등 후속 문제가 줄줄이 생깁니다.

 

4. Hadoop은 원래 DELETE 못 하는 거 아니었어?

여기서 중요한 개념이 등장합니다. Hadoop은 단일 시스템이 아니라 여러 저장 방식이 공존하는 생태계입니다.

HDFS (전통적인 Hadoop 저장 방식)

HDFS는 append-only(추가만 가능) 구조입니다. 한번 쓴 파일의 일부만 수정하거나 한 행만 지우는 작업이 매우 비효율적입니다. 보통 그런 작업이 필요하면 파일을 통째로 다시 쓰거나, 파티션 단위로 교체합니다.

 

이런 설계 철학은 "쓰기는 한 번, 읽기는 여러 번(write-once, read-many)"이라는 원칙에서 나왔습니다. 로그처럼 시간순으로 쌓이기만 하는 데이터에는 굉장히 효율적이지만, 행 단위 수정·삭제가 잦은 데이터에는 맞지 않습니다.

Kudu

Kudu는 Hadoop 생태계 안에 있지만 설계 목적이 다릅니다. INSERT, UPDATE, UPSERT, DELETE 모두 row-level로 지원합니다. LSM-tree 계열 구조라 행 단위 변경이 효율적이고, 그래서 자주 바뀌는 마스터성·트랜잭션성 데이터를 다룰 때 많이 씁니다.

💡 핵심 정리 "Hadoop은 DELETE 안 된다"는 말은 정확히는 HDFS에 한정된 이야기입니다. Kudu는 DELETE가 됩니다. 단, WHERE 절에 PK가 들어가야 효율적으로 동작합니다.

 

5. 그래서 DELETE 문제는 왜 생긴 걸까?

이제 사례를 다시 보면 정리가 됩니다.

  • 데이터는 Kudu에 저장됨 → DELETE는 기술적으로 가능
  • 그런데 배치 로직이 DELETE를 처리하지 않음 → 설계상의 누락

배치가 INSERT/UPDATE만 보고, "원천에서 사라진 행"은 따로 챙기지 않은 거죠. CDC(Change Data Capture, 변경 데이터 캡처) 같은 도구를 쓰지 않고 단순 증분 적재만 하면 흔히 발생하는 패턴입니다.

 

6. 잠깐, 그런데 데이터를 직접 지우는 경우가 있긴 한가?

여기서 의문이 들 수 있습니다. "실무에서 데이터를 진짜로 지우긴 하나? 보통 안 지우지 않나?" 정확한 지적입니다. 그래서 이 부분을 짚고 넘어가겠습니다.

일반론: 물리 DELETE는 실제로 드뭅니다

대부분의 운영 DB에서는 데이터를 물리적으로 삭제하지 않는 게 정석입니다. 이유는 다음과 같습니다.

  • 감사·추적(audit) 요구사항: 금융·통신·공공 분야는 데이터를 함부로 못 지웁니다. 모든 변경 이력이 추적 가능해야 합니다.
  • 법적 보존 의무: 전자상거래법, 개인정보보호법, 통신비밀보호법 등 일정 기간 보관해야 하는 데이터가 있습니다.
  • 데이터 분석 가치: 탈퇴 회원, 취소된 주문, 폐기된 상품도 모두 분석 자산입니다. 지우면 통계가 왜곡됩니다.

그래서 대부분 soft delete 패턴으로 처리합니다.

-- 물리 삭제 대신
DELETE FROM members WHERE member_id = 1234;

-- 플래그만 변경
UPDATE members SET use_yn = 'N', del_dtm = SYSDATE WHERE member_id = 1234;

 

USE_YN, DEL_YN, STATUS 같은 컬럼이 그래서 거의 모든 테이블에 들어가 있는 거죠.

그럼에도 물리 DELETE가 일어나는 케이스

그런데도 다음과 같은 상황에서는 실제로 물리 삭제가 발생합니다.

  • 이력성 테이블의 끝일자 관리: SCD Type 2(천천히 변하는 차원, Slowly Changing Dimension) 패턴을 적용한 테이블에서 이력 행을 잘못 끊거나 재처리할 때 물리 삭제가 들어가는 경우가 있습니다.
  • 임시·작업성 테이블: 매일 truncate 후 재적재되는 staging 테이블, 일별 스냅샷 테이블 등.
  • 개인정보 파기: GDPR이나 개인정보보호법상의 파기 요청(right to be forgotten) 처리 시.
  • 데이터 정정: 잘못 들어간 데이터를 운영자가 수동으로 정리할 때.
  • 마스터 데이터 정비: 중복 코드 통합, 잘못 발급된 ID 회수 등.
  • 시스템 마이그레이션·정합성 보정 작업: 일회성으로 발생.

 

우리 사례를 다시 보면

처음에 봤던 테이블 이름이 ORD_RGLR_ORD_CTR_ADDR_H였는데, 끝의 _H는 보통 **History(이력)**를 의미하는 명명 규칙입니다. VLD_BGN_DTM(유효 시작일시), VLD_END_DTM(유효 종료일시) 컬럼이 있는 것도 SCD Type 2 이력 관리 패턴이고요.

이런 테이블은 운영 중 정정 작업이나 이력 재처리에서 물리 삭제가 종종 발생합니다. 일반적인 "DELETE는 드물다"는 원칙에서 벗어나는 대표적인 케이스죠.

그래서 모든 테이블에 일률 적용할 필요는 없습니다

PK 차집합 비교 방식은 비용이 들기 때문에, 모든 테이블에 적용하면 부담이 큽니다. 다음 우선순위로 적용하는 것이 합리적입니다.

우선순위테이블 유형적용 여부
1순위 이력 테이블(_H), SCD Type 2 적용 테이블 ✅ 우선 적용
2순위 마스터 테이블 중 정정·통합이 잦은 것 ⚠️ 적용 검토
3순위 트랜잭션 테이블 중 물리 삭제 정책이 있는 것 ⚠️ 정책 확인 후 적용
4순위 순수 append-only 로그성 테이블 ❌ 적용 불필요

💡 정리 일반론으로는 DELETE 동기화가 불필요해 보이지만, 이력 테이블이나 정정·파기가 일어나는 테이블에서는 반드시 필요합니다. 적용 대상을 잘 가려서 선택적으로 도입하는 것이 핵심입니다.

 

7. 해결 방법: PK 차집합 비교

해결 흐름은 다음과 같습니다.

  1. 원천에서 PK(기본키)만 추출합니다. PK는 행을 유일하게 식별하는 값이라 가볍습니다. 전체 컬럼을 다 가져오는 것보다 IO 부담이 훨씬 적습니다.
  2. Kudu에 있는 PK 목록과 비교합니다. "Kudu에는 있는데 원천엔 없는 PK"가 곧 삭제됐어야 할 행입니다.
  3. 그 PK들을 Kudu에서 DELETE 합니다.
  4. 그 다음 평소처럼 INSERT/UPDATE 배치를 수행합니다.

효율을 위한 SQL 힌트

원천에서 PK만 빠르게 떠오기 위해 보통 이런 힌트를 씁니다.

SELECT /*+ INDEX_FFS(t PK_TABLE_NAME) PARALLEL(t 4) */
       PK_COL1, PK_COL2
  FROM SOURCE_TABLE t
 WHERE [조건];
  • INDEX_FFS (Index Fast Full Scan): 테이블 전체 블록을 읽지 않고 PK 인덱스만 빠르게 스캔합니다. PK만 필요한 작업이므로 인덱스에 모든 정보가 있어 테이블 본체를 읽지 않아도 됩니다.
  • PARALLEL(t 4): 병렬도 4로 실행해 추출 속도를 높입니다.

처리 순서가 중요합니다

DELETE는 반드시 INSERT/UPDATE보다 먼저 실행해야 합니다. 그렇지 않으면 같은 배치 사이클 안에서 새로 들어온 행이 PK 비교 시점에 잠깐 존재하다 사라지는 race condition이 발생할 수 있습니다.

반드시 챙겨야 할 안전장치

원천에서 PK 추출이 실패해서 결과가 0건으로 나오면 어떻게 될까요?

 

차집합 기준으로 계산하면 "Kudu에 있는 모든 행을 삭제해야 한다"가 됩니다. 그대로 실행하면 테이블이 통째로 비워집니다. 운영 환경에서 실제로 발생하는 사고 유형입니다.

 

그래서 다음과 같은 안전장치를 반드시 둡니다.

  • 추출 건수 0건이면 작업 중단(Abort) 및 알람
  • 삭제 비율(전체 대비 %)이 임계치를 넘으면 일시 정지 후 수동 검토
  • 전일 대비 삭제 건수 변동이 비정상이면 Hold

 

질문들

데이터 엔지니어, 백엔드 개발자, DBA 면접에서 자주 묻는 패턴으로 정리했습니다.

Q1. HDFS와 Kudu의 차이를 설명해보세요.

핵심 답변 포인트

HDFS는 append-only, write-once-read-many 철학을 따르는 분산 파일 시스템입니다.
대용량 순차 쓰기·읽기에 강하지만 row-level 수정·삭제가 비효율적입니다. Kudu는 row-level INSERT/UPDATE/UPSERT/DELETE를 모두 지원하며 LSM-tree 계열 구조로 자주 변경되는 데이터에 적합합니다. 실무에서는 로그/이벤트성 대용량 데이터는 HDFS(Parquet)로, 자주 바뀌는 마스터성 데이터는 Kudu로 나눠 운영하는 경우가 많습니다.

Q2. Kudu에서 DELETE 시 주의할 점은?

핵심 답변 포인트

DELETE는 가능하지만 WHERE 절에 PK가 포함되어야 효율적입니다. PK 없이 임의 컬럼으로 DELETE 하면 풀스캔이 발생할 수 있습니다. 운영 정책상 DELETE 권한이 막혀 있는 환경도 있는데, 이런 경우 soft delete 컬럼(USE_YN='N' UPSERT)이나 tombstone 패턴으로 우회합니다.

Q3. 원천과 데이터 웨어하우스 간 정합성이 깨지는 대표 원인은?

핵심 답변 포인트

  • INSERT/UPDATE만 동기화하고 DELETE를 미반영하는 경우 (오늘 다룬 케이스)
  • 배치 실행 중 트랜잭션 타이밍 차이로 인한 dirty read
  • CDC 누락 또는 처리 지연
  • 원천 스키마 변경을 타겟이 따라가지 못하는 경우

Q4. 원천에서 DELETE가 발생했는지 인지하는 방법은?

핵심 답변 포인트

  • CDC (Change Data Capture): Oracle GoldenGate, Debezium 등. 가장 정석적인 방법.
  • PK 차집합 비교: 원천 PK를 추출해 타겟과 비교 (오늘 다룬 방법). CDC 도입이 어려울 때 현실적인 대안.
  • 소프트 딜리트 정책: 원천에서 물리 삭제 대신 삭제 플래그만 변경하도록 정책을 설계.

Q5. PK 차집합 비교 방식의 장단점은?

장점

  • CDC 인프라 없이 구현 가능, 도입 비용 낮음
  • 로직이 단순하고 검증이 쉬움

단점

  • 원천 전체 PK를 매번 추출해야 해서 테이블이 클수록 부담
  • 실시간성이 떨어짐 (배치 주기에 의존)
  • 추출 실패 시 전체 삭제 위험 → 반드시 안전장치 필요

Q6. INSERT, UPDATE, UPSERT, DELETE의 차이는?

명령 설명

INSERT 새 행 추가. 같은 PK가 이미 있으면 에러.
UPDATE 기존 행 수정. 해당 PK가 없으면 아무 일도 일어나지 않음.
UPSERT PK가 있으면 UPDATE, 없으면 INSERT. (Update + Insert)
DELETE 행 삭제.

 

Kudu에서 UPSERT가 자주 쓰이는 이유는, 적재 배치에서 "이 PK가 신규인지 기존인지"를 미리 확인할 필요 없이 한 번에 처리할 수 있기 때문입니다.

Q7. PK 추출 시 INDEX_FFS, PARALLEL 힌트를 쓰는 이유는?

핵심 답변 포인트

  • INDEX_FFS: 테이블 본체 블록을 읽지 않고 PK 인덱스만 빠르게 스캔합니다. PK만 필요하므로 인덱스에 모든 정보가 있어 테이블 접근이 불필요합니다.
  • PARALLEL: 병렬 실행으로 추출 시간 단축.
  • 결과적으로 원천 DB의 부하를 최소화하면서 빠르게 PK 목록을 확보할 수 있습니다.

Q8. 원천이 Oracle이 아닌 MySQL/PostgreSQL이라면 같은 방식이 적용 가능한가?

핵심 답변 포인트

개념은 동일하지만 힌트 문법이 다릅니다. MySQL은 USE INDEX / FORCE INDEX, PostgreSQL은 옵티마이저가 자동으로 index-only scan을 선택하는 경우가 많아 별도 힌트 없이도 효율적인 경우가 많습니다. DBMS별로 통계 정보 수집 방식과 인덱스 구조가 다르므로 실행 계획을 반드시 확인하는 것이 중요합니다.

Q9. 만약 Kudu에서 DELETE 권한이 막혀 있다면 어떻게 우회할 수 있나요?

핵심 답변 포인트

  • Soft delete: 삭제여부 컬럼을 두고 UPSERT로 'N' → 'Y' 변경. 단, 모든 조회 SQL이 이 필터를 포함해야 함.
  • Tombstone UPSERT: 삭제 표시만 해두고 별도 컴팩션 잡에서 정리.
  • 단점은 데이터가 실제로 줄지 않아 스토리지 사용량이 계속 증가한다는 점. 주기적인 정리 잡이 필요.

 

Q10. 모든 테이블에 DELETE 동기화 로직을 적용해야 하나요?

핵심 답변 포인트

아니요, 일률 적용은 비효율적입니다. PK 추출과 비교에는 비용이 들기 때문에 다음 우선순위로 적용 대상을 선별해야 합니다.

  • 우선 적용: 이력 테이블(_H 명명, SCD Type 2 적용 테이블) — 정정·재처리에서 물리 삭제가 자주 발생.
  • 적용 검토: 마스터 테이블 중 정정·통합이 잦은 것 (예: 코드 통합, 중복 ID 정리).
  • 정책 확인 후 적용: 트랜잭션 테이블 중 물리 삭제 정책이 있는 것 (예: 개인정보 파기 대상).
  • 적용 불필요: 순수 append-only 로그성 테이블, soft delete만 사용하는 테이블.

실무에서는 물리 DELETE보다 soft delete가 일반적이므로, 무조건 모든 테이블에 적용하기보다 삭제 패턴을 먼저 분석한 후 선택적으로 도입하는 것이 합리적입니다.


8. 마무리

전체 데이터를 매번 통째로 복사하면 정합성은 보장되지만, 대용량 환경에서는 시간·네트워크·시스템 부하 측면에서 현실적이지 않습니다. 그래서 데이터 엔지니어링은 늘 "바뀐 부분만 효율적으로 옮기는 방법"을 고민하는 분야가 됩니다.

 

오늘 본 PK 차집합 비교는 그중 하나의 답입니다. CDC가 가장 정석적인 방법이지만, 도입 환경이 갖춰지지 않았거나 비용·복잡도 문제가 있을 때 PK 차집합 방식이 좋은 대안이 됩니다.

결국 데이터 엔지니어링은 정확성, 속도, 비용 사이에서 균형을 잡는 일입니다. 이번 글이 그 의사결정의 한 장면을 이해하는 데 도움이 됐길 바랍니다.

 

 

더 공부해볼 만한 주제

  • Apache Kudu 공식 문서: 구조, DML 제약, 파티셔닝 전략
  • Debezium: 오픈소스 CDC 도구, 이벤트 기반 동기화의 표준
  • Apache Iceberg / Delta Lake / Hudi: 최근 주목받는 "DELETE 가능한 데이터 레이크" 포맷들. HDFS의 한계를 극복하는 방향성

다음 글에서는 Iceberg가 어떻게 데이터 레이크 환경에서 row-level DELETE를 가능하게 만들었는지 다뤄볼 예정입니다.

 

Claude Code 소프트웨어 아키텍처 AI 에이전트 읽기
Claude Code 소스코드가 열렸다
— 왜 그렇게 작동하는지 이제 알았다
1,884개 파일, 800KB짜리 단일 번들, 65ms를 아끼기 위한 설계.
분석 문서를 읽고 나서야 "그래서 그랬구나" 했던 것들을 정리했다.
2025 · 재니로그 · 기술 읽기
2026년 3월 말, Anthropic의 Claude Code 소스코드가 외부에 노출됐다. 약 1,884개의 TypeScript + React 파일. 누군가 이걸 분석해서 정리한 문서를 읽다가, 나도 모르게 "아, 그래서 그랬구나" 하는 순간이 몇 번이나 왔다.
Claude Code를 쓰면서 늘 궁금했던 것들. 왜 가끔 응답이 끊겼다가 이어지는지, 위험한 명령엔 왜 꼭 확인을 물어보는지, 어떻게 한 번 요청했는데 파일도 읽고 수정도 하고 테스트까지 돌려주는지. 그게 다 소스코드 안에 있었다.
이 글은 그 분석 내용을 내가 이해한 방식으로 다시 풀어쓴 것이다. 소스코드 원문은 한 줄도 인용하지 않고, 구조와 흐름만 이야기한다.
사용자가 입력한다 → AI가 판단한다 → 도구를 실행한다 → 결과를 AI에게 돌려준다 → 반복한다.
이 루프가 Claude Code의 전부다.
나머지 수천 개의 파일은 이 루프를 더 빠르고, 더 안전하고, 더 다양한 환경에서 돌아가게 만들기 위해 존재한다.
🔧 "현장 엔지니어"라는 비유
Claude Code를 이해하는 데 제일 도움이 됐던 비유가 있다.
일반적인 AI 챗봇은 "전화 상담원"이다. 조언은 줄 수 있지만, 직접 뭔가를 할 수는 없다. "이렇게 해보세요"라고 말하면, 결국 내가 직접 해야 한다.
Claude Code는 "현장 엔지니어"다. 내 컴퓨터에 직접 들어와서 파일을 열어보고, 코드를 고치고, 테스트를 돌리고, 결과를 확인한다. "이 버그 고쳐줘" 한 마디 하면 알아서 다 한다.
이게 가능한 이유가 "도구(Tool)" 시스템인데, 그건 아래에서 설명한다.
⚡ 시작하자마자 65밀리초를 아끼는 방법
Claude Code를 실행하면 맨 처음 main.tsx라는 파일이 실행된다. 재밌는 건 이 파일 하나가 800KB라는 사실이다. 일반적으로는 파일을 작게 쪼개는 게 좋은 습관인데, 여기서는 의도적으로 하나로 합쳤다.
이유는 단순하다. CLI 도구는 실행할 때마다 처음부터 시작한다. 파일이 10개면 디스크 읽기가 10번, 1개면 1번이다.
거기서 한 발 더 나간 최적화가 있다. 프로그램이 켜지는 순간, 무거운 모듈들을 불러오는 데 약 135ms가 걸린다. 그 135ms 동안 동시에 두 가지를 미리 처리한다.
• macOS 키체인에서 인증 토큰 미리 가져오기
• MDM(기업 기기 관리) 설정 읽기
기다리는 시간을 그냥 흘려보내지 않고, 그 사이에 할 수 있는 일을 먼저 해두는 것. 이렇게 하면 65ms를 공짜로 아낄 수 있다.
💡 시작 순서 6단계: ① 병렬 I/O 사전 실행 → ② 필요한 모듈만 로딩 → ③ 설정 파일 로딩 → ④ 인증(OAuth → API 키 → AWS Bedrock → Google Vertex → Azure 순서) → ⑤ 모델 결정 → ⑥ REPL 실행
🔄 한 번 입력했는데 왜 여러 번 처리될까
이게 처음에 제일 신기했던 부분이다. "auth.ts 버그 고쳐줘"라고 한 번 입력하면, 화면에는 AI가 파일을 읽고, 수정하고, 테스트까지 돌리는 과정이 주르륵 펼쳐진다. 내가 뭔가를 더 입력한 게 아닌데.
이게 바로 쿼리 루프다. query.ts라는 파일(68KB)이 이 모든 걸 담당한다.
사용자 입력 → API 호출 → AI 응답 수신 ↓ 도구 사용 요청이 있으면? YES → 도구 실행 → 결과를 AI에게 전달 → 다시 API 호출 NO → 루프 종료, 사용자에게 결과 표시
"버그 고쳐줘" 한 마디가 내부에서는 이렇게 흘러간다.
• Turn 1: AI가 파일을 읽음 → FileRead("auth.ts") 실행 → 내용 수신
• Turn 2: AI가 버그를 찾아 수정 → FileEdit(42번 줄) 실행 → 완료
• Turn 3: AI가 테스트를 돌림 → Bash("npm test") 실행 → 통과
• Turn 4: AI가 결과를 설명 → 더 이상 도구 없음 → 루프 종료
사용자는 1번 입력했지만, 내부에서는 4번의 턴, 3번의 도구 실행, 4번의 API 호출이 일어난 것이다.
💡 응답이 글자 단위로 스르륵 나오는 것도 이유가 있다. 비동기 제너레이터(Async Generator) 패턴 덕분에 API에서 토큰이 도착하는 즉시 화면에 표시한다. 음식이 다 완성될 때까지 기다렸다가 한 번에 내오는 게 아니라, 준비되는 대로 하나씩 내오는 것.
🗜️ 대화가 너무 길어졌을 때 — 자동 압축
AI가 한 번에 처리할 수 있는 텍스트 양(컨텍스트 윈도우)에는 한계가 있다. 화이트보드가 꽉 차는 것과 같다.
토큰 사용량이 임계값(컨텍스트 한계 - 13,000 토큰)을 넘으면 자동 압축(Auto-Compact)이 작동한다.
1. 먼저 세밀한 방식(세션 메모리 압축)으로 시도
2. 실패하면 전체 압축: 이미지 먼저 제거 → AI에게 전체 요약 → 최근 참조 파일 상위 5개 복원(50K 토큰 예산)
3. 3번 연속 실패하면 포기 (무한 루프 방지)
⚠️ 에러가 나도 바로 사용자에게 보여주지 않는 이유도 있다. 프롬프트가 너무 길어서 413 에러가 나면 대화 압축을 먼저 시도하고, 응답이 너무 길어서 끊기면 토큰 한도를 8K에서 64K로 올려서 최대 3번 재시도한다. 복구 가능한 에러를 먼저 자동으로 고쳐보고, 그래도 안 될 때만 사용자에게 알린다.
🛠️ 도구(Tool) — AI가 세상과 만나는 방법
Claude AI 자체는 텍스트만 생성할 수 있다. 파일을 읽거나, 명령을 실행하거나, 인터넷을 검색하는 건 AI 단독으로는 불가능하다. 그래서 Claude Code가 "도구"를 제공한다. 45개 이상의 도구가 내장되어 있다.
주요 도구들
BashTool — 가장 강력하고 가장 위험하다
셸 명령을 실행한다. rm -rf / 같은 명령도 실행할 수 있다는 뜻이다. 그래서 특별한 보안 장치가 붙어 있다.
🔒 Tree-sitter라는 파서로 명령어를 문법 구조(AST) 단위로 분석한다. 단순히 "rm이 들어있으면 위험하다"는 식의 문자열 매칭이 아니라, 명령어의 실제 구조를 파악한다. "안전하다고 증명된 것만 허용"한다. 판단하기 어려우면 차단한다. 15초를 넘으면 자동으로 백그라운드로 전환되고, 2초마다 진행 상황을 보고한다.
FileEditTool
파일 전체를 덮어쓰는 게 아니라, 특정 문자열을 찾아서 교체한다. 퍼지 매칭으로 정확히 일치하지 않아도 의도한 위치를 찾아주고, 파일의 원래 인코딩과 줄바꿈 방식을 그대로 유지한다.
AgentTool — AI가 AI를 고용한다
복잡한 작업을 만나면 메인 AI가 서브에이전트를 만들어서 부분 작업을 위임한다. 서브에이전트는 부모 AI보다 제한된 권한을 받는다.
도구가 실행되기 전 10단계
하나의 도구가 실제로 실행되기까지 10단계를 거친다. 각 단계는 하나의 질문에 답하는 것이다 — "지금 이 작업을 해도 괜찮은가?"
1
이름으로 도구 찾기 (없으면 별칭 시도)
2
사용자가 Ctrl+C를 눌렀는지 확인
3
입력 형식 검증
4
PreToolUse 훅 실행 (훅이 차단하면 여기서 종료)
5
권한 확인
6
실제 실행 ← 여기서 진짜 일이 일어남
7
결과를 API 형식으로 변환
8
결과가 너무 크면 디스크에 저장하고 참조만 전달
9
PostToolUse 훅 실행
10
텔레메트리 기록
안전한 것은 함께, 위험한 것은 혼자
여러 도구를 한 번에 써야 할 때, 안전한 도구들은 최대 10개까지 동시에 실행한다. 위험한 도구는 하나씩 순서대로 실행한다.
[파일읽기] [grep] [glob] [파일수정] [파일읽기] [bash] 안전 안전 안전 위험 안전 위험 → 배치1: [파일읽기, grep, glob] 동시 실행 → 배치2: [파일수정] 단독 실행 → 배치3: [파일읽기] 단독 실행 → 배치4: [bash] 단독 실행
🔐 권한 시스템 — 어디까지 믿고 맡길 것인가
Claude Code가 내 컴퓨터에서 실행된다는 건, 잘못되면 정말 큰일이 날 수 있다는 뜻이기도 하다. 4가지 모드가 있다.
모드 동작 적합한 상황
Default 읽기는 자동 승인, 쓰기/실행은 확인 요청 대부분의 사람에게 적합
Auto AI 분류기가 2단계로 위험도 스스로 평가 후 결정 속도 중시, 분류기 오류 감수 가능할 때
Plan 읽기 전용만 허용, 수정 불가 "AI가 뭘 하려는지 먼저 보고 싶다"
Bypass 모든 작업 자동 승인 개발 환경 전용, 중요 환경 절대 금지
규칙 우선순위: 로컬 설정 > 프로젝트 설정 > 사용자 설정 > 플래그 > 정책
👥 코디네이터 모드 — AI 팀을 꾸려서 일을 나누다
"리더 AI 한 명 + 워커 AI 여러 명" 구조로 큰 작업을 병렬로 처리한다. 시니어 개발자(리더)가 여러 주니어 개발자(워커)에게 작업을 분배하는 것과 같다.
1
리서치 (병렬) — 워커들이 각자 다른 파일과 부분을 동시에 조사
2
종합 (리더만) — 리더가 모든 결과를 직접 읽고 이해. 위임 금지
3
구현 (영역별) — 워커들이 코드를 수정. 충돌 방지를 위해 한 번에 한 영역씩
4
검증 (병렬) — 워커들이 독립된 테스트를 동시에 실행
🧠 메모리 시스템 — 다음에 만나도 기억한다
세션이 끝나면 대화는 사라진다. 메모리 시스템은 중요한 정보를 파일로 저장해두고, 다음 세션에서 자동으로 불러온다.
저장 위치는 ~/.claude/projects/{프로젝트 이름}/memory/이고, MEMORY.md가 인덱스 역할을 한다.
유형 내용
user나는 누구인지 — 역할, 전문성, 선호
feedback어떻게 일할지 — 수정 사항, 작업 방식
project지금 무슨 일이 있는지 — 목표, 마감, 결정
reference어디를 봐야 하는지 — 외부 문서, 시스템 링크
단순히 "이렇게 해라"만 기록하는 게 아니라, "왜(Why)"와 "이 상황에서는 어떻게 적용(How)"도 함께 저장한다. 나중에 애매한 상황에서도 맥락을 이해하고 올바른 판단을 할 수 있게.
🖥️ 터미널에서 React가 돌아간다
Claude Code의 터미널 화면은 React로 만들어져 있다. 웹 브라우저 대신 터미널 문자 그리드에 렌더링한다는 차이만 있을 뿐, React 컴포넌트로 UI를 구성한다.
⚙️ 렌더링 최적화: 이중 버퍼링(다음 화면 미리 준비 후 바뀐 부분만 출력) / 객체 풀링(같은 문자열은 메모리에 하나만) / 더티 추적(변경된 부분만 다시 그림) / 프레임 조절(업데이트 빈도 제한)
✍️ 소스코드를 읽고 나서 바뀐 것
분석 문서를 읽기 전까지는 Claude Code를 그냥 "잘 되는 도구"로만 썼다. 왜 잘 되는지는 몰랐다.
이제는 조금 다르게 보인다. 65ms를 아끼기 위해 병렬 I/O를 쓰는 것, 에러를 바로 사용자에게 던지지 않고 먼저 자동으로 복구를 시도하는 것, "안전하다고 증명된 것만 허용"하는 기본 거부 설계. 이런 것들이 하나하나 이유가 있는 결정이었다.
설계 문서를 읽을 때마다 드는 생각이 있다. 좋은 시스템은 "어떻게 작동하는지"가 자연스럽게 이해된다는 것. Claude Code도 그랬다. 왜 이렇게 만들었는지가 납득이 됐다.
우리가 Impala나 Oozie 파이프라인을 설계할 때도 결국 같은 질문을 하게 되는 것 같다. 이게 왜 이렇게 생겼는지, 이 선택이 다른 선택을 어떻게 밀어냈는지. 그 맥락을 이해하는 게, 그냥 쓰는 것과 제대로 쓰는 것의 차이가 아닐까.

 

새로운 환경에 투입됐을 때 가장 먼저 해야 할 일 중 하나는 DB 구조 파악이다. Oracle은 시스템 딕셔너리 뷰가 잘 갖춰져 있어서, 쿼리 몇 개만 돌려봐도 전체 그림이 빠르게 잡힌다. 이 글은 내가 실무에서 쓰는 순서대로 정리한 것이다.

 

1. 스키마 / 테이블 파악

가장 먼저 어떤 스키마가 있고, 각 스키마에 어떤 테이블이 있는지 확인한다.

-- 내가 접근 가능한 스키마(owner) 목록 전체 조회
SELECT DISTINCT owner
FROM all_tables
ORDER BY owner;
-- 특정 스키마의 테이블 목록
-- num_rows: 마지막 통계 수집 시점 기준 건수 (실제와 다를 수 있음)
-- last_analyzed: 통계 수집일 → 오래됐으면 num_rows 신뢰도 낮음
SELECT
    t.table_name,
    c.comments        AS 테이블설명,
    t.num_rows,
    t.last_analyzed
FROM all_tables t
LEFT JOIN all_tab_comments c
    ON t.owner = c.owner
    AND t.table_name = c.table_name
WHERE t.owner = 'CCIP'   -- 조회할 스키마명
ORDER BY t.table_name;

 

 

2. 컬럼 상세 조회

테이블 구조를 파악할 때 핵심이다. all_tab_columns에서 타입/길이/nullable을 가져오고, all_col_comments에서 한글 컬럼명(COMMENT)을 조인한다.

-- 컬럼 목록 + 한글명 + 타입 정보
SELECT
    col.table_name,
    col.column_id       AS 순번,
    col.column_name     AS 컬럼ID,
    com.comments        AS 컬럼명,      -- COMMENT ON COLUMN으로 달아둔 한글명
    col.data_type       AS 타입,
    col.data_length     AS 길이,
    col.nullable        AS NULL허용,    -- Y: 허용, N: NOT NULL
    col.data_default    AS 기본값
FROM all_tab_columns col
LEFT JOIN all_col_comments com
    ON col.owner = com.owner
    AND col.table_name = com.table_name
    AND col.column_name = com.column_name
WHERE col.owner = 'CCIP'
ORDER BY col.table_name, col.column_id;

 

 

3. PK / FK / 인덱스

테이블 간 관계와 조회 성능 구조를 파악하는 데 필요하다.

-- 제약조건 조회 (PK, UK, FK)
-- constraint_type: P=PK, U=Unique, R=FK
-- r_constraint_name: FK인 경우 참조하는 PK constraint 이름
SELECT
    c.table_name,
    c.constraint_name,
    c.constraint_type,     -- P: PK, U: Unique, R: FK
    cc.column_name,
    cc.position,           -- 복합 PK일 경우 컬럼 순서
    c.r_constraint_name    -- FK가 참조하는 제약조건 이름
FROM all_constraints c
JOIN all_cons_columns cc
    ON c.owner = cc.owner
    AND c.constraint_name = cc.constraint_name
WHERE c.owner = 'CCIP'
  AND c.constraint_type IN ('P', 'U', 'R')
ORDER BY c.table_name, c.constraint_type, cc.position;
-- 인덱스 구성 컬럼 조회
-- column_position: 복합 인덱스일 경우 컬럼 순서
-- descend: 내림차순 여부
SELECT
    index_name,
    table_name,
    column_name,
    column_position,
    descend
FROM all_ind_columns
WHERE index_owner = 'CCIP'
ORDER BY table_name, index_name, column_position;

 

 

4. 데이터 볼륨 / 최신성

어떤 테이블이 핵심 테이블인지 감을 잡는 데 유용하다. num_rows는 통계 수집 시점 기준이므로, last_analyzed가 오래됐다면 실제 건수와 다를 수 있다.

-- 테이블별 추정 건수 + 통계 수집일 (건수 내림차순)
SELECT
    table_name,
    num_rows,
    last_analyzed   -- 이 날짜가 오래됐으면 num_rows 신뢰 낮음
FROM all_tables
WHERE owner = 'CCIP'
ORDER BY num_rows DESC NULLS LAST;

 

 

5. 뷰 / 시퀀스 / 프로시저

테이블 외에 어떤 오브젝트들이 있는지 확인한다.

-- 뷰 목록
SELECT view_name
FROM all_views
WHERE owner = 'CCIP'
ORDER BY view_name;
-- 시퀀스 목록 (자동 채번에 사용)
-- last_number: 마지막으로 발급된 번호
SELECT
    sequence_name,
    last_number,
    increment_by,
    cycle_flag      -- 최대값 도달 시 처음부터 순환 여부
FROM all_sequences
WHERE sequence_owner = 'CCIP'
ORDER BY sequence_name;
-- 프로시저 / 함수 / 패키지 / 트리거 목록
-- last_ddl_time: 마지막 수정 시각
-- status: VALID / INVALID (컴파일 오류 여부)
SELECT
    object_name,
    object_type,
    last_ddl_time,
    status
FROM all_objects
WHERE owner = 'CCIP'
  AND object_type IN ('PROCEDURE', 'FUNCTION', 'PACKAGE', 'TRIGGER')
ORDER BY object_type, object_name;

 

 

6. 배치 잡 파악

처리계 DB에서 특히 중요한 부분이다. 언제 어떤 테이블이 갱신되는지를 파악하려면 배치 잡 스케줄을 봐야 한다.

-- DBMS_SCHEDULER 잡 (비교적 최신 방식)
-- state: SCHEDULED / RUNNING / DISABLED
SELECT
    job_name,
    enabled,
    state,
    last_start_date,    -- 마지막 실행 시각
    next_run_date       -- 다음 실행 예정 시각
FROM all_scheduler_jobs
WHERE owner = 'CCIP'
ORDER BY next_run_date;
-- DBMS_JOB (구형 방식, 여전히 쓰는 곳 많음)
-- interval: 실행 주기 표현식 (예: SYSDATE + 1/24 = 1시간마다)
SELECT
    job,
    what,           -- 실행 내용 (프로시저명 등)
    last_date,      -- 마지막 실행일
    next_date,      -- 다음 실행 예정일
    interval        -- 실행 주기
FROM all_jobs
ORDER BY next_date;

 

 

7. DB 링크 (연계 시스템 파악)

타 시스템과의 연계 구조를 파악할 수 있다. 어떤 외부 DB로 뻗어있는지 보이기 때문에, 데이터 흐름 파악에 유용하다.

-- DB 링크 목록
-- host: 연결 대상 DB 정보
SELECT
    db_link,
    username,
    host
FROM all_db_links
WHERE owner = 'CCIP'
ORDER BY db_link;

 

 

조회 순서 요약

 

  1. 스키마 목록 → 어떤 owner가 있는지
  2. 테이블 목록 → 규모와 구조 파악
  3. 컬럼 상세 → 핵심 테이블 위주로
  4. PK/FK/인덱스 → 테이블 간 관계 파악
  5. 볼륨/최신성 → 핵심 테이블 식별
  6. 배치 잡 → 언제 어떤 테이블이 갱신되는지
  7. DB 링크 → 외부 연계 파악

 

 

🗂️ 배경 — 가끔 몰아서 들어오는 이관 요청

 

Oracle, SAP 데이터를 Hadoop으로 이관해달라는 요청은 자주 오지 않는다. 평소엔 조용하다가 어느 날 갑자기 "이 테이블들 좀 올려주세요" 하고 10개, 20개씩 한꺼번에 들어온다.

 

문제는 그때마다 Oracle에서 컬럼 목록을 손으로 뽑고, 타입을 Impala 문법으로 하나씩 바꾸고, Kudu인지 Parquet인지 판단해서 DDL을 직접 작성해야 한다는 것이다. 요청이 몰리면 이 단순 반복 작업이 꽤 많은 시간을 잡아먹는다(그치만 성과는 안 나는 ..^^;) 그래서 PyCharm에서 바로 돌릴 수 있는 DDL 자동 생성 스크립트를 만들었다.

💡 Oracle ALL_TAB_COLUMNS, ALL_CONSTRAINTS 같은 시스템 뷰는 누구나 접근 가능한 메타데이터 뷰다. 이걸 활용하면 별도 문서 없이도 테이블 구조를 프로그래밍 방식으로 읽어올 수 있다.

 

🔄 전체 흐름

Oracle CCD(Change Capture Data) 스테이징 영역에서 Hadoop DW 본 영역까지의 이관 구조는 이렇다.

단계 영역 설명

① Oracle CRM (원천) 업무 시스템 원천 테이블, 메타데이터는 ALL_TAB_COLUMNS 등으로 조회
② CCD 스테이징 Oracle → Hadoop 임시 적재 영역. 원천과 동일한 구조로 DDL을 맞춰야 한다
③ Hadoop DW 본 영역 분석/서비스에 사용되는 최종 테이블. 유형에 따라 Kudu 또는 Parquet

②~③의 DDL을 수작업으로 만들고 있었는데, ①의 메타데이터를 자동으로 읽어 ②③을 한 번에 생성하도록 바꿨다. PyCharm에서 스크립트를 실행하면 테이블별로 유형을 선택하는 프롬프트가 뜨고, 선택 즉시 .sql 파일이 생성된다.

 

적재 유형은 사람이 판단

타입 매핑은 자동화할 수 있지만, 어떤 스토리지 포맷을 쓸지는 데이터 특성을 알아야 결정할 수 있다. 그래서 코드는 메타데이터 기반으로 힌트를 주되, 최종 선택은 실행 중에 입력받는 구조로 만들었다.

유형 포맷 적합한 케이스

1 · Kudu Kudu 실시간성 필요, UPDATE/UPSERT 발생, PK 기반 단건 조회
2 · Parquet 전건 Parquet 배치 전체 재적재, 단순 덮어쓰기, 변경이 적은 마스터성 테이블
3 · Parquet 파티션 Parquet 대용량 이력 데이터, 기간 조회 빈번, 날짜 컬럼으로 파티션 분리

⚠️ PK 존재 여부가 힌트가 된다. Oracle ALL_CONSTRAINTS에서 CONSTRAINT_TYPE = 'P'로 PK를 확인한다. PK가 있으면 Kudu upsert에 유리하고, 없으면 Parquet 계열이 적합한 경우가 많다. 단, 비즈니스 로직상 업데이트가 없으면 PK가 있어도 Parquet 전건으로 충분하다.

실행하면 이렇게 보인다

테이블별로 메타데이터를 조회한 뒤, 아래처럼 PK 힌트와 함께 유형 선택 프롬프트가 뜬다.

[INFO] Oracle 연결 중... (db-host / SRC_SERVICE / SRC_SCHEMA)
[INFO] Oracle 연결 성공

==============================================================
  테이블 : SRC_SCHEMA.SOME_FACT_TABLE
  설명   : 일별 거래 내역
  컬럼수 : 42개
  PK     : ORG_ID, TXN_DT, TXN_SEQ
  💡 PK 존재 → Kudu(upsert) 또는 Parquet 전건 모두 적합합니다.
--------------------------------------------------------------
    [1] Kudu             (실시간/upsert)
    [2] Parquet 전건     (배치 전체 재적재)
    [3] Parquet 파티션   (대용량 이력/기간 조회)
    [s] 건너뛰기
--------------------------------------------------------------
  선택 > 3
  컬럼 목록 (앞 10개): ORG_ID, TXN_DT, TXN_SEQ, AMT, ...
  파티션 컬럼명 입력 (예: BASE_DT) > TXN_DT
  → ./ddl_output/some_fact_table_parquet_part_txn_dt.sql

 

한 번 유형이 결정되면 table_config.csv에 정리해두고 이후엔 배치로 돌릴 수 있다.

TABLE_NAME,TYPE,PARTITION_COL
SOME_MASTER_TABLE,1,
SOME_CODE_TABLE,2,
SOME_FACT_TABLE,3,TXN_DT
SOME_HIST_TABLE,3,BASE_DT

 

 

Oracle 메타데이터 조회

핵심은 Oracle 시스템 뷰 세 개다. 별도 도구 없이 cx_Oracle로 직접 쿼리한다.

컬럼 정보 — ALL_TAB_COLUMNS

SELECT COLUMN_NAME, COLUMN_ID,
       DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE,
       NULLABLE
  FROM ALL_TAB_COLUMNS
 WHERE OWNER      = :owner
   AND TABLE_NAME = :tname
 ORDER BY COLUMN_ID

PK 컬럼 — ALL_CONSTRAINTS + ALL_CONS_COLUMNS

SELECT acc.COLUMN_NAME
  FROM ALL_CONSTRAINTS  ac
  JOIN ALL_CONS_COLUMNS acc
    ON  ac.OWNER           = acc.OWNER
   AND ac.CONSTRAINT_NAME = acc.CONSTRAINT_NAME
 WHERE ac.CONSTRAINT_TYPE = 'P'
   AND ac.OWNER      = :owner
   AND ac.TABLE_NAME = :tname
 ORDER BY acc.POSITION

코멘트 — ALL_TAB_COMMENTS / ALL_COL_COMMENTS

테이블·컬럼 코멘트까지 읽어서 생성되는 DDL의 COMMENT 절에 자동으로 넣는다. Oracle에서 코멘트를 잘 관리했다면 Hadoop에서도 그대로 이어받을 수 있다.

 

🔁 Oracle → Impala 타입 매핑

가장 신경 쓴 부분은 NUMBER 타입이다. precision/scale 조합에 따라 정수형과 소수형을 분기해야 한다.

Oracle 타입 조건 Impala 타입

NUMBER precision=0, scale=0 (선언만) DECIMAL(38,10)
NUMBER(p,0) p ≤ 9 INT
NUMBER(p,0) p ≤ 18 BIGINT
NUMBER(p,s) s > 0 DECIMAL(p,s)
VARCHAR2(n) n ≤ 255 VARCHAR(n)
VARCHAR2(n) n > 255 STRING
DATE — TIMESTAMP
CLOB / NCLOB — STRING
BLOB / RAW — STRING (fallback)

⚠️ Oracle DATE는 날짜+시간을 모두 담는다. Impala의 DATE 타입(날짜만)이 아닌 TIMESTAMP로 매핑해야 시간 정보 손실을 막을 수 있다.

생성되는 DDL 예시

[1] Kudu

-- =============================================
-- 테이블 : SRC_SCHEMA.SOME_MASTER_TABLE
-- 유형   : Kudu (실시간/upsert)
-- PK     : ORG_ID, ITEM_CD
-- =============================================
CREATE TABLE IF NOT EXISTS dw.some_master_table (
  org_id        VARCHAR(10)   COMMENT '조직 ID',
  item_cd       VARCHAR(20)   COMMENT '아이템 코드',
  item_nm       STRING        COMMENT '아이템명',
  use_yn        CHAR(1)       COMMENT '사용여부',
  reg_dt        TIMESTAMP     COMMENT '등록일시',
  PRIMARY KEY (org_id, item_cd)
)
STORED AS KUDU
TBLPROPERTIES ('kudu.master_addresses' = 'kudu-master:7051');

[3-a] Parquet 파티션 — base_ymd (일 단위)

일별 이력 데이터처럼 데이터가 날마다 쌓이는 테이블. base_ymd 기준으로 파티션을 나눈다.

-- =============================================
-- 테이블 : SRC_SCHEMA.SOME_DAILY_HIST
-- 유형   : Parquet 파티션 (파티션 컬럼: BASE_YMD)
-- PK     : 없음
-- =============================================
CREATE EXTERNAL TABLE IF NOT EXISTS dw.some_daily_hist (
  org_id        VARCHAR(10)    COMMENT '조직 ID',
  cust_id       VARCHAR(20)    COMMENT '고객 ID',
  item_cd       VARCHAR(20)    COMMENT '아이템 코드',
  txn_cnt       INT            COMMENT '거래 건수',
  txn_amt       DECIMAL(15,2)  COMMENT '거래 금액',
  reg_dtm       TIMESTAMP      COMMENT '등록일시'
)
COMMENT '일별 거래 이력'
PARTITIONED BY (base_ymd STRING)  -- 예: '20240101'
STORED AS PARQUET
LOCATION '/user/hive/warehouse/dw.db/some_daily_hist';

-- 파티션 동기화 (Hive Metastore)
-- MSCK REPAIR TABLE dw.some_daily_hist;

[3-b] Parquet 파티션 — base_ym (월 단위)

월별 집계·스냅샷 테이블처럼 파티션 단위가 월인 경우. base_ym으로 파티션을 나누면 특정 월 전체를 한 번에 재적재할 때 편하다.

-- =============================================
-- 테이블 : SRC_SCHEMA.SOME_MONTHLY_SNAP
-- 유형   : Parquet 파티션 (파티션 컬럼: BASE_YM)
-- PK     : ORG_ID, CUST_ID
-- =============================================
CREATE EXTERNAL TABLE IF NOT EXISTS dw.some_monthly_snap (
  org_id        VARCHAR(10)    COMMENT '조직 ID',
  cust_id       VARCHAR(20)    COMMENT '고객 ID',
  grade_cd      CHAR(2)        COMMENT '등급 코드',
  accum_amt     DECIMAL(15,2)  COMMENT '누적 금액',
  use_yn        CHAR(1)        COMMENT '사용여부',
  upd_dtm       TIMESTAMP      COMMENT '수정일시'
)
COMMENT '월별 고객 스냅샷'
PARTITIONED BY (base_ym STRING)  -- 예: '202401'
STORED AS PARQUET
LOCATION '/user/hive/warehouse/dw.db/some_monthly_snap';

-- 파티션 동기화 (Hive Metastore)
-- MSCK REPAIR TABLE dw.some_monthly_snap;

💡 base_ymd vs base_ym 선택 기준 데이터가 날마다 추가되는 이력 테이블이면 base_ymd, 월말 기준으로 스냅샷을 찍거나 월 단위로 재적재하는 집계 테이블이면 base_ym이 적합하다. 파티션이 너무 잘게 나뉘면 Hive Metastore 부하가 커지므로 적재 주기에 맞게 선택한다.


마무리

이관 요청이 자주 오는 편은 아닌데, 올 때는 한꺼번에 온다. 그 타이밍에 테이블 수십 개를 수작업으로 DDL 짜고 있으면 꽤 고역이라 한 번 만들어두길 잘했다 싶었다.

 

만들고 나서 달라진 점

  • 테이블 하나당 10~15분 걸리던 DDL 작성이 유형 선택 포함 1분 이내로 줄었다
  • Oracle 코멘트가 Hadoop DDL에 그대로 이어져 메타데이터 관리가 편해졌다
  • table_config.csv 한 파일로 전체 이관 이력이 관리된다
  • 타입 미스매치로 인한 적재 오류가 거의 사라졌다

메타데이터는 Oracle이 이미 다 갖고 있다. 읽어서 변환하는 코드만 한 번 잘 만들어두면 이후엔 계속 써먹을 수 있다.

+ Recent posts