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

 


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이 이미 다 갖고 있다. 읽어서 변환하는 코드만 한 번 잘 만들어두면 이후엔 계속 써먹을 수 있다.

📘 데이터 엔지니어링 디자인 패턴 · Chapter 10

데이터 관찰 가능성 디자인 패턴

데이터 탐지기 · 시간 탐지기 · 데이터 계보
파이프라인 밖에서 데이터를 지키는 6가지 패턴

📑 목차

10.1 데이터 탐지기 — 패턴 #65~66

10.2 시간 탐지기 — 패턴 #67~68

10.3 데이터 계보 — 패턴 #69~70

9장의 품질 패턴은 데이터셋 자체에 초점을 맞췄습니다. 하지만 데이터 품질에만 의존하는 것은 충분하지 않습니다. AWAP 패턴이 모든 문제를 완벽하게 감지하더라도, 업스트림 흐름이 차단되어 AWAP 잡이 실행조차 안 되는 경우가 생길 수 있기 때문입니다. 이 챕터는 파이프라인 밖에서 데이터와 시간의 이상 신호를 탐지하고, 데이터셋 간의 의존성을 추적하는 6가지 관찰 가능성 패턴을 다룹니다.

10.1

데이터 탐지기

데이터 자체의 문제를 탐지합니다. 데이터가 들어오지 않거나, 예상과 크기가 다른 경우를 잡아냅니다.

패턴 #65

흐름 중단 탐지기

핵심 아이디어: 데이터가 흘러야 할 곳에 흐르지 않을 때를 감지합니다. 데이터 비가용성은 다운스트림 컨슈머가 데이터를 사용할 수 없게 만드는 심각한 문제입니다.

두 가지 탐지 모드

① 지속적인 데이터 전달

1분 또는 1초 단위로 레코드를 기대. 지정된 시간 단위 동안 새 데이터 포인트가 등록되지 않으면 경고 발생.

② 불규칙한 데이터 전달

5분 연속으로 안 들어올 수 있는 데이터. 시간 윈도를 분석하고 허용된 데이터 없는 윈도 기간보다 길면 경고.

탐지 계층 3가지

메타데이터 계층 — 생성 시간, 마지막 수정 시간 기반. 1시간 이상 변화 없으면 경고

데이터 계층 — 실제 데이터를 직접 분석. 처리 요구가 적어 비용이 덜 들지만 메타데이터가 없을 수 있음

스토리지 계층 — 스토리지 공간의 마지막 파일이 쓰인 시간 모니터링

⚠️ 거짓 양성 주의: 컴팩션 같은 유지 관리 작업은 새 데이터를 생성하지 않으므로 흐름 중단 경고가 발생할 수 있습니다. 임곗값 설정이 중요합니다.

💼 실무 관점: 파이프라인에서 매일 새벽 적재가 돼야 하는데, 어느 날 소스 시스템 문제로 데이터가 안 들어왔을 때 다운스트림 팀이 먼저 발견하는 경우가 생깁니다. 흐름 중단 탐지기가 있었다면 적재 완료 시각 기준으로 경고를 받았을 것입니다. 현재 옵저빙 테이블에 "마지막 적재 시각" 컬럼을 추가하면 간단하게 구현 가능합니다.

패턴 #66

스큐 탐지기

핵심 아이디어: 파티션 간 데이터 크기 불균형(스큐)을 감지합니다. 스큐된 파티션은 처리 시간을 늘리거나 불완전한 데이터셋으로 파이프라인을 실패시킬 수 있습니다.

스큐 계산 방법 2가지

① 윈도-윈도 비교

현재 처리 중인 파이프라인과 이전 비교 윈도 사이의 백분율 차이를 계산. 배치 잡과 스트리밍 모두 적용 가능.

② 표준 편차 비율

각 데이터 포인트가 평균에서 얼마나 벗어나는지 측정. STDDEV(x)/AVG(x) * 100으로 계산.

📌 치명적 루프 주의

윈도-윈도 비교에서 스큐가 발생한 경우, 이전 날 데이터셋이 전날의 데이터셋보다 세 배나 작아 스큐 유효성 확인이 실패했다면 — 다음 날까지 문제가 해결되지 않으면 그날도 스큐된 것으로 간주됩니다. 실패한 날과 비교하므로 데이터가 세 배나 많아지는 악순환이 발생합니다.

⚠️ 계절성 문제: 마케팅 캠페인, 연말 등 특정 기간에 데이터가 폭발적으로 증가할 수 있습니다. 이를 스큐로 오인하지 않으려면 비즈니스 지식을 활용해 임곗값을 조정하거나 특정 기간을 무시하는 예외를 추가해야 합니다.

💼 실무 관점: 현재 옵저빙 테이블에서 워크플로우별 평균 적재 건수를 모니터링하는 것이 스큐 탐지의 시작입니다. 여기에 표준 편차를 추가하면 "이번 달 건수가 평균 대비 얼마나 벗어났는가"를 자동으로 감지할 수 있습니다. 실적 집계에서 특정 월 데이터가 갑자기 50% 줄었을 때 알람을 받는 구조입니다.

10.2

시간 탐지기

데이터 외에 시간도 파이프라인에서 중요한 지표입니다. 데이터 지연과 SLA 위반을 탐지하는 두 가지 패턴입니다.

패턴 #67

지연 탐지기

핵심 아이디어: 데이터 컨슈머가 데이터 프로듀서보다 얼마나 뒤처지는지를 측정합니다. 지연이 크면 컨슈머가 처리하는 데이터가 이미 오래된 데이터일 수 있습니다.

지연 계산 핵심 공식

# 기본 공식
last_available_unit = get_last_available_unit()
last_processed_unit = get_last_processed_unit()
lag = last_available_unit - last_processed_unit

# 파티션별 지연 집계 전략
# MAX → 최악의 시나리오 모니터링
# P90/P95 → 대부분의 파티션 상태 파악 (권장)

📌 평균보다 백분위수가 더 적합한 이유

7개 파티션의 지연 지표가 10초, 30초, 3초, 5초, 3초일 때 평균은 8초입니다. 그런데 P90 지연 시간은 18초입니다. 평균 8초를 보고 "잘 됩니다"라고 판단하면 잘못된 결론입니다. 지연 탐지에는 백분위수가 더 적합합니다.

⚠️ 데이터 스큐와의 관계: 한 파티션이 다른 파티션보다 부하가 많으면 컨슈머가 자연스럽게 느리게 처리합니다. 이건 컨슈머 문제가 아니라 업스트림 스큐 문제입니다. 지연 탐지기와 스큐 탐지기를 함께 운용해야 원인을 제대로 파악할 수 있습니다.

💼 실무 관점: Kafka 토픽에서 데이터를 소비하는 파이프라인이라면 컨슈머 그룹의 오프셋 지연을 모니터링하는 것이 이 패턴입니다. Hadoop 배치 환경에서는 "어제 데이터가 오늘 적재됐는가"를 파티션 타임스탬프로 추적할 수 있습니다.

패턴 #68

SLA 위반 탐지기

핵심 아이디어: 처리에 걸린 시간을 최대 허용 실행 시간과 비교합니다. SLA를 초과하면 다운스트림 컨슈머에게 즉시 알림을 보냅니다.

처리 모드별 SLA 측정 방식

배치 잡

시작~종료 시간 차이. 가장 단순. Airflow의 sla 매개변수로 정의 가능.

스트리밍 잡

각 마이크로배치 반복에서 종료~시작 차이. 이벤트 시간 기반 SLA도 별도 모니터링 필요.

📌 처리 시간 SLA vs 이벤트 시간 SLA

처리 시간은 "잡이 얼마나 오래 걸렸나"이고, 이벤트 시간은 "레코드 생성에서 처리까지 얼마나 걸렸나"입니다. 두 영역은 동일하지 않습니다. 예를 들어 프로듀서가 네트워크 연결을 잃고 로컬 버퍼에 데이터를 쌓다가 뒤늦게 전송하면, 이벤트 시간 기반 SLA는 위반할 수 있지만 처리 시간 SLA는 정상일 수 있습니다.

⚠️ Airflow 2.x 주의: SLA 메커니즘은 파이프라인의 실행 시작이 아닌 태스크의 실행 시작 시간부터 SLA를 계산합니다. 8시로 예약된 잡이 08:00:10 이후에 시작하면 이미 지연된 것으로 간주합니다.

💼 실무 관점: 일별 실적 파이프라인이 매일 오전 8시까지 완료돼야 하는데, 어느 날 40분이 걸렸을 때 관련 팀에 자동으로 알람이 가는 구조입니다. Airflow의 sla=timedelta(seconds=2400)처럼 간단하게 설정할 수 있습니다.

10.3

데이터 계보

문제가 발견됐을 때 "누구에게 물어봐야 하나"를 알려면 데이터셋 간의 의존성 그래프가 필요합니다. 데이터 계보는 데이터셋 추적과 컬럼 수준 추적 두 가지 수준에서 작동합니다.

패턴 #69

데이터셋 추적기

핵심 아이디어: 테이블, 폴더, 토픽, 데이터열 등 다양한 컨테이너 간의 의존성 트리를 생성합니다. 데이터 제공자와 컨슈머를 명확히 표현해 문제 발생 시 누구에게 연락해야 하는지 파악할 수 있습니다.

구현 두 가지 방법

① 완전 관리형 클라우드 서비스

Unity Catalog(Databricks), GCP 데이터플렉스, Dataproc. 투명하게 자동 작동하지만 해당 플랫폼에만 제한됩니다.

② 직접 구현

각 파이프라인이 입력/출력을 계보 서비스에 보고. OpenLineage + Marquez UI가 대표적인 오픈소스 조합. 아파치 에어플로와 Spark 모두 지원.

⚠️ 단점: 완전 관리형은 벤더 락인 문제가 있고, 직접 구현은 서비스 간 상호 작용을 모니터링해야 해서 구현 노력이 많이 필요합니다.

💼 실무 관점: "이 테이블 데이터가 이상한데 원인이 뭐야?"라는 질문에 답하려면 이 테이블이 어떤 소스에서 왔는지 알아야 합니다. 지금 환경에서 OpenLineage를 Airflow에 붙이면 DAG별 입력/출력 의존성이 자동으로 시각화됩니다. 데이터 거버넌스가 약한 조직일수록 이 도구의 가치가 큽니다.

패턴 #70

세밀한 추적기

핵심 아이디어: 데이터셋 수준을 넘어 컬럼 또는 레코드 수준의 의존성을 추적합니다. "이 출력 컬럼이 어떤 입력 컬럼에서 왔는가"를 파악할 수 있습니다.

컬럼 계보 예시

쿼리: SELECT CONCAT(u.first_name, d.delivery_address) AS user_with_address FROM users u JOIN addresses d ON u.id = d.user_id

user_with_address 컬럼의 하위 의존성: users.first_name + addresses.delivery_address. 계보 코드가 실행 트리를 분석해 이 관계를 자동으로 파악합니다.

⚠️ 한계: 사용자 정의 코드(Python 매핑 함수 등)로 데이터를 변환할 때는 계보를 자동으로 추출하기 어렵습니다. SQL 함수는 잘 동작하지만, 커스텀 변환 로직은 블랙박스처럼 보입니다.

💼 실무 관점: 역정규화된 분석 테이블에서 특정 컬럼 값이 이상할 때, "이 컬럼이 어떤 소스 테이블의 어떤 컬럼에서 왔는지"를 즉시 파악할 수 있습니다. 지금처럼 코드를 직접 뒤져야 하는 상황을 줄여줍니다. 30개 이상의 컬럼으로 확장된 테이블에서 새 팀원이 의존성을 파악할 때 특히 유용합니다.

📌 챕터 10 핵심 요약

🚨

데이터 탐지기: 흐름 중단 탐지기는 데이터가 아예 안 들어오는 상황을 잡습니다. 스큐 탐지기는 데이터가 들어와도 파티션 간 불균형이 심각한 경우를 감지합니다. 둘 다 AWAP 패턴이 작동하기도 전에 문제를 잡는 첫 번째 방어선입니다.

⏱️

시간 탐지기: 지연 탐지기는 컨슈머가 프로듀서를 얼마나 뒤따르는지 측정합니다. SLA 위반 탐지기는 처리 시간이 약속된 시간을 넘었는지 확인합니다. 평균보다 백분위수로 측정하는 것이 더 신뢰할 수 있습니다.

🗺️

데이터 계보: 데이터셋 추적기는 테이블/토픽 간 의존성 그래프를 만듭니다. 세밀한 추적기는 컬럼 수준까지 내려가 "이 컬럼은 어디서 왔는가"를 추적합니다. OpenLineage + Marquez가 대표적인 오픈소스 조합입니다.

📖 책을 마치며

이 책은 데이터 엔지니어링 스택이 아무리 바뀌어도 변하지 않을 설계 원칙들을 다룹니다. SQL이 다른 쿼리 언어로, Spark가 다른 프레임워크로 대체되더라도 — 데이터를 가져오는 방법, 품질을 보장하는 방법, 관찰 가능성을 확보하는 방법은 여전히 필요합니다. 10개 챕터, 70개 패턴을 통해 실무에서 직접 마주친 문제들과 연결 지으면서 읽을 수 있었습니다.

+ Recent posts