<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>newnew_daddy.log</title>
        <link>https://velog.io/</link>
        <description>데이터 엔지니어의 작업공간 / #PYTHON #CLOUD #SPARK #AWS #GCP #NCLOUD</description>
        <lastBuildDate>Sun, 22 Mar 2026 01:08:58 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>newnew_daddy.log</title>
            <url>https://velog.velcdn.com/images/newnew_daddy/profile/b6d6fdd3-b6de-4ec8-8e29-83ec551ab59d/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. newnew_daddy.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/newnew_daddy" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Cloud VM에 Apache Airflow 설치하기]]></title>
            <link>https://velog.io/@newnew_daddy/AIRFLOW07</link>
            <guid>https://velog.io/@newnew_daddy/AIRFLOW07</guid>
            <pubDate>Sun, 22 Mar 2026 01:08:58 GMT</pubDate>
            <description><![CDATA[<h3 id="✅-1-airflow-vm-최소권장-사양">✅ 1) Airflow VM 최소/권장 사양</h3>
<p>👉 <a href="https://airflow.apache.org/docs/apache-airflow/stable/installation/prerequisites.html">공식 문서</a></p>
<p>Airflow는 여러 컨테이너(Webserver, Scheduler, DB, Redis 등)가 함께 동작하기 때문에, VM 리소스가 너무 작으면 설치는 되더라도 실행이 불안정할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>CPU</th>
<th>RAM</th>
<th>디스크</th>
</tr>
</thead>
<tbody><tr>
<td>최소</td>
<td>2 vCPU</td>
<td>4GB</td>
<td>-</td>
</tr>
<tr>
<td>권장</td>
<td>2 vCPU</td>
<td>8~16GB</td>
<td>20GB 이상</td>
</tr>
</tbody></table>
<hr>
<h3 id="✅-2-docker-설치">✅ 2) Docker 설치</h3>
<p>VM에 Docker 및 Docker Compose 플러그인을 설치합니다.<br>아래 스크립트를 파일로 저장 후 실행하면 한 번에 설치할 수 있습니다.</p>
<h4 id="docker_installsh"><code>docker_install.sh</code></h4>
<blockquote>
<p>파일 생성 후 실행 → bash docker_install.sh</p>
</blockquote>
<pre><code class="language-bash">#!/bin/bash

set -e

echo &quot;=== Docker 및 Docker Compose 설치 스크립트 ===&quot;

# 1. 기존 Docker 제거
echo &quot;[1/6] 기존 Docker 제거...&quot;
sudo apt-get remove -y docker docker-engine docker.io containerd runc || true

# 2. 패키지 업데이트 및 의존성 설치
echo &quot;[2/6] 패키지 업데이트 및 의존성 설치...&quot;
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# 3. Docker 공식 GPG 키 추가
echo &quot;[3/6] Docker GPG 키 추가...&quot;
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# 4. Docker 리포지토리 등록
echo &quot;[4/6] Docker 리포지토리 추가...&quot;
echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&quot; | \
  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null

# 5. Docker Engine 설치
echo &quot;[5/6] Docker 설치...&quot;
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# 6. 설치 확인
echo &quot;[6/6] 설치 확인...&quot;
docker --version
docker compose version

echo &quot;✅ Docker 및 Docker Compose 설치 완료!&quot;</code></pre>
<hr>
<h3 id="✅-3-vm-방화벽-설정">✅ 3) VM 방화벽 설정</h3>
<p>Airflow Web UI는 기본적으로 <strong>8080 포트</strong>에서 실행됩니다.<br>따라서 VM 외부에서 웹 UI에 접속하려면 <strong>클라우드의 방화벽 설정</strong> 에서 8080 포트를 허용해야 합니다.</p>
<ul>
<li>AWS → VPC 보안그룹(Security Group) 설정</li>
<li>GCP → VPC 방화벽 규칙 설정 (<a href="https://youtu.be/6wXNwkcRVQ8?si=syAhmEtdMBb94j1D">참고 영상</a>)</li>
</ul>
<hr>
<h3 id="✅-4-airflow-설치-docker-compose-기반">✅ 4) Airflow 설치 (Docker Compose 기반)</h3>
<p>Airflow는 공식 문서에서 제공하는 <code>docker-compose.yaml</code> 파일로 빠르게 설치할 수 있습니다.</p>
<p>👉 <a href="https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html">공식 문서</a></p>
<hr>
<h5 id="1-작업-디렉토리-생성-및-이동">1) 작업 디렉토리 생성 및 이동</h5>
<p>Airflow 관련 파일을 관리할 디렉토리를 만들고 이동합니다.</p>
<pre><code class="language-bash">mkdir Apache_Airflow
cd Apache_Airflow</code></pre>
<h5 id="2-docker-composeyaml-다운로드">2) docker-compose.yaml 다운로드</h5>
<p>아래 명령어로 Airflow 공식 docker-compose.yaml 파일을 다운로드합니다.</p>
<pre><code class="language-bash">curl -LfO &#39;https://airflow.apache.org/docs/apache-airflow/3.1.5/docker-compose.yaml&#39;</code></pre>
<h5 id="3-예제-dag-로딩-비활성화">3) 예제 DAG 로딩 비활성화</h5>
<p><code>docker-compose.yaml</code> 파일에서 아래 설정을 찾아 값을 변경합니다.</p>
<ul>
<li><code>AIRFLOW__CORE__LOAD_EXAMPLES: &#39;true&#39;</code> → <code>&#39;false&#39;</code></li>
</ul>
<h5 id="4-필요한-폴더-및-env-파일-생성">4) 필요한 폴더 및 .env 파일 생성</h5>
<p>Airflow가 기본적으로 사용하는 디렉토리 및 파일들을 만들어줍니다. (디렉토리 4개 + 파일 1개)</p>
<pre><code class="language-bash">mkdir -p ./dags ./logs ./plugins ./config
echo -e &quot;AIRFLOW_UID=50000&quot; &gt; .env</code></pre>
<h5 id="5-airflow-초기화-수행">5) Airflow 초기화 수행</h5>
<p>초기 DB 셋업 등을 진행합니다.
명령 실행 후 터미널로 돌아올 때까지 기다립니다.</p>
<pre><code class="language-bash">sudo docker compose up airflow-init</code></pre>
<h5 id="6-airflow-실행">6) Airflow 실행</h5>
<p>전체 컨테이너를 실행합니다.</p>
<pre><code class="language-bash">sudo docker compose up -d</code></pre>
<h5 id="7-모든-컨테이너가-정상-상태healthy-될-때까지-대기">7) 모든 컨테이너가 정상 상태(healthy) 될 때까지 대기</h5>
<p>Airflow는 여러 컨테이너가 동시에 떠야 정상적으로 동작합니다.
아래 명령어로 컨테이너 상태를 모니터링합니다.</p>
<pre><code class="language-bash">sudo watch -n 1 docker ps</code></pre>
<blockquote>
<p>총 7개 컨테이너가 모두 <code>healthy</code> 상태가 될 때까지 기다립니다.</p>
</blockquote>
<h5 id="8-airflow-ui-접속">8) Airflow UI 접속</h5>
<p>&#39;healthy&#39; 확인 후 VM의 공인 IP를 확인한 후 아래 URL로 접속합니다.</p>
<pre><code>http://[Compute Engine 공인 IP]:8080

ID : airflow
PW : airflow</code></pre><h3 id="✅-5-airflow-작업-디렉토리-권한-변경">✅ 5) Airflow 작업 디렉토리 권한 변경</h3>
<p>Docker Compose로 Airflow를 설치하면 <code>dags/</code>, <code>logs/</code>, <code>plugins/</code>, <code>config/</code> 등의 폴더를 컨테이너와 <strong>volume(bind mount)</strong> 으로 연결하게 됩니다.<br>이 과정에서 컨테이너가 파일을 생성하거나 수정하면, <strong>파일 소유자가 내 계정이 아닌 다른 사용자(root/airflow)</strong> 로 잡히는 경우가 있어 VM에서 직접 편집할 때 권한 오류(Permission denied)가 발생할 수 있습니다.</p>
<p>이를 해결하려면, 아래 명령어를 통해 Airflow 작업 디렉토리의 소유권을 현재 로그인한 사용자 계정으로 변경해주면 됩니다.</p>
<pre><code class="language-bash">sudo chown -R &quot;${USER:-$(id -un)}&quot; .</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Airflow의 다양한 스케줄링 방식 총정리!]]></title>
            <link>https://velog.io/@newnew_daddy/AIRFLOW06</link>
            <guid>https://velog.io/@newnew_daddy/AIRFLOW06</guid>
            <pubDate>Mon, 02 Feb 2026 01:47:44 GMT</pubDate>
            <description><![CDATA[<h1 id="0-intro">0. INTRO</h1>
<h3 id="왜-airflow-스케줄링을-이해해야-할까">왜 Airflow 스케줄링을 이해해야 할까?</h3>
<p>데이터 파이프라인을 운영하다 보면 &quot;매일 새벽에 ETL을 돌리고 싶다&quot;, &quot;평일 오후에만 리포트를 생성하고 싶다&quot;, &quot;3일마다 한 번씩 배치를 돌리고 싶다&quot; 같은 요구가 생깁니다. Apache Airflow에서는 DAG의 <strong>schedule</strong> 파라미터로 이런 실행 주기를 정의합니다. 스케줄 방식을 잘못 설정하면 과거 구간이 한꺼번에 돌아가는 <strong>catchup</strong> 문제가 생기거나, 의도와 다른 시간에 DAG가 실행될 수 있어요. 이 글에서는 Airflow가 제공하는 4가지 스케줄링 방식(Presets, Cron, Timedelta/Delta, Timetable)을 개념부터 코드 예시, 실무 선택 기준까지 정리합니다.</p>
<h3 id="airflow-스케줄링-방식-개요">Airflow 스케줄링 방식 개요</h3>
<p>Airflow에서 DAG 실행 시점을 정하는 방식은 크게 네 가지로 나눌 수 있습니다.</p>
<ol>
<li><strong>Presets (사전 정의 문자열)</strong> — <code>@daily</code>, <code>@hourly</code> 등 고정된 의미의 문자열</li>
<li><strong>Cron Expression</strong> — Unix cron 형식의 5필드 표현식</li>
<li><strong>Timedelta / DeltaDataIntervalTimetable</strong> — 고정 간격(예: 3일마다, 6시간마다)</li>
<li><strong>Timetable (Custom / EventsTimetable)</strong> — 불규칙한 날짜·이벤트 기반 스케줄</li>
</ol>
<hr>
<h1 id="1-presets-사전-정의-문자열">1. Presets (사전 정의 문자열)</h1>
<h3 id="1-개요">1) 개요</h3>
<p><strong>주요 기능:</strong></p>
<ul>
<li>Airflow가 미리 정의한 문자열로, &quot;매일 자정&quot;, &quot;매시간&quot;, &quot;매주 일요일&quot; 등 흔한 주기를 한 번에 표현</li>
<li>내부적으로 cron 표현식으로 변환되어 처리됨</li>
<li>코드가 짧고 의도가 바로 읽혀서 초보자에게 적합</li>
</ul>
<p><strong>실무 관점:</strong></p>
<ul>
<li>일일 리포트, 시간 단위 로그 수집, 주간/월간 정산처럼 <strong>규칙적인 주기</strong>가 있을 때 가장 먼저 고려</li>
<li>&quot;매일 새벽 0시에 돌리면 된다&quot; 수준이면 <code>@daily</code> 한 줄로 해결</li>
<li>나중에 &quot;매일 오전 9시로 바꿔 달라&quot;는 요구가 생기면 cron 표현식(<code>0 9 * * *</code>)으로 전환하는 경우가 많음</li>
</ul>
<p><strong>언제 필요한가?</strong></p>
<ul>
<li>단순하고 일반적인 주기만 필요할 때</li>
<li>팀 내에서 &quot;매일/매시간/매주&quot;라는 표현으로 소통할 때</li>
</ul>
<table>
<thead>
<tr>
<th>Preset</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>@daily</code></td>
<td>매일 자정 (0 0 * * *)</td>
</tr>
<tr>
<td><code>@hourly</code></td>
<td>매 시간 정각 (0 * * * *)</td>
</tr>
<tr>
<td><code>@weekly</code></td>
<td>매주 일요일 자정 (0 0 * * 0)</td>
</tr>
<tr>
<td><code>@monthly</code></td>
<td>매월 1일 자정 (0 0 1 * *)</td>
</tr>
<tr>
<td><code>@yearly</code></td>
<td>매년 1월 1일 자정</td>
</tr>
<tr>
<td><code>@once</code></td>
<td>한 번만 실행</td>
</tr>
<tr>
<td><code>None</code></td>
<td>스케줄 없음 (수동 트리거)</td>
</tr>
</tbody></table>
<pre><code class="language-python">from airflow import DAG
from airflow.operators.empty import EmptyOperator
from pendulum import datetime

with DAG(
    dag_id=&quot;preset_example&quot;,
    schedule=&quot;@daily&quot;,  # 매일 자정 실행
    start_date=datetime(2026, 1, 1, tz=&quot;Asia/Seoul&quot;),
    catchup=False,
):
    task = EmptyOperator(task_id=&quot;daily_task&quot;)</code></pre>
<hr>
<h1 id="2-cron-expression">2. Cron Expression</h1>
<h3 id="1-개요-1">1) 개요</h3>
<p><strong>주요 기능:</strong></p>
<ul>
<li>Unix cron과 동일한 5필드 형식(분, 시, 일, 월, 요일)으로 실행 시점을 세밀하게 지정</li>
<li>&quot;평일 오후 4시&quot;, &quot;매월 1일·15일 자정&quot;, &quot;30분마다&quot; 등 복합 조건 표현 가능</li>
<li>Airflow 내부에서는 <strong>CronDataIntervalTimetable</strong>로 변환되어 동작</li>
</ul>
<p><strong>실무 관점:</strong></p>
<ul>
<li>업무 시간대에만 돌리거나(예: 평일 9–18시), 특정 요일·특정 일자에만 실행해야 할 때 cron이 표준 선택</li>
<li>예: <code>0 16 * * MON-FRI</code> → 평일 오후 4시, <code>0 0 1,15 * *</code> → 매월 1일·15일 자정</li>
<li>타임존은 DAG의 <code>start_date</code> 등에서 사용하는 datetime에 <code>tz</code>를 지정해 일관되게 맞추는 것이 중요</li>
</ul>
<p><strong>언제 필요한가?</strong></p>
<ul>
<li>&quot;특정 요일&quot;, &quot;특정 시각&quot;, &quot;특정 일자&quot; 조합이 필요할 때</li>
<li>Preset으로는 표현이 안 되는 구체적인 시간 조건이 있을 때</li>
</ul>
<pre><code>┌─────── 분 (0-59)
│ ┌───── 시 (0-23)
│ │ ┌─── 일 (1-31)
│ │ │ ┌─ 월 (1-12)
│ │ │ │ ┌ 요일 (0-6, 0=일요일)
│ │ │ │ │
* * * * *</code></pre><pre><code class="language-python">from airflow import DAG
from airflow.operators.empty import EmptyOperator
from pendulum import datetime

with DAG(
    dag_id=&quot;cron_example&quot;,
    schedule=&quot;0 16 * * MON-FRI&quot;,  # 평일 오후 4시
    start_date=datetime(2026, 1, 1, tz=&quot;Asia/Seoul&quot;),
    catchup=False,
):
    task = EmptyOperator(task_id=&quot;weekday_afternoon_task&quot;)</code></pre>
<p>자주 쓰는 패턴:</p>
<ul>
<li><code>0 9 * * *</code> → 매일 오전 9시</li>
<li><code>*/30 * * * *</code> → 30분마다</li>
<li><code>0 0 1,15 * *</code> → 매월 1일, 15일 자정</li>
</ul>
<hr>
<h1 id="3-timedelta--deltadataintervaltimetable">3. Timedelta / DeltaDataIntervalTimetable</h1>
<h3 id="1-timedelta-직접-사용-간단한-방식">1) timedelta 직접 사용 (간단한 방식)</h3>
<p><strong>주요 기능:</strong></p>
<ul>
<li>&quot;N일마다&quot;, &quot;N시간마다&quot;, &quot;N분마다&quot;처럼 <strong>고정 간격</strong> 실행을 표현</li>
<li><code>schedule=timedelta(days=3)</code>처럼 넣으면 Airflow가 내부적으로 <strong>DeltaDataIntervalTimetable</strong>로 변환</li>
</ul>
<p><strong>실무 관점:</strong></p>
<ul>
<li>&quot;3일마다 한 번&quot;, &quot;6시간마다 한 번&quot; 같은 주기는 cron보다 timedelta가 의도가 분명함</li>
<li>예: <code>timedelta(days=3)</code>, <code>timedelta(hours=6)</code>, <code>timedelta(minutes=30)</code></li>
<li>cron은 &quot;언제&quot;를 지정하고, timedelta는 &quot;간격&quot;을 지정한다는 차이를 두고 선택하면 됨</li>
</ul>
<p><strong>언제 필요한가?</strong></p>
<ul>
<li>실행 시점이 &quot;매일 0시&quot;가 아니라 &quot;N시간/ N일 간격&quot;일 때</li>
<li>주기만 중요하고 정각 맞춤이 필요 없을 때</li>
</ul>
<pre><code class="language-python">from airflow import DAG
from airflow.operators.empty import EmptyOperator
from pendulum import datetime
from datetime import timedelta

with DAG(
    dag_id=&quot;timedelta_simple_example&quot;,
    schedule=timedelta(days=3),  # 3일마다 실행
    start_date=datetime(2026, 1, 1, tz=&quot;Asia/Seoul&quot;),
    catchup=False,
):
    task = EmptyOperator(task_id=&quot;every_3_days_task&quot;)</code></pre>
<h3 id="2-deltadataintervaltimetable-사용-명시적-방식">2) DeltaDataIntervalTimetable 사용 (명시적 방식)</h3>
<p><strong>주요 기능:</strong></p>
<ul>
<li>Timetable 객체를 직접 지정해 &quot;고정 간격&quot; 스케줄을 명시적으로 표현</li>
<li>커스텀 Timetable과 조합하거나, 코드에서 &quot;Timetable 기반 스케줄&quot;임을 드러내고 싶을 때 사용</li>
</ul>
<p><strong>실무 관점:</strong></p>
<ul>
<li><code>schedule=timedelta(...)</code>와 동작은 동일함. 다만 &quot;Timetable을 쓰고 있다&quot;는 것이 코드에 드러남</li>
<li>커스텀 Timetable을 만들거나, 여러 Timetable을 조합하는 고급 패턴으로 넘어갈 때 같은 계열로 이해하면 됨</li>
</ul>
<p><strong>언제 필요한가?</strong></p>
<ul>
<li>고정 간격을 Timetable API 수준에서 명시하고 싶을 때</li>
<li>커스텀 Timetable과의 일관성을 위해 같은 방식으로 쓰고 싶을 때</li>
</ul>
<pre><code class="language-python">from airflow import DAG
from airflow.operators.empty import EmptyOperator
from airflow.timetables.simple import DeltaDataIntervalTimetable
from pendulum import datetime
from datetime import timedelta

every_3_days = DeltaDataIntervalTimetable(timedelta(days=3))

with DAG(
    dag_id=&quot;delta_timetable_example&quot;,
    schedule=every_3_days,
    start_date=datetime(2026, 1, 1, tz=&quot;Asia/Seoul&quot;),
    catchup=False,
):
    task = EmptyOperator(task_id=&quot;every_3_days_task&quot;)</code></pre>
<table>
<thead>
<tr>
<th>구분</th>
<th>timedelta 직접 사용</th>
<th>DeltaDataIntervalTimetable</th>
</tr>
</thead>
<tbody><tr>
<td>코드</td>
<td>간결함</td>
<td>명시적</td>
</tr>
<tr>
<td>내부 동작</td>
<td>자동으로 Timetable로 변환됨</td>
<td>직접 Timetable 사용</td>
</tr>
<tr>
<td>사용 시점</td>
<td>단순한 경우</td>
<td>커스텀 Timetable과 조합 시</td>
</tr>
</tbody></table>
<blockquote>
<p>💡 <code>schedule=timedelta(...)</code>를 넣으면 Airflow 내부에서 <code>DeltaDataIntervalTimetable</code>로 자동 변환됩니다. 결과는 동일합니다.</p>
</blockquote>
<hr>
<h1 id="4-timetable-custom--eventstimetable">4. Timetable (Custom / EventsTimetable)</h1>
<h3 id="1-eventstimetable">1) EventsTimetable</h3>
<p><strong>주요 기능:</strong></p>
<ul>
<li><strong>특정 datetime 목록</strong>만큼만 DAG를 실행하도록 함</li>
<li>공휴일, 이벤트일, 월말 등 불규칙한 날짜에만 돌리고 싶을 때 사용</li>
<li>Airflow 2.2+에서 도입된 Timetable API의 대표 활용 예</li>
</ul>
<p><strong>실무 관점:</strong></p>
<ul>
<li>&quot;이 날짜들에만 실행&quot;이 필요할 때(예: 월별 마감일, 특별 이벤트 일자)</li>
<li><code>event_dates</code> 리스트를 코드나 설정에서 생성해 넘기면 됨</li>
<li><code>catchup=True</code>와 조합하면 지정한 과거 날짜들도 한 번씩 실행 가능</li>
</ul>
<p><strong>언제 필요한가?</strong></p>
<ul>
<li>실행 일자가 규칙적인 주기가 아닐 때</li>
<li>공휴일 제외, 특정 영업일만 등 <strong>불규칙한 날짜 집합</strong>이 필요할 때</li>
</ul>
<pre><code class="language-python">from airflow.sdk import dag, task
from pendulum import datetime
from airflow.timetables.events import EventsTimetable

special_dates = EventsTimetable(
    event_dates=[
        datetime(2026, 1, 1, tz=&quot;Asia/Seoul&quot;),
        datetime(2026, 1, 15, tz=&quot;Asia/Seoul&quot;),
        datetime(2026, 1, 26, tz=&quot;Asia/Seoul&quot;),
        datetime(2026, 1, 30, tz=&quot;Asia/Seoul&quot;),
    ]
)

@dag(
    schedule=special_dates,
    start_date=datetime(2026, 1, 1, tz=&quot;Asia/Seoul&quot;),
    end_date=datetime(2026, 1, 31, tz=&quot;Asia/Seoul&quot;),
    catchup=True,
)
def events_timetable_example():
    @task
    def run_on_special_date():
        print(&quot;특별한 날에만 실행!&quot;)

    run_on_special_date()

events_timetable_example()</code></pre>
<hr>
<h1 id="5-스케줄링-방식-요약">5. 스케줄링 방식 요약</h1>
<table>
<thead>
<tr>
<th>방식</th>
<th>사용 시점</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Presets</strong></td>
<td>단순하고 일반적인 주기</td>
<td><code>@daily</code>, <code>@hourly</code></td>
</tr>
<tr>
<td><strong>Cron</strong></td>
<td>특정 시간/요일/일자 지정 필요</td>
<td><code>0 16 * * MON-FRI</code></td>
</tr>
<tr>
<td><strong>Timedelta</strong></td>
<td>고정 간격 실행</td>
<td><code>timedelta(days=3)</code></td>
</tr>
<tr>
<td><strong>DeltaDataIntervalTimetable</strong></td>
<td>고정 간격 (명시적)</td>
<td>Timetable 객체로 조합</td>
</tr>
<tr>
<td><strong>EventsTimetable</strong></td>
<td>불규칙 날짜, 이벤트 기반</td>
<td>공휴일, 이벤트 날짜 등</td>
</tr>
</tbody></table>
<hr>
<h1 id="6-timetable-import-정리">6. Timetable Import 정리</h1>
<pre><code class="language-python"># Delta 관련
from datetime import timedelta
from airflow.timetables.simple import DeltaDataIntervalTimetable

# Events 관련
from airflow.timetables.events import EventsTimetable

# Cron 관련 (필요시)
from airflow.timetables.simple import CronDataIntervalTimetable</code></pre>
<hr>
<h1 id="7-실무-활용-가이드">7. 실무 활용 가이드</h1>
<p><strong>신규 DAG 설계 시 선택 순서</strong></p>
<ol>
<li>&quot;매일/매시간/매주&quot; 수준이면 → <strong>Presets</strong> (<code>@daily</code>, <code>@hourly</code> 등)</li>
<li>&quot;평일 오후 4시&quot;, &quot;매월 1일·15일&quot;처럼 구체 시각/요일/일자가 필요하면 → <strong>Cron</strong></li>
<li>&quot;N시간/ N일마다&quot; 간격이 중요하면 → <strong>Timedelta</strong> (또는 필요 시 DeltaDataIntervalTimetable)</li>
<li>날짜가 불규칙하면 → <strong>EventsTimetable</strong> 또는 커스텀 Timetable</li>
</ol>
<p><strong>주의사항</strong></p>
<ul>
<li><code>start_date</code>와 <code>catchup</code>: 과거 구간을 채울지 여부를 <code>catchup</code>로 제어. 기본값이 True이므로 의도치 않은 대량 실행을 막으려면 <code>catchup=False</code>를 자주 사용함</li>
<li>타임존: <code>pendulum.datetime(..., tz=&quot;Asia/Seoul&quot;)</code> 등으로 DAG와 태스크에서 타임존을 통일할 것</li>
<li>Timetable은 Airflow 2.2+ 기능이며, <code>schedule</code>이 내부적으로 어떻게 Timetable로 매핑되는지 이해하면 디버깅과 확장에 유리함</li>
</ul>
<hr>
<h1 id="8-마무리">8. 마무리</h1>
<ul>
<li>Airflow 스케줄은 <strong>Presets → Cron → Timedelta/Delta → EventsTimetable(커스텀)</strong> 순으로 &quot;단순 규칙 → 세밀한 시간 → 간격 → 불규칙 일자&quot;를 다룹니다.</li>
<li>실무에서는 &quot;매일 새벽&quot;이면 Preset, &quot;평일 특정 시각&quot;이면 Cron, &quot;N일마다&quot;면 Timedelta, &quot;이 날들만&quot;이면 EventsTimetable로 정리하면 선택이 쉽습니다.</li>
<li><code>catchup</code>과 타임존 설정을 함께 점검하면 예상치 못한 실행을 줄일 수 있습니다.</li>
</ul>
<hr>
<p><strong>참고 자료:</strong></p>
<ul>
<li><a href="https://airflow.apache.org/docs/">Apache Airflow Documentation - DAGs</a></li>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/dags.html#timetable">Airflow Timetables</a> (공식 문서 내 Timetable 설명)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Databricks 플랫폼 아키텍처 상세 가이드]]></title>
            <link>https://velog.io/@newnew_daddy/DATABRICKS03</link>
            <guid>https://velog.io/@newnew_daddy/DATABRICKS03</guid>
            <pubDate>Tue, 27 Jan 2026 09:49:48 GMT</pubDate>
            <description><![CDATA[<h1 id="0-intro">0. INTRO</h1>
<h3 id="왜-databricks-아키텍처를-이해해야-할까">왜 Databricks 아키텍처를 이해해야 할까?</h3>
<p>Databricks를 처음 사용하는 개발자나 관리자에게 가장 혼란스러운 부분 중 하나는 &quot;데이터가 어디에 저장되는가?&quot;, &quot;컴퓨팅 리소스는 어디서 실행되는가?&quot;, &quot;보안은 어떻게 구성되는가?&quot;와 같은 아키텍처 관련 질문입니다. </p>
<p>Databricks는 전통적인 단일 계정 구조가 아니라, <strong>Databricks 계정</strong>과 <strong>고객 계정</strong>으로 분리된 하이브리드 아키텍처를 사용합니다. 이 구조를 이해하지 못하면, 네트워크 설정, 보안 정책, 비용 관리 등에서 예상치 못한 문제에 직면할 수 있습니다.</p>
<p>이 글에서는 Databricks의 데이터 인텔리전스 플랫폼 아키텍처를 계층별로 설명하고, 각 구성 요소의 역할과 상호작용을 이해할 수 있도록 정리합니다.</p>
<hr>
<h3 id="databricks-아키텍처의-핵심-개념">Databricks 아키텍처의 핵심 개념</h3>
<p>Databricks 플랫폼은 크게 두 가지 계정 영역으로 나뉘어 운영됩니다:</p>
<ol>
<li><p><strong>Databricks 계정 (Databricks Account)</strong></p>
<ul>
<li>Databricks가 직접 관리하는 영역</li>
<li>Control Plane과 Serverless Compute가 배포됨</li>
<li>고객의 클라우드 계정과 분리되어 운영</li>
</ul>
</li>
<li><p><strong>고객 계정 (Customer Account)</strong></p>
<ul>
<li>고객의 클라우드 환경(AWS, Azure, GCP) 내에 존재</li>
<li>데이터 저장소(Cloud Storage)와 Classic Compute가 실행됨</li>
<li>고객이 직접 관리하는 네트워크 및 보안 설정 적용</li>
</ul>
</li>
</ol>
<p>이러한 분리 구조는 다음과 같은 이점을 제공합니다:</p>
<ul>
<li><strong>보안 격리</strong>: Control Plane과 데이터 저장소를 분리하여 보안 강화</li>
<li><strong>유연한 배포</strong>: Serverless와 Classic Compute를 선택적으로 사용 가능</li>
<li><strong>비용 최적화</strong>: 사용 패턴에 따라 적절한 컴퓨팅 모델 선택 가능</li>
</ul>
<hr>
<h1 id="1-전체-플랫폼-구조-high-level-architecture">1. 전체 플랫폼 구조 (High-Level Architecture)</h1>
<h3 id="1-아키텍처-다이어그램-개념">1) 아키텍처 다이어그램 개념</h3>
<pre><code>┌─────────────────────────────────────────────────────────┐
│              Databricks 계정 (Databricks Account)   │
│  ┌──────────────────────────────────────────────────┐   │
│  │         Control Plane (컨트롤 플레인)          │   │
│  │  - Web App                                   │   │
│  │  - Unity Catalog                             │   │
│  │  - Workflow Management                       │   │
│  │  - Intelligence Engine                       │   │
│  └──────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────┐   │
│  │    Serverless Compute Plane (서버리스 컴퓨팅)  │   │
│  │  - Serverless SQL Warehouse                  │   │
│  │  - Model Serving                             │   │
│  │  - Vector Search                             │   │
│  │  - Online Tables                             │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
                          │
                          │ 네트워크 연결
                          │
┌─────────────────────────────────────────────────────────┐
│            고객 계정 (Customer Cloud Account)        │
│  ┌──────────────────────────────────────────────────┐   │
│  │         Classic Compute Plane                │   │
│  │  - Workspace Clusters                        │   │
│  │  - Classic SQL Warehouse                     │   │
│  └──────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────┐   │
│  │              Cloud Storage                   │   │
│  │  - S3 / ADLS / GCS                           │   │
│  │  - Delta Tables                              │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘</code></pre><h3 id="2-계정-간-데이터-흐름">2) 계정 간 데이터 흐름</h3>
<ol>
<li><strong>사용자 요청</strong>: 사용자가 Web App을 통해 쿼리나 작업을 요청</li>
<li><strong>Control Plane 처리</strong>: Unity Catalog가 권한을 확인하고 작업을 스케줄링</li>
<li><strong>Compute 실행</strong>: Classic 또는 Serverless Compute에서 실제 데이터 처리</li>
<li><strong>Storage 접근</strong>: Compute가 고객 계정의 Cloud Storage에서 데이터 읽기/쓰기</li>
<li><strong>결과 반환</strong>: 처리 결과를 사용자에게 반환</li>
</ol>
<hr>
<h1 id="2-주요-구성-요소-상세-설명">2. 주요 구성 요소 상세 설명</h1>
<h3 id="1-control-plane-컨트롤-플레인">1) Control Plane (컨트롤 플레인)</h3>
<p>Control Plane은 Databricks의 두뇌 역할을 하며, 플랫폼의 모든 관리 및 오케스트레이션 기능을 제공합니다.</p>
<h4 id="web-app">Web App</h4>
<p><strong>역할:</strong> 사용자가 Databricks 플랫폼에 접속하는 주요 인터페이스</p>
<p><strong>주요 기능:</strong></p>
<ul>
<li>노트북 작성 및 실행</li>
<li>SQL 쿼리 실행</li>
<li>작업(Jobs) 관리 및 모니터링</li>
<li>Unity Catalog를 통한 데이터 탐색</li>
<li>클러스터 및 워크스페이스 관리</li>
</ul>
<p><strong>실무 관점:</strong></p>
<ul>
<li>Web App은 Databricks 계정에 배포되어 있으므로, 인터넷 연결만 있으면 어디서든 접근 가능</li>
<li>SSO(Single Sign-On)를 통해 기업 인증 시스템과 통합 가능</li>
</ul>
<h4 id="unity-catalog">Unity Catalog</h4>
<p><strong>역할:</strong> 데이터 거버넌스의 핵심으로, 모든 데이터 객체에 대한 중앙 집중식 메타데이터 및 권한 관리</p>
<p><strong>주요 기능:</strong></p>
<ul>
<li><strong>접근 제어(Access Control)</strong>: 테이블, 뷰, 함수 등에 대한 세밀한 권한 관리</li>
<li><strong>메타데이터 관리</strong>: 데이터 객체의 스키마, 통계, 리니지 정보 저장</li>
<li><strong>데이터 리니지</strong>: 데이터의 출처와 변환 과정 추적</li>
<li><strong>데이터 검색</strong>: 메타데이터 기반 데이터 검색 및 탐색</li>
</ul>
<p><strong>실무 관점:</strong></p>
<ul>
<li>Unity Catalog는 Databricks 계정에 위치하지만, 고객 계정의 스토리지에 대한 메타데이터를 관리</li>
<li>여러 워크스페이스에서 동일한 메타스토어를 공유하여 데이터 일관성 유지</li>
</ul>
<h4 id="workflow-management">Workflow Management</h4>
<p><strong>역할:</strong> 데이터 파이프라인과 작업 워크플로우를 오케스트레이션</p>
<p><strong>주요 기능:</strong></p>
<ul>
<li><strong>Jobs</strong>: 스케줄링된 작업 실행 및 관리</li>
<li><strong>Delta Live Tables (DLT)</strong>: 선언적 데이터 파이프라인 구축</li>
<li><strong>의존성 관리</strong>: 작업 간 의존성 및 실행 순서 관리</li>
<li><strong>모니터링</strong>: 작업 실행 상태 및 성능 모니터링</li>
</ul>
<p><strong>실무 관점:</strong></p>
<ul>
<li>Workflow Management는 작업을 스케줄링하고 모니터링하지만, 실제 실행은 Compute Plane에서 수행</li>
<li>작업 실패 시 자동 재시도 및 알림 기능 제공</li>
</ul>
<h4 id="intelligence-engine">Intelligence Engine</h4>
<p><strong>역할:</strong> 머신러닝 모델을 사용하여 플랫폼을 최적화하고 관리</p>
<p><strong>주요 기능:</strong></p>
<ul>
<li><strong>자동 최적화</strong>: 쿼리 성능 및 리소스 사용량 최적화</li>
<li><strong>예측 분석</strong>: 리소스 사용 패턴 예측 및 자동 스케일링</li>
<li><strong>비용 최적화</strong>: 비용 효율적인 리소스 할당 제안</li>
<li><strong>문제 감지</strong>: 성능 저하나 오류 패턴 자동 감지</li>
</ul>
<p><strong>실무 관점:</strong></p>
<ul>
<li>Intelligence Engine은 백그라운드에서 작동하여 사용자가 명시적으로 설정하지 않아도 자동으로 최적화 수행</li>
<li>시간이 지날수록 더 정확한 최적화 제안 제공</li>
</ul>
<hr>
<h3 id="2-compute-plane-컴퓨팅-플레인">2) Compute Plane (컴퓨팅 플레인)</h3>
<p>Compute Plane은 실제 데이터 처리가 일어나는 계층입니다. Databricks는 두 가지 컴퓨팅 모델을 제공합니다.</p>
<h4 id="classic-compute-클래식-컴퓨팅">Classic Compute (클래식 컴퓨팅)</h4>
<p><strong>특징:</strong></p>
<ul>
<li>고객의 클라우드 계정 내 가상 네트워크(VNet/VPC)에서 실행</li>
<li>고객이 직접 네트워크 및 보안 설정 관리</li>
<li>완전한 제어권과 커스터마이징 가능</li>
</ul>
<p><strong>주요 구성 요소:</strong></p>
<ul>
<li><strong>Workspace Clusters</strong>: 노트북 실행 및 작업 실행용 클러스터</li>
<li><strong>Classic SQL Warehouse</strong>: SQL 쿼리 실행용 전용 웨어하우스</li>
</ul>
<p><strong>장점:</strong></p>
<ul>
<li>네트워크 격리 및 보안 정책을 완전히 제어 가능</li>
<li>기존 클라우드 인프라와의 통합 용이</li>
<li>특정 규정 준수 요구사항 충족 가능</li>
</ul>
<p><strong>단점:</strong></p>
<ul>
<li>클러스터 시작 시간이 상대적으로 김 (수 분 소요)</li>
<li>유지보수 및 패치 관리를 고객이 담당해야 함</li>
<li>초기 설정 및 구성이 복잡할 수 있음</li>
</ul>
<p><strong>실무 활용:</strong></p>
<ul>
<li>엄격한 보안 요구사항이 있는 조직</li>
<li>기존 클라우드 네트워크와의 통합이 필요한 경우</li>
<li>장기 실행 작업이나 대용량 데이터 처리</li>
</ul>
<h4 id="serverless-compute-서버리스-컴퓨팅">Serverless Compute (서버리스 컴퓨팅)</h4>
<p><strong>특징:</strong></p>
<ul>
<li>Databricks 계정 내에서 실행</li>
<li>Databricks가 인프라 관리 및 유지보수 담당</li>
<li>빠른 시작 시간과 자동 스케일링</li>
</ul>
<p><strong>주요 구성 요소:</strong></p>
<ul>
<li><strong>Serverless SQL Warehouse</strong>: 서버리스 환경에서 실행되는 SQL 웨어하우스</li>
<li><strong>Model Serving</strong>: 실시간 ML 모델 서빙</li>
<li><strong>Vector Search</strong>: 벡터 검색 서비스</li>
<li><strong>Online Tables</strong>: 실시간 데이터 동기화</li>
</ul>
<p><strong>장점:</strong></p>
<ul>
<li><strong>빠른 시작</strong>: 클러스터 시작 시간이 수 초 내로 단축</li>
<li><strong>유지보수 부담 감소</strong>: Databricks가 패치 및 업데이트 관리</li>
<li><strong>자동 스케일링</strong>: 워크로드에 따라 자동으로 리소스 조정</li>
<li><strong>비용 효율성</strong>: 사용한 만큼만 비용 지불</li>
</ul>
<p><strong>단점:</strong></p>
<ul>
<li>네트워크 제어가 제한적 (Private Link로 일부 해결 가능)</li>
<li>특정 커스터마이징 제한</li>
<li>다중 테넌트 환경 (격리는 보장되지만)</li>
</ul>
<p><strong>실무 활용:</strong></p>
<ul>
<li>빠른 쿼리 응답이 필요한 BI 및 분석 작업</li>
<li>간헐적인 워크로드</li>
<li>유지보수 부담을 줄이고 싶은 경우</li>
<li>실시간 AI 애플리케이션</li>
</ul>
<hr>
<h1 id="3-서비스-모델별-아키텍처-특징">3. 서비스 모델별 아키텍처 특징</h1>
<h3 id="1-databricks-sql-classic-vs-serverless">1) Databricks SQL: Classic vs. Serverless</h3>
<h4 id="classic-sql-warehouse">Classic SQL Warehouse</h4>
<p><strong>아키텍처:</strong></p>
<pre><code>고객 클라우드 계정
  └─ VNet/VPC
      └─ 로드 밸런서
          └─ 컴퓨팅 클러스터 (고객 관리)
              └─ Cloud Storage 접근</code></pre><p><strong>특징:</strong></p>
<ul>
<li>고객의 클라우드 계정 내에서 실행</li>
<li>로드 밸런서 뒤에서 실행되는 컴퓨팅 클러스터 사용</li>
<li>네트워크 및 보안 설정을 고객이 완전히 제어</li>
<li>VNet/VPC 피어링을 통한 온프레미스 시스템과의 통합 가능</li>
</ul>
<p><strong>실무 관점:</strong></p>
<ul>
<li>기존 클라우드 네트워크와의 통합이 필요한 경우</li>
<li>특정 IP 대역이나 방화벽 규칙이 필요한 경우</li>
<li>장기 실행 쿼리나 대용량 데이터 처리</li>
</ul>
<h4 id="serverless-sql-warehouse">Serverless SQL Warehouse</h4>
<p><strong>아키텍처:</strong></p>
<pre><code>Databricks 계정
  └─ 다중 테넌트 인프라
      └─ VM 격리 (테넌트별)
          └─ 네트워크 격리 (테넌트별)
              └─ Cloud Storage 접근 (Private Link)</code></pre><p><strong>특징:</strong></p>
<ul>
<li>컴퓨팅 리소스가 Databricks 클라우드 계정에서 실행</li>
<li>다중 테넌트(Multi-tenant) 구조</li>
<li>각 테넌트 간 VM 및 네트워크 수준에서 엄격히 격리</li>
<li>Private Link를 통한 안전한 스토리지 접근</li>
</ul>
<p><strong>실무 관점:</strong></p>
<ul>
<li>빠른 쿼리 시작이 필요한 경우</li>
<li>유지보수 부담을 줄이고 싶은 경우</li>
<li>Azure 환경에서는 Private Link로 보안 연결 가능</li>
</ul>
<p><strong>보안 고려사항:</strong></p>
<ul>
<li>다중 테넌트 환경이지만 격리는 보장됨</li>
<li>Azure에서는 Private Link를 통해 스토리지와의 통신을 비공개로 유지 가능</li>
<li>AWS와 GCP에서도 유사한 프라이빗 연결 옵션 제공</li>
</ul>
<hr>
<h3 id="2-serverless-확장-서비스">2) Serverless 확장 서비스</h3>
<h4 id="model-serving--vector-search">Model Serving &amp; Vector Search</h4>
<p><strong>역할:</strong> 실시간 AI 애플리케이션을 위한 서버리스 인프라 제공</p>
<p><strong>Model Serving:</strong></p>
<ul>
<li>ML 모델을 REST API 엔드포인트로 노출</li>
<li>자동 스케일링 및 고가용성 보장</li>
<li>A/B 테스팅 및 모델 버전 관리 지원</li>
</ul>
<p><strong>Vector Search:</strong></p>
<ul>
<li>벡터 임베딩을 인덱싱하여 유사도 검색 제공</li>
<li>RAG(Retrieval-Augmented Generation) 애플리케이션 지원</li>
<li>실시간 업데이트 및 검색 가능</li>
</ul>
<p><strong>실무 활용:</strong></p>
<ul>
<li>챗봇 및 생성형 AI 애플리케이션</li>
<li>추천 시스템</li>
<li>이미지 및 텍스트 유사도 검색</li>
</ul>
<h4 id="online-tables">Online Tables</h4>
<p><strong>역할:</strong> Delta Table과 실시간으로 동기화되는 온라인 테이블 제공</p>
<p><strong>특징:</strong></p>
<ul>
<li>Delta Table의 변경사항을 실시간으로 동기화</li>
<li>낮은 지연 시간의 읽기 접근 제공</li>
<li>서버리스 인프라에서 자동 관리</li>
</ul>
<p><strong>실무 활용:</strong></p>
<ul>
<li>실시간 추천 시스템</li>
<li>실시간 대시보드</li>
<li>실시간 의사결정 애플리케이션</li>
</ul>
<hr>
<h1 id="4-보안-및-사용자-관리">4. 보안 및 사용자 관리</h1>
<h3 id="1-identity-provider-idp-통합">1) Identity Provider (IDP) 통합</h3>
<p><strong>역할:</strong> 고객의 중앙 사용자 관리 시스템과 Databricks를 통합</p>
<p><strong>지원하는 IDP:</strong></p>
<ul>
<li><strong>Azure AD / Microsoft Entra ID</strong>: Azure 환경에서 주로 사용</li>
<li><strong>Okta</strong>: 엔터프라이즈 SSO 솔루션</li>
<li><strong>Google Workspace</strong>: GCP 환경에서 주로 사용</li>
<li><strong>SAML 2.0 호환 IDP</strong>: 기타 SAML 2.0을 지원하는 모든 IDP</li>
</ul>
<p><strong>주요 기능:</strong></p>
<ul>
<li><strong>SSO (Single Sign-On)</strong>: 기업 인증 시스템을 통한 자동 로그인</li>
<li><strong>SCIM 프로비저닝</strong>: 사용자 및 그룹 자동 동기화</li>
<li><strong>역할 기반 접근 제어</strong>: IDP 그룹을 Databricks 그룹으로 매핑</li>
</ul>
<p><strong>실무 관점:</strong></p>
<ul>
<li>사용자 생명주기 관리를 IDP에서 중앙 집중식으로 관리</li>
<li>퇴사자나 역할 변경 시 자동으로 Databricks 접근 권한 업데이트</li>
<li>여러 워크스페이스에서 동일한 사용자 및 그룹 구조 공유 가능</li>
</ul>
<h3 id="2-network-security">2) Network Security</h3>
<h4 id="private-link-azure">Private Link (Azure)</h4>
<p><strong>역할:</strong> 서버리스 컴퓨팅 플레인에서 고객의 스토리지 계정으로의 보안 연결 제공</p>
<p><strong>작동 방식:</strong></p>
<ol>
<li>Databricks가 고객의 VNet에 Private Endpoint 생성</li>
<li>서버리스 컴퓨팅이 Private Endpoint를 통해 스토리지 접근</li>
<li>모든 트래픽이 Microsoft 백본 네트워크를 통해 전송</li>
<li>공용 인터넷을 거치지 않아 보안 강화</li>
</ol>
<p><strong>장점:</strong></p>
<ul>
<li>공용 인터넷을 거치지 않는 안전한 연결</li>
<li>네트워크 격리 및 방화벽 규칙 적용 가능</li>
<li>데이터 유출 위험 감소</li>
</ul>
<p><strong>실무 활용:</strong></p>
<ul>
<li>엄격한 보안 요구사항이 있는 조직</li>
<li>규정 준수 요구사항 충족</li>
<li>민감한 데이터 처리</li>
</ul>
<h4 id="vnetvpc-피어링-classic-compute">VNet/VPC 피어링 (Classic Compute)</h4>
<p><strong>역할:</strong> Classic Compute가 고객의 기존 네트워크와 직접 통신</p>
<p><strong>작동 방식:</strong></p>
<ol>
<li>Databricks VNet/VPC와 고객 VNet/VPC 간 피어링 설정</li>
<li>Classic Compute가 피어링된 네트워크를 통해 리소스 접근</li>
<li>온프레미스 시스템과의 VPN/ExpressRoute 연결 가능</li>
</ol>
<p><strong>실무 활용:</strong></p>
<ul>
<li>온프레미스 데이터베이스 접근</li>
<li>기존 클라우드 리소스와의 통합</li>
<li>하이브리드 클라우드 아키텍처</li>
</ul>
<hr>
<h1 id="5-실무-활용-가이드">5. 실무 활용 가이드</h1>
<h3 id="1-시나리오-1-엄격한-보안-요구사항이-있는-금융-기관">1) 시나리오 1: 엄격한 보안 요구사항이 있는 금융 기관</h3>
<p><strong>요구사항:</strong></p>
<ul>
<li>모든 데이터가 고객 계정 내에만 존재</li>
<li>네트워크 격리 및 방화벽 규칙 적용</li>
<li>온프레미스 시스템과의 통합 필요</li>
</ul>
<p><strong>아키텍처 선택:</strong></p>
<ul>
<li><strong>Classic Compute</strong> 사용</li>
<li>VNet/VPC 피어링을 통한 온프레미스 연결</li>
<li>Private Link는 사용하지 않음 (모든 리소스가 고객 계정 내)</li>
</ul>
<p><strong>구성:</strong></p>
<pre><code>고객 계정 (Azure)
  ├─ VNet (피어링됨)
  │   ├─ Classic SQL Warehouse
  │   └─ Workspace Clusters
  ├─ Storage Account (Private Endpoint)
  └─ 온프레미스 연결 (ExpressRoute)</code></pre><h3 id="2-시나리오-2-빠른-프로토타이핑이-필요한-스타트업">2) 시나리오 2: 빠른 프로토타이핑이 필요한 스타트업</h3>
<p><strong>요구사항:</strong></p>
<ul>
<li>빠른 시작 및 유지보수 최소화</li>
<li>비용 효율성</li>
<li>실시간 AI 기능 필요</li>
</ul>
<p><strong>아키텍처 선택:</strong></p>
<ul>
<li><strong>Serverless Compute</strong> 우선 사용</li>
<li>Model Serving 및 Vector Search 활용</li>
<li>Classic Compute는 대용량 배치 작업에만 사용</li>
</ul>
<p><strong>구성:</strong></p>
<pre><code>Databricks 계정
  ├─ Serverless SQL Warehouse (일반 쿼리)
  ├─ Model Serving (실시간 추론)
  └─ Vector Search (RAG 애플리케이션)

고객 계정
  └─ Cloud Storage (Delta Tables)</code></pre><h3 id="3-시나리오-3-하이브리드-워크로드가-있는-대기업">3) 시나리오 3: 하이브리드 워크로드가 있는 대기업</h3>
<p><strong>요구사항:</strong></p>
<ul>
<li>다양한 워크로드 지원 (배치, 스트리밍, 실시간)</li>
<li>보안과 성능의 균형</li>
<li>비용 최적화</li>
</ul>
<p><strong>아키텍처 선택:</strong></p>
<ul>
<li><strong>Classic Compute</strong>: 장기 실행 배치 작업, 엄격한 보안 요구사항</li>
<li><strong>Serverless Compute</strong>: 빠른 쿼리, 실시간 AI, 간헐적 워크로드</li>
<li><strong>Private Link</strong>: Serverless에서 스토리지 접근 시 사용</li>
</ul>
<p><strong>구성:</strong></p>
<pre><code>Databricks 계정
  ├─ Serverless SQL Warehouse (BI 쿼리)
  ├─ Model Serving (실시간 AI)
  └─ Private Link (스토리지 접근)

고객 계정
  ├─ Classic SQL Warehouse (대용량 배치)
  ├─ Workspace Clusters (데이터 엔지니어링)
  └─ Cloud Storage (Delta Tables)</code></pre><hr>
<h1 id="6-아키텍처-선택-가이드">6. 아키텍처 선택 가이드</h1>
<h3 id="1-classic-vs-serverless-비교표">1) Classic vs. Serverless 비교표</h3>
<table>
<thead>
<tr>
<th>기준</th>
<th>Classic Compute</th>
<th>Serverless Compute</th>
</tr>
</thead>
<tbody><tr>
<td><strong>시작 시간</strong></td>
<td>수 분</td>
<td>수 초</td>
</tr>
<tr>
<td><strong>유지보수</strong></td>
<td>고객 담당</td>
<td>Databricks 담당</td>
</tr>
<tr>
<td><strong>네트워크 제어</strong></td>
<td>완전한 제어</td>
<td>제한적 (Private Link로 보완)</td>
</tr>
<tr>
<td><strong>비용 모델</strong></td>
<td>예약 인스턴스 가능</td>
<td>사용한 만큼 지불</td>
</tr>
<tr>
<td><strong>커스터마이징</strong></td>
<td>높음</td>
<td>제한적</td>
</tr>
<tr>
<td><strong>보안</strong></td>
<td>완전한 격리</td>
<td>다중 테넌트 (격리 보장)</td>
</tr>
<tr>
<td><strong>온프레미스 통합</strong></td>
<td>VNet 피어링 가능</td>
<td>제한적</td>
</tr>
<tr>
<td><strong>적합한 워크로드</strong></td>
<td>장기 실행, 대용량</td>
<td>간헐적, 빠른 응답 필요</td>
</tr>
</tbody></table>
<h3 id="2-선택-기준">2) 선택 기준</h3>
<p><strong>Classic Compute를 선택해야 하는 경우:</strong></p>
<ul>
<li>엄격한 네트워크 격리 요구사항</li>
<li>온프레미스 시스템과의 직접 통합 필요</li>
<li>장기 실행 작업이나 대용량 데이터 처리</li>
<li>특정 규정 준수 요구사항 (예: 데이터가 특정 지역에만 존재해야 함)</li>
</ul>
<p><strong>Serverless Compute를 선택해야 하는 경우:</strong></p>
<ul>
<li>빠른 시작 시간이 중요</li>
<li>유지보수 부담을 줄이고 싶음</li>
<li>간헐적인 워크로드</li>
<li>실시간 AI 애플리케이션</li>
<li>비용 효율적인 리소스 사용</li>
</ul>
<p><strong>하이브리드 접근:</strong></p>
<ul>
<li>대부분의 조직은 두 방식을 조합하여 사용</li>
<li>워크로드 특성에 따라 적절한 Compute 모델 선택</li>
<li>Classic은 배치 및 데이터 엔지니어링, Serverless는 분석 및 실시간 작업</li>
</ul>
<hr>
<h1 id="7-마무리">7. 마무리</h1>
<p>Databricks의 데이터 인텔리전스 플랫폼 아키텍처는 Databricks 계정과 고객 계정으로 분리된 하이브리드 구조를 통해 보안, 성능, 유연성을 모두 제공합니다.</p>
<p><strong>핵심 요약:</strong></p>
<ol>
<li><strong>두 계정 구조</strong>: Control Plane과 Serverless Compute는 Databricks 계정에, 데이터와 Classic Compute는 고객 계정에 위치</li>
<li><strong>Compute 선택</strong>: Classic은 완전한 제어와 격리, Serverless는 빠른 시작과 유지보수 편의성 제공</li>
<li><strong>보안 계층</strong>: IDP 통합, 네트워크 격리, 세밀한 접근 제어를 통해 다층 보안 구현</li>
<li><strong>유연한 구성</strong>: 워크로드 특성에 따라 Classic과 Serverless를 조합하여 사용</li>
</ol>
<p>아키텍처를 올바르게 이해하고 구성하면, 보안을 유지하면서도 성능과 비용을 최적화할 수 있습니다. 처음 구축할 때는 작은 규모로 시작하여 점진적으로 확장하는 것이 좋습니다.</p>
<hr>
<p><strong>참고 자료:</strong></p>
<ul>
<li><a href="https://docs.databricks.com/getting-started/overview.html">Databricks 공식 문서: Architecture Overview</a></li>
<li><a href="https://docs.databricks.com/compute/serverless-compute/index.html">Databricks 공식 문서: Serverless Compute</a></li>
<li><a href="https://docs.databricks.com/security/network/index.html">Databricks 공식 문서: Network Security</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Databricks 사용자 역할 완전 가이드: 누가 무엇을 관리하는가?]]></title>
            <link>https://velog.io/@newnew_daddy/DATABRICKS02</link>
            <guid>https://velog.io/@newnew_daddy/DATABRICKS02</guid>
            <pubDate>Wed, 21 Jan 2026 02:30:40 GMT</pubDate>
            <description><![CDATA[<h1 id="0-intro">0. INTRO</h1>
<h3 id="왜-databricks-관리자-역할을-이해해야-할까">왜 Databricks 관리자 역할을 이해해야 할까?</h3>
<p>Databricks를 처음 도입하거나 운영하는 조직에서 가장 자주 마주치는 질문 중 하나는 &quot;누가 어떤 권한을 가져야 하는가?&quot;입니다. Databricks는 엔터프라이즈급 데이터 플랫폼으로, 다양한 관리자 역할을 제공하여 조직의 보안과 운영 효율성을 보장합니다.</p>
<p>각 역할의 책임 범위를 명확히 이해하지 못하면, 불필요하게 높은 권한을 부여하거나 반대로 필요한 권한이 부족하여 업무가 지연되는 문제가 발생할 수 있습니다. 이 글에서는 Databricks의 주요 관리자 역할들을 계층별로 정리하고, 각 역할이 담당하는 영역과 실무에서의 활용 방법을 설명합니다.</p>
<hr>
<h3 id="databricks-관리자-역할의-계층-구조">Databricks 관리자 역할의 계층 구조</h3>
<p>Databricks의 관리자 역할은 크게 네 가지 계층으로 구분할 수 있습니다:</p>
<ol>
<li><strong>계정 및 인프라 관리 수준</strong>: 클라우드 자원과 계정 전체를 관리</li>
<li><strong>워크스페이스 및 데이터 거버넌스 수준</strong>: 특정 워크스페이스와 데이터 카탈로그 관리</li>
<li><strong>특정 기능 관리 수준</strong>: 결제, 마켓플레이스 등 특화된 기능 관리</li>
<li><strong>소유권 개념</strong>: 개별 데이터 객체의 소유자</li>
</ol>
<hr>
<h1 id="1-계정-및-인프라-관리-수준">1. 계정 및 인프라 관리 수준</h1>
<h3 id="1-cloud-administrator-클라우드-관리자">1) Cloud Administrator (클라우드 관리자)</h3>
<p><strong>주요 책임:</strong></p>
<ul>
<li>스토리지 계정(버킷) 및 클라우드 네이티브 자원 관리</li>
<li>IAM 역할 및 서비스 주체(Service Principal) 설정</li>
<li>클라우드 서비스와 Databricks 간의 통합 구성</li>
</ul>
<p><strong>실무 관점:</strong>
Cloud Administrator는 Databricks가 아닌 클라우드 플랫폼(AWS, Azure, GCP) 레벨에서 작업합니다. 예를 들어, Databricks가 S3 버킷에 접근하기 위한 IAM 역할을 생성하거나, Azure Storage Account에 대한 접근 권한을 설정하는 것이 이 역할의 주요 업무입니다.</p>
<p><strong>언제 필요한가?</strong></p>
<ul>
<li>Databricks 워크스페이스를 처음 생성할 때</li>
<li>외부 스토리지(데이터 레이크)와 Databricks를 연결할 때</li>
<li>클라우드 네이티브 서비스(예: AWS Glue, Azure Data Factory)와 통합할 때</li>
</ul>
<hr>
<h3 id="2-identity-administrator-id-관리자">2) Identity Administrator (ID 관리자)</h3>
<p><strong>주요 책임:</strong></p>
<ul>
<li>기업의 ID 공급자(IdP)를 Databricks와 통합</li>
<li>사용자 및 그룹을 계정에 자동 프로비저닝</li>
<li>SSO(Single Sign-On) 설정 및 관리</li>
</ul>
<p><strong>실무 관점:</strong>
대규모 조직에서는 수백 명의 사용자를 수동으로 관리하기 어렵습니다. Identity Administrator는 SAML 또는 SCIM 프로토콜을 통해 기업의 Active Directory나 Okta 같은 IdP와 Databricks를 연동하여, 사용자 추가/삭제/권한 변경을 자동화합니다.</p>
<p><strong>언제 필요한가?</strong></p>
<ul>
<li>10명 이상의 사용자가 있는 조직</li>
<li>기존 기업 인증 시스템과 통합이 필요한 경우</li>
<li>사용자 생명주기 관리를 자동화하고 싶을 때</li>
</ul>
<hr>
<h3 id="3-account-administrator-계정-관리자">3) Account Administrator (계정 관리자)</h3>
<p><strong>주요 책임:</strong></p>
<ul>
<li>메타스토어(Metastore) 생성 및 관리</li>
<li>워크스페이스 생성 및 삭제</li>
<li>계정 레벨의 사용자 및 그룹 구조 관리</li>
<li>스토리지 자격 증명(Storage Credentials) 생성</li>
<li>다른 사용자에게 계정 관리자 권한 부여</li>
</ul>
<p><strong>실무 관점:</strong>
Account Administrator는 Databricks 계정의 최상위 관리자입니다. 이 역할은 조직 내에서 매우 제한적으로 부여되어야 하며, 보통 데이터 플랫폼 팀의 리더나 IT 관리자가 담당합니다. 모든 데이터 객체를 관리할 수 있는 권한을 가지므로, 신중하게 권한을 부여해야 합니다.</p>
<p><strong>언제 필요한가?</strong></p>
<ul>
<li>새로운 워크스페이스를 생성해야 할 때</li>
<li>Unity Catalog의 메타스토어를 설정할 때</li>
<li>계정 전체의 사용자 구조를 재구성할 때</li>
</ul>
<hr>
<h1 id="2-워크스페이스-및-데이터-거버넌스-수준">2. 워크스페이스 및 데이터 거버넌스 수준</h1>
<h3 id="1-workspace-administrator-워크스페이스-관리자">1) Workspace Administrator (워크스페이스 관리자)</h3>
<p><strong>주요 책임:</strong></p>
<ul>
<li>특정 워크스페이스 내의 자산 관리 (노트북, Repo, 클러스터, 작업 등)</li>
<li>워크스페이스 레벨의 사용자 추가/제거</li>
<li>클러스터 생성 정책 및 인스턴스 프로파일 설정</li>
<li>워크스페이스 설정 및 구성 관리</li>
</ul>
<p><strong>실무 관점:</strong>
Workspace Administrator는 특정 워크스페이스의 &quot;관리자&quot;입니다. 예를 들어, &quot;개발 워크스페이스&quot;와 &quot;프로덕션 워크스페이스&quot;가 있다면, 각각 별도의 Workspace Administrator를 둘 수 있습니다. 이 역할은 개발자들이 필요한 리소스를 사용할 수 있도록 환경을 구성하고, 비용 관리를 위한 클러스터 정책을 설정합니다.</p>
<p><strong>언제 필요한가?</strong></p>
<ul>
<li>워크스페이스 내에서 사용자 권한을 세밀하게 관리해야 할 때</li>
<li>클러스터 자동 종료 정책이나 인스턴스 타입 제한을 설정할 때</li>
<li>워크스페이스별로 독립적인 개발 환경을 운영할 때</li>
</ul>
<hr>
<h3 id="2-metastore-administrator-메타스토어-관리자">2) Metastore Administrator (메타스토어 관리자)</h3>
<p><strong>주요 책임:</strong></p>
<ul>
<li>Unity Catalog의 카탈로그(Catalog) 생성 및 관리</li>
<li>외부 위치(External Locations) 생성 및 관리</li>
<li>데이터 객체(테이블, 뷰 등)에 대한 권한 부여</li>
<li>데이터 객체의 소유권 변경</li>
<li>데이터 거버넌스 정책 수립 및 관리</li>
</ul>
<p><strong>실무 관점:</strong>
Metastore Administrator는 데이터 거버넌스의 핵심 역할입니다. Unity Catalog를 통해 데이터 레이크의 모든 데이터에 대한 접근 제어를 관리합니다. 예를 들어, &quot;마케팅 팀은 sales 데이터베이스의 특정 테이블만 읽을 수 있다&quot;와 같은 정책을 설정하고 관리합니다.</p>
<p><strong>언제 필요한가?</strong></p>
<ul>
<li>Unity Catalog를 사용하여 데이터 거버넌스를 구현할 때</li>
<li>여러 워크스페이스에서 공유하는 데이터 카탈로그를 관리할 때</li>
<li>데이터 접근 권한을 세밀하게 제어해야 할 때</li>
</ul>
<hr>
<h1 id="3-특정-기능-관리-수준">3. 특정 기능 관리 수준</h1>
<h3 id="1-marketplace-administrator-마켓플레이스-관리자">1) Marketplace Administrator (마켓플레이스 관리자)</h3>
<p><strong>주요 책임:</strong></p>
<ul>
<li>Databricks Marketplace의 공급자 프로필 생성 및 관리</li>
<li>데이터 제품 리스팅 생성 및 관리</li>
<li>데이터 공유를 위한 Share 생성 및 관리</li>
<li>공급자 콘솔(Provider Console) 접근 및 운영</li>
</ul>
<p><strong>실무 관점:</strong>
Marketplace Administrator는 조직이 Databricks Marketplace를 통해 데이터를 공유하거나 판매할 때 필요한 역할입니다. 데이터 제품을 마켓플레이스에 등록하고, 다른 조직과 데이터를 공유하는 비즈니스를 관리합니다.</p>
<p><strong>언제 필요한가?</strong></p>
<ul>
<li>조직의 데이터를 외부에 공유하거나 판매할 때</li>
<li>Databricks Marketplace를 활용한 데이터 비즈니스를 운영할 때</li>
</ul>
<hr>
<h3 id="2-billing-administrator-결제-관리자">2) Billing Administrator (결제 관리자)</h3>
<p><strong>주요 책임:</strong></p>
<ul>
<li>예산 조회 및 예산 정책 관리</li>
<li>구독 및 결제 수단(신용카드 등) 관리</li>
<li>사용량 대시보드 모니터링</li>
<li>예산 알림 설정 및 비용 최적화</li>
</ul>
<p><strong>실무 관점:</strong>
Billing Administrator는 Databricks 사용 비용을 관리하는 역할입니다. 각 워크스페이스나 사용자 그룹별로 예산을 설정하고, 예산 초과 시 알림을 받아 비용을 제어합니다. 재무팀이나 IT 관리팀의 담당자가 주로 이 역할을 수행합니다.</p>
<p><strong>언제 필요한가?</strong></p>
<ul>
<li>Databricks 사용 비용을 모니터링하고 제어해야 할 때</li>
<li>팀별 또는 프로젝트별 예산을 관리할 때</li>
<li>비용 최적화를 위한 정책을 수립할 때</li>
</ul>
<hr>
<h1 id="4-소유권-개념-owner-소유자">4. 소유권 개념: Owner (소유자)</h1>
<p><strong>정의:</strong>
메타스토어 내의 각 데이터 객체(테이블, 뷰, 함수 등)를 생성한 주체이거나, 소유권을 이전받은 주체입니다.</p>
<p><strong>권한:</strong></p>
<ul>
<li>명시적인 권한 없이도 자신이 소유한 객체를 읽고 수정할 수 있음</li>
<li>타인에게 권한을 부여하거나 회수할 수 있음</li>
<li>하위 객체(예: 테이블의 컬럼)를 생성할 수 있음</li>
<li>객체의 소유권을 다른 사용자에게 이전할 수 있음</li>
</ul>
<p><strong>실무 관점:</strong>
Owner는 역할(Role)이 아니라 객체별로 부여되는 개념입니다. 예를 들어, 데이터 엔지니어가 새로운 테이블을 생성하면 자동으로 그 테이블의 Owner가 됩니다. Owner는 해당 객체에 대한 완전한 제어권을 가지므로, 퇴사자나 역할 변경 시 소유권 이전을 고려해야 합니다.</p>
<hr>
<h1 id="5-역할별-관리-대상-요약">5. 역할별 관리 대상 요약</h1>
<table>
<thead>
<tr>
<th>구분</th>
<th>역할명</th>
<th>주요 관리 대상</th>
<th>권한 범위</th>
</tr>
</thead>
<tbody><tr>
<td><strong>인프라</strong></td>
<td>Cloud Admin</td>
<td>클라우드 자원 (S3, IAM 등)</td>
<td>클라우드 플랫폼 레벨</td>
</tr>
<tr>
<td><strong>계정</strong></td>
<td>Account Admin</td>
<td>워크스페이스, 메타스토어, 계정 전체</td>
<td>계정 전체</td>
</tr>
<tr>
<td><strong>보안</strong></td>
<td>Identity Admin</td>
<td>사용자 ID 연동 및 프로비저닝</td>
<td>계정 전체</td>
</tr>
<tr>
<td><strong>데이터</strong></td>
<td>Metastore Admin</td>
<td>카탈로그, 권한 체계, 데이터 거버넌스</td>
<td>메타스토어 범위</td>
</tr>
<tr>
<td><strong>실행</strong></td>
<td>Workspace Admin</td>
<td>워크스페이스 내 자산 (클러스터, 노트북)</td>
<td>워크스페이스 범위</td>
</tr>
<tr>
<td><strong>비용</strong></td>
<td>Billing Admin</td>
<td>예산, 결제, 사용량 모니터링</td>
<td>계정 전체</td>
</tr>
<tr>
<td><strong>공유</strong></td>
<td>Marketplace Admin</td>
<td>데이터 공유 및 마켓플레이스 관리</td>
<td>계정 전체</td>
</tr>
<tr>
<td><strong>객체</strong></td>
<td>Owner</td>
<td>개별 데이터 객체 (테이블, 뷰 등)</td>
<td>객체별</td>
</tr>
</tbody></table>
<hr>
<h1 id="6-실무-활용-가이드">6. 실무 활용 가이드</h1>
<h3 id="신규-프로젝트-시작-시-권한-구성-예시">신규 프로젝트 시작 시 권한 구성 예시</h3>
<p><strong>시나리오: 중소규모 조직의 Databricks 도입</strong></p>
<ol>
<li><p><strong>초기 설정 단계</strong></p>
<ul>
<li>Cloud Administrator: 클라우드 자원 설정 (1-2명)</li>
<li>Account Administrator: 계정 및 첫 워크스페이스 생성 (1명)</li>
</ul>
</li>
<li><p><strong>운영 단계</strong></p>
<ul>
<li>Identity Administrator: 사용자 자동 프로비저닝 설정 (1명)</li>
<li>Workspace Administrator: 각 워크스페이스별 관리자 (워크스페이스당 1-2명)</li>
<li>Metastore Administrator: 데이터 거버넌스 담당 (1-2명)</li>
<li>Billing Administrator: 비용 관리 담당 (1명)</li>
</ul>
</li>
<li><p><strong>일반 사용자</strong></p>
<ul>
<li>데이터 엔지니어: 테이블 생성 시 자동으로 Owner 권한 획득</li>
<li>데이터 분석가: Metastore Admin이 부여한 읽기 권한으로 데이터 접근</li>
</ul>
</li>
</ol>
<h3 id="권한-부여-시-주의사항">권한 부여 시 주의사항</h3>
<ol>
<li><strong>최소 권한 원칙</strong>: 필요한 최소한의 권한만 부여</li>
<li><strong>역할 분리</strong>: Account Admin과 Workspace Admin을 분리하여 권한 집중 방지</li>
<li><strong>정기 검토</strong>: 분기별로 권한 목록을 검토하고 불필요한 권한 회수</li>
<li><strong>소유권 관리</strong>: 퇴사자나 역할 변경 시 데이터 객체의 소유권 이전</li>
</ol>
<hr>
<h1 id="7-마무리">7. 마무리</h1>
<p>Databricks의 관리자 역할 체계는 조직의 규모와 요구사항에 따라 유연하게 구성할 수 있습니다. 각 역할의 책임 범위를 명확히 이해하고, 조직의 구조에 맞게 권한을 부여하는 것이 안전하고 효율적인 Databricks 운영의 핵심입니다.</p>
<p>특히 계정 관리자(Account Admin)와 워크스페이스 관리자(Workspace Admin)의 차이, 그리고 메타스토어 관리자(Metastore Admin)의 데이터 거버넌스 역할을 이해하는 것이 중요합니다. 이러한 역할들을 적절히 조합하면, 보안을 유지하면서도 개발자들이 필요한 리소스에 자유롭게 접근할 수 있는 환경을 구축할 수 있습니다.</p>
<hr>
<p><strong>참고 자료:</strong></p>
<ul>
<li><a href="https://docs.databricks.com/administration-guide/users-groups/index.html">Databricks 공식 문서: Account Admin</a></li>
<li><a href="https://docs.databricks.com/data-governance/unity-catalog/index.html">Databricks 공식 문서: Unity Catalog 관리</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Airflow XCom으로 데이터를 주고받는 5가지 방법]]></title>
            <link>https://velog.io/@newnew_daddy/AIRFLOW05</link>
            <guid>https://velog.io/@newnew_daddy/AIRFLOW05</guid>
            <pubDate>Thu, 15 Jan 2026 13:51:47 GMT</pubDate>
            <description><![CDATA[<h1 id="0-intro">0. INTRO</h1>
<p>모든 워크플로우 자동화 도구가 그렇듯, Apache Airflow에서도 테스크(Task) 간에 데이터를 주고받아야 하는 상황이 빈번하게 발생합니다. Airflow에서는 이를 위해 XCom(Cross-Communication)이라는 메커니즘을 제공합니다.</p>
<h2 id="xcom이란">XCom이란?</h2>
<p>XCom은 Airflow의 메타데이터베이스에 저장되는 작은 데이터 조각으로, 태스크 간에 데이터를 공유할 수 있게 해줍니다. 예를 들어, 한 태스크에서 생성한 파일 경로, 처리된 데이터의 요약 정보, 또는 다음 태스크에 필요한 설정값 등을 전달할 수 있습니다.</p>
<h2 id="xcom의-크기-제한">XCom의 크기 제한</h2>
<p>XCom을 사용할 때 주의해야 할 중요한 점은 <strong>데이터 크기 제한</strong>입니다. 표준 XCom 백엔드를 사용할 경우, XCom의 크기 제한은 사용 중인 메타데이터 데이터베이스에 따라 결정됩니다:</p>
<table>
<thead>
<tr>
<th align="left">데이터베이스</th>
<th align="left">크기 제한</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>PostgreSQL</strong></td>
<td align="left">1 GB</td>
</tr>
<tr>
<td align="left"><strong>SQLite</strong></td>
<td align="left">2 GB</td>
</tr>
<tr>
<td align="left"><strong>MySQL</strong></td>
<td align="left">64 KB</td>
</tr>
</tbody></table>
<p>보시다시피, 특히 MySQL의 경우 64KB라는 매우 작은 제한이 있어 큰 데이터를 전달하기에는 부적합합니다. 만약 XCom을 통해 전달하려는 데이터가 메타데이터 데이터베이스의 크기 제한을 초과할 가능성이 있다면, <strong>Custom XCom 저장소</strong>를 구축하여 활용하는 것을 고려해야 합니다.</p>
<hr>
<h1 id="1-context-파라미터-활용하기">1. <code>**context</code> 파라미터 활용하기</h1>
<p>전통적인 <code>PythonOperator</code> 방식에서 호출되는 함수에 <code>**context</code>를 인자로 받아 <code>ti</code>(Task Instance) 객체에 접근하는 방법입니다.</p>
<pre><code class="language-python">def push_to_xcom(**context):
    message = &quot;사과&quot;
    ti = context[&quot;ti&quot;]

    ti.xcom_push(
        key=&#39;message&#39;,
        value=message
    )
    return message # &#39;return_value&#39;라는 키로도 자동 저장됨 

def pull_from_xcom(**context):
    ti = context[&quot;ti&quot;]
    xcom_value = ti.xcom_pull(
        task_ids=&#39;py1&#39;,
        key=&#39;message&#39;
    )
    print(&quot;py1에서 전달받은 결과 : &quot;, xcom_value)</code></pre>
<ul>
<li><strong>장점</strong>: Airflow의 모든 컨텍스트 정보에 명시적으로 접근할 수 있습니다.</li>
<li><strong>특징</strong>: <code>ti.xcom_push</code>를 사용할 때 고유한 <code>key</code>를 지정할 수 있습니다.</li>
</ul>
<h3 id="💡-잠깐-context에는-무엇이-들어있을까요">💡 잠깐! <code>**context</code>에는 무엇이 들어있을까요?</h3>
<p>많은 분들이 <code>**context</code>를 프린트했을 때 쏟아지는 방대한 양의 메타데이터에 당황하곤 합니다. 이 데이터들은 현재 실행 중인 <strong>DAG과 태스크의 &#39;상태 정보&#39;</strong>입니다.</p>
<p>주요 항목들을 표로 정리하면 다음과 같습니다:</p>
<table>
<thead>
<tr>
<th align="left">키(Key)</th>
<th align="left">설명</th>
<th align="left">예시</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>ds</code></td>
<td align="left">태스크가 실행되는 논리적 날짜 (Date String)</td>
<td align="left"><code>&#39;2024-01-14&#39;</code></td>
</tr>
<tr>
<td align="left"><code>ds_nodash</code></td>
<td align="left">대시(<code>-</code>)가 제거된 날짜 문자열</td>
<td align="left"><code>&#39;20240114&#39;</code></td>
</tr>
<tr>
<td align="left"><code>ti</code> / <code>task_instance</code></td>
<td align="left">현재 실행 중인 태스크 인스턴스 객체</td>
<td align="left"><code>&lt;TaskInstance: ...&gt;</code></td>
</tr>
<tr>
<td align="left"><code>dag</code></td>
<td align="left">현재 태스크가 속한 DAG 객체</td>
<td align="left"><code>&lt;DAG: ...&gt;</code></td>
</tr>
<tr>
<td align="left"><code>logical_date</code></td>
<td align="left">태스크의 논리적 실행 시점 (Pendulum 객체)</td>
<td align="left"><code>2024-01-14T00:00:00+00:00</code></td>
</tr>
<tr>
<td align="left"><code>run_id</code></td>
<td align="left">현재 DAG Run의 고유 식별자</td>
<td align="left"><code>&#39;scheduled__2024-01-14T00:00:00+00:00&#39;</code></td>
</tr>
</tbody></table>
<h3 id="printcontext-실행-시-실제-출력-예시"><code>print(context)</code> 실행 시 실제 출력 예시</h3>
<pre><code class="language-python">{
    &#39;ds&#39;: &#39;2024-01-14&#39;,
    &#39;ds_nodash&#39;: &#39;20240114&#39;,
    &#39;logical_date&#39;: DateTime(2024, 1, 14, 0, 0, 0, tzinfo=Timezone(&#39;UTC&#39;)),
    &#39;dag&#39;: &lt;DAG: xcom01_dag&gt;,
    &#39;ti&#39;: &lt;TaskInstance: xcom01_dag.py1 [running]&gt;,
    &#39;run_id&#39;: &#39;scheduled__2024-01-14T00:00:00+00:00&#39;,
    &#39;params&#39;: {},
    ... (중략) ...
}</code></pre>
<p>따라서 <code>**context</code>를 사용한다는 것은, <strong>&quot;Airflow가 태스크를 실행하면서 들고 있는 모든 보따리(메타데이터)를 다 넘겨줘!&quot;</strong>라고 요청하는 것과 같습니다.</p>
<hr>
<h1 id="2-get_current_context-사용하기">2. <code>get_current_context()</code> 사용하기</h1>
<p>함수의 인자로 <code>**context</code>를 넘기지 않더라도, <code>get_current_context()</code>를 통해 현재 실행 중인 태스크의 컨텍스트를 동적으로 가져올 수 있습니다.</p>
<pre><code class="language-python">from airflow.sdk import get_current_context

def push_to_xcom():
    message = &quot;사과&quot;
    context = get_current_context()
    ti = context[&#39;ti&#39;]

    ti.xcom_push(key=&#39;message&#39;, value=message)
    return message

def pull_from_xcom():
    context = get_current_context()
    ti = context[&#39;ti&#39;]

    xcom_value = ti.xcom_pull(task_ids=&#39;py1&#39;, key=&#39;message&#39;)
    print(&quot;py1에서 전달받은 결과 : &quot;, xcom_value)</code></pre>
<ul>
<li><strong>장점</strong>: 함수의 시그니처를 깔끔하게 유지할 수 있습니다.</li>
</ul>
<hr>
<h1 id="3-taskflow-api에서-return-사용하기-현대적인-방식">3. TaskFlow API에서 <code>return</code> 사용하기 (현대적인 방식)</h1>
<p>Airflow 2.0 이상에서 권장되는 TaskFlow API를 사용하면, 복잡한 <code>xcom_push/pull</code> 코드 없이 함수의 <code>return</code> 값만으로 데이터를 전달할 수 있습니다.</p>
<pre><code class="language-python">@task(task_id=&quot;first&quot;)
def first_func(args):
    join_list = &#39; &#39;.join(args)
    return join_list # 자동으로 XCom에 push됨

@task(task_id=&#39;second&#39;)
def second_func(message):
    # 인자로 넘겨받은 message는 이전 태스크의 return 값 (XCom pull)
    changed_list = &#39;!&#39; + message + &#39;!&#39;
    return changed_list

# DAG 내에서 호출
message = first_func([&#39;FLOWER&#39;, &#39;AIRFLOW&#39;])
second_func(message)</code></pre>
<ul>
<li><strong>장점</strong>: Python 함수를 호출하듯 직관적으로 태스크 간 데이터 흐름을 정의할 수 있습니다.</li>
</ul>
<hr>
<h1 id="4-taskflow-api-옵션-활용-multiple_outputs">4. TaskFlow API 옵션 활용 (<code>multiple_outputs</code>)</h1>
<p>태스크가 여러 개의 결과값을 딕셔너리 형태로 반환할 때, <code>multiple_outputs=True</code> 옵션을 주면 각각의 키 값이 개별 XCom 항목으로 저장됩니다.</p>
<pre><code class="language-python">@task(task_id=&quot;first&quot;, do_xcom_push=True, multiple_outputs=True)
def first_func(args):
    join_list = &#39; &#39;.join(args)
    return {&quot;key1&quot;: join_list} # &#39;key1&#39;이라는 이름으로 XCom에 저장됨</code></pre>
<ul>
<li><strong>특징</strong>: 반환된 딕셔너리의 키를 통해 특정 데이터만 Pull 할 수 있어 관리가 용이합니다.</li>
</ul>
<hr>
<h1 id="5-다른-operator에서-jinja-템플릿-활용하기">5. 다른 Operator에서 Jinja 템플릿 활용하기</h1>
<p>Python이 아닌 다른 오퍼레이터(예: <code>BashOperator</code>)에서 XCom 데이터를 사용하고 싶을 때는 Jinja 템플릿 형식을 사용합니다.</p>
<pre><code class="language-python">t1 = PythonOperator(
    task_id=&quot;make_dirname&quot;,
    python_callable=make_dirname, # 내부에서 ti.xcom_push(key=&quot;dir_path&quot;, ...) 수행
)

t2 = BashOperator(
    task_id=&quot;make_dir&quot;,
    bash_command=&quot;mkdir -p {{ ti.xcom_pull(task_ids=&#39;make_dirname&#39;, key=&#39;dir_path&#39;) }}&quot;
)</code></pre>
<ul>
<li><strong>장점</strong>: 서로 다른 언어나 환경을 사용하는 오퍼레이터 간의 협업이 가능해집니다.</li>
</ul>
<hr>
<h1 id="6-요약">6. 요약</h1>
<table>
<thead>
<tr>
<th align="left">방식</th>
<th align="left">특징</th>
<th align="left">추천 상황</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>`</strong>context`**</td>
<td align="left">명시적 객체 전달</td>
<td align="left">복잡한 컨텍스트 제어가 필요할 때</td>
</tr>
<tr>
<td align="left"><strong><code>get_current_context</code></strong></td>
<td align="left">깔끔한 함수 시그니처</td>
<td align="left">함수 인자를 단순하게 유지하고 싶을 때</td>
</tr>
<tr>
<td align="left"><strong>TaskFlow <code>return</code></strong></td>
<td align="left">가장 간결하고 직관적</td>
<td align="left">일반적인 Python 기반 태스크 연결 시</td>
</tr>
<tr>
<td align="left"><strong><code>multiple_outputs</code></strong></td>
<td align="left">딕셔너리 자동 분리</td>
<td align="left">여러 결과값을 개별적으로 전달할 때</td>
</tr>
<tr>
<td align="left"><strong>Jinja 템플릿</strong></td>
<td align="left">오퍼레이터 간 통합</td>
<td align="left">Bash, SQL 등 다른 오퍼레이터로 전달할 때</td>
</tr>
</tbody></table>
<h1 id="7-참고-문서">7. 참고 문서</h1>
<ul>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/xcoms.html">https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/xcoms.html</a></li>
<li><a href="https://www.astronomer.io/docs/learn/airflow-passing-data-between-tasks#intermediary-data-storage">https://www.astronomer.io/docs/learn/airflow-passing-data-between-tasks#intermediary-data-storage</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[🧱 Databricks Unity Catalog(UC) 완벽 정리 가이드]]></title>
            <link>https://velog.io/@newnew_daddy/DATABRICKS01</link>
            <guid>https://velog.io/@newnew_daddy/DATABRICKS01</guid>
            <pubDate>Wed, 14 Jan 2026 14:32:33 GMT</pubDate>
            <description><![CDATA[<h1 id="0-intro">0. INTRO</h1>
<p>데이터 플랫폼이 커질수록 가장 먼저 복잡해지는 것은 <strong>데이터 자체가 아니라 데이터의 관리 방식</strong>입니다.</p>
<p>여러 팀이 같은 데이터 레이크를 공유하고, 수많은 테이블·파일·모델이 쌓이기 시작하면
“이 데이터는 누가 만들었는가?”, “누가 접근할 수 있는가?”, “어디에 저장되어 있는가?” 같은 질문에 명확히 답하기 어려워지죠.😅</p>
<p>Databricks <strong>Unity Catalog</strong>는 이런 문제를 해결하기 위해 등장한 <strong>통합 데이터 거버넌스 계층</strong>입니다.
단순히 테이블 권한을 관리하는 기능을 넘어,
👉 <strong>데이터·파일·모델 전반에 대해 중앙 집중식으로 접근 제어, 감사, 메타데이터 관리</strong>를 제공합니다.</p>
<p>기존 Hive Metastore 기반 환경에서는 워크스페이스마다 메타스토어가 분리되거나, 클라우드 스토리지 접근 권한과 Databricks 권한이 이중으로 관리되는 경우가 많았습니다. Unity Catalog는 이를 <strong>Account 단위의 단일 Metastore 구조</strong>로 통합하여 보안과 운영 복잡도를 크게 줄였습니다.</p>
<p>또한 Unity Catalog는</p>
<ul>
<li>SQL 기반의 일관된 권한 모델</li>
<li>메이저 클라우드 벤더(AWS / Azure / GCP)에 독립적인 설계</li>
<li>테이블뿐 아니라 <strong>External Location, Volume, ML 모델</strong>까지 관리 가능</li>
</ul>
<p>이라는 점에서,
Databricks를 단순한 분석 도구가 아닌 <strong>엔터프라이즈 데이터 플랫폼</strong>으로 확장시키는 핵심 구성 요소라고 볼 수 있습니다.</p>
<p>이 글에서는 Unity Catalog의 핵심 개념과 구조를 중심으로,
왜 필요한지, 그리고 기존 방식과 무엇이 다른지 차근차근 살펴보겠습니다.</p>
<hr>
<h1 id="1-unity-catalog-계층-구조">1. Unity Catalog 계층 구조</h1>
<p>UC는 <code>Metastore &gt; Catalog &gt; Schema &gt; Table/Volume</code>의 4단계 계층 구조를 가집니다. 특히 저장 위치(Storage Location)는 상위 계층에서 하위 계층으로 상속되는 구조를 가집니다.</p>
<h3 id="1-1-metastore">1-1) <strong>Metastore</strong></h3>
<ul>
<li>UC의 최상위 컨테이너로, 모든 권한 관리와 메타데이터의 중심지입니다.</li>
<li><strong>Account Console</strong> 수준에서 생성하며, AWS/GCP/Azure 객체 저장소를 기본 위치로 사용합니다.</li>
<li>하나의 Metastore는 여러 Workspace에 연결될 수 있어 조직 전체의 통합 거버넌스를 가능하게 합니다.</li>
<li><code>Workspace &gt; Catalog Explorer &gt; External Data &gt; External Locations</code>에서 연결된 저장소 정보를 확인할 수 있습니다.<h3 id="1-2-catalog">1-2) <strong>Catalog</strong></h3>
</li>
<li>데이터 자산을 그룹화하는 첫 번째 단위입니다.</li>
<li>카탈로그 생성 시 실제 데이터가 저장될 <code>MANAGED LOCATION</code>을 지정할 수 있습니다. </li>
<li>지정하지 않으면 Metastore의 경로에 저장됩니다.<h3 id="1-3-schema-database">1-3) <strong>Schema (Database)</strong></h3>
</li>
<li>카탈로그 내의 하위 단위로 태이블, 뷰, 볼륨을 포함합니다.</li>
<li>스키마 생성 시 <code>LOCATION</code>을 지정하면 해당 경로가 하위 Managed 객체의 기본 경로가 됩니다.<h3 id="1-4-objects-table--volume">1-4) <strong>Objects (Table / Volume)</strong></h3>
</li>
<li>실제 데이터가 담기는 최종 단위입니다.</li>
<li>최하위 객체는 상위 스키마나 카탈로그에 설정된 위치를 따라가거나, 직접 <code>LOCATION</code>을 지정(External)할 수 있습니다.</li>
</ul>
<hr>
<h1 id="2-managed-vs-external-핵심-개념">2. Managed vs External 핵심 개념</h1>
<p>UC에서 가장 중요한 차이는 데이터의 소유권(Lifecycle Management)입니다.</p>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">Managed (관리형)</th>
<th align="left">External (외부형)</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>정의</strong></td>
<td align="left">UC가 데이터의 위치와 생명주기를 모두 관리</td>
<td align="left">사용자가 데이터 저장 위치를 지정</td>
</tr>
<tr>
<td align="left"><strong>저장 위치</strong></td>
<td align="left">Metastore/Catalog/Schema에 설정된 기본 경로</td>
<td align="left">DDL 작성 시 명시한 <code>LOCATION</code> 경로</td>
</tr>
<tr>
<td align="left"><strong>DROP 시 동작</strong></td>
<td align="left"><strong>메타데이터 + 물리적 데이터 모두 삭제</strong></td>
<td align="left"><strong>메타데이터만 삭제 (실제 데이터 유지)</strong></td>
</tr>
<tr>
<td align="left"><strong>UNDROP 시 동작</strong></td>
<td align="left"><strong>메타데이터 + 물리적 데이터 모두 복구 (7일 이내)</strong></td>
<td align="left"><strong>메타데이터 복구 및 기존 데이터 재연결</strong></td>
</tr>
<tr>
<td align="left"><strong>용도</strong></td>
<td align="left">일반적인 데이터베이스 워크로드</td>
<td align="left">기존 데이터의 연결 또는 외부 시스템 공유용</td>
</tr>
</tbody></table>
<hr>
<h1 id="3-시나리오별-물리-저장--삭제-규칙">3. 시나리오별 물리 저장 &amp; 삭제 규칙</h1>
<p>스키마가 External(Location 지정)이라 하더라도 그 안의 객체가 Managed인 경우, UC의 관리 원칙이 우선 적용됩니다.</p>
<table>
<thead>
<tr>
<th align="left">#</th>
<th align="left">Schema 타입</th>
<th align="left">Object</th>
<th align="left">Object 타입</th>
<th align="left">물리 저장 위치 (Path Logic)</th>
<th align="left">DROP 시 데이터 삭제 여부</th>
</tr>
</thead>
<tbody><tr>
<td align="left">1</td>
<td align="left"><strong>Managed</strong></td>
<td align="left">Table</td>
<td align="left"><strong>Managed</strong></td>
<td align="left"><code>s3://root/catalog/schema/table_name/</code></td>
<td align="left"><strong>삭제 (O)</strong></td>
</tr>
<tr>
<td align="left">2</td>
<td align="left"><strong>Managed</strong></td>
<td align="left">Table</td>
<td align="left"><strong>External</strong></td>
<td align="left"><code>gs://external/path/to/table/</code></td>
<td align="left">유지 (X)</td>
</tr>
<tr>
<td align="left">3</td>
<td align="left"><strong>Managed</strong></td>
<td align="left">Volume</td>
<td align="left"><strong>Managed</strong></td>
<td align="left"><code>s3://root/catalog/schema/volume_name/</code></td>
<td align="left"><strong>삭제 (O)</strong></td>
</tr>
<tr>
<td align="left">4</td>
<td align="left"><strong>Managed</strong></td>
<td align="left">Volume</td>
<td align="left"><strong>External</strong></td>
<td align="left"><code>gs://external/path/to/volume/</code></td>
<td align="left">유지 (X)</td>
</tr>
<tr>
<td align="left">5</td>
<td align="left"><strong>External</strong></td>
<td align="left">Table</td>
<td align="left"><strong>Managed</strong></td>
<td align="left"><strong>Schema Location</strong> 또는 <strong>Managed Storage</strong></td>
<td align="left"><strong>삭제 (O)</strong></td>
</tr>
<tr>
<td align="left">6</td>
<td align="left"><strong>External</strong></td>
<td align="left">Table</td>
<td align="left"><strong>External</strong></td>
<td align="left">테이블 DDL에 지정한 <code>LOCATION</code></td>
<td align="left">유지 (X)</td>
</tr>
<tr>
<td align="left">7</td>
<td align="left"><strong>External</strong></td>
<td align="left">Volume</td>
<td align="left"><strong>Managed</strong></td>
<td align="left"><strong>Schema Location</strong> 또는 <strong>Managed Storage</strong></td>
<td align="left"><strong>삭제 (O)</strong></td>
</tr>
<tr>
<td align="left">8</td>
<td align="left"><strong>External</strong></td>
<td align="left">Volume</td>
<td align="left"><strong>External</strong></td>
<td align="left">볼륨 DDL에 지정한 <code>LOCATION</code></td>
<td align="left">유지 (X)</td>
</tr>
</tbody></table>
<blockquote>
<p>💡 <strong>핵심 포인트</strong>: </p>
<ol>
<li>테이블 선언이 <strong>Managed</strong>라면, 하위 저장소가 어디든 <code>DROP</code> 시 데이터는 삭제됩니다.</li>
<li><strong>External Schema</strong> 내의 <strong>Managed Table</strong>은 스키마가 가진 <code>LOCATION</code> 경로 아래에 생성되더라도 &#39;Managed&#39; 특성상 삭제 권한이 UC에 있습니다.</li>
</ol>
</blockquote>
<hr>
<h1 id="4-ddl-명령어-레퍼런스">4. DDL 명령어 레퍼런스</h1>
<h3 id="4-1-catalog-생성">4-1) Catalog 생성</h3>
<p>카탈로그 수준에서 격리된 저장 공간을 사용하고 싶을 때 <code>MANAGED LOCATION</code>을 사용합니다.</p>
<pre><code class="language-sql">-- 1. 기본형 카탈로그 (메타스토어 저장소 상속)
CREATE CATALOG IF NOT EXISTS prod_catalog;

-- 2. 관리형 위치를 지정한 카탈로그 (분리된 버킷 사용)
CREATE CATALOG IF NOT EXISTS dev_catalog
MANAGED LOCATION &#39;s3://my-dev-bucket/uc-managed/&#39;;</code></pre>
<h3 id="4-2-schema-생성">4-2) Schema 생성</h3>
<ul>
<li>Schema는 Catalog 하위에 생성되며, 해당 Schema에서 생성될 <strong>Managed 객체</strong>들의 기본 저장 경로를 결정할 수 있습니다.
```sql</li>
<li><ul>
<li>Managed Schema (상위 Catalog의 경로 상속)
CREATE SCHEMA IF NOT EXISTS catalog.managed_schema;</li>
</ul>
</li>
</ul>
<p>-- External Schema (Managed 객체들이 저장될 특정 경로 지정)
CREATE SCHEMA IF NOT EXISTS catalog.external_schema
MANAGED LOCATION &#39;gs://my-bucket/external-schema-path/&#39;;</p>
<pre><code>
### 4-3) Table 생성
- **Managed Table**: `LOCATION`을 지정하지 않으면 상위 Schema/Catalog의 경로 하위에 데이터가 저장됩니다.
- **External Table**: `LOCATION`을 명시해야 하며, **External Location**으로 등록된 경로라면 어디든 저장 가능합니다. (반드시 상위 스키마 경로 아래일 필요는 없음)
```sql
-- Managed Table
CREATE TABLE catalog.schema.man_tbl (id INT, name STRING) USING DELTA;

-- External Table (External Location 등록 선행 필요)
CREATE TABLE catalog.schema.ext_tbl (id INT) 
LOCATION &#39;gs://my-bucket/data/ext_tbl/&#39;;</code></pre><h3 id="4-4-volume-생성">4-4) Volume 생성</h3>
<ul>
<li><strong>Managed Volume</strong>: UC가 관리하는 기본 경로에 파일이 저장됩니다.</li>
<li><strong>External Volume</strong>: 등록된 외부 경로를 직접 참조하여 비정형 데이터를 관리합니다.
```sql</li>
<li><ul>
<li>Managed Volume (비정형 데이터용)
CREATE VOLUME catalog.schema.man_vol;</li>
</ul>
</li>
</ul>
<p>-- External Volume (LOCATION 명시 및 External Location 등록 필요)
CREATE EXTERNAL VOLUME catalog.schema.ext_vol
LOCATION &#39;gs://my-bucket/files/ext_vol/&#39;;</p>
<p>```</p>
<hr>
<h1 id="5-관리형-볼륨volume-vs-테이블table">5. 관리형 볼륨(Volume) vs 테이블(Table)</h1>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">Tables</th>
<th align="left">Volumes</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>데이터 형태</strong></td>
<td align="left">Tabular (행/열)</td>
<td align="left">Files (모든 형식)</td>
</tr>
<tr>
<td align="left"><strong>주요 포맷</strong></td>
<td align="left">Delta, Parquet, CSV 등</td>
<td align="left">로그, 이미지, 하위 디렉토리 등</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>💡 권장 워크로드</strong>:</p>
<ul>
<li>정형 데이터 및 분석용 지표 데이터는 <strong>Table</strong> 추천</li>
<li>머신러닝 모델 파일, 로그 파일, 원본(Raw) 파일 관리는 <strong>Volume</strong> 추천</li>
</ul>
</blockquote>
<hr>
<h1 id="6-요약-및-주의사항">6. 요약 및 주의사항</h1>
<ol>
<li><strong>External Location 등록</strong>: External Table/Volume을 만들기 전, 클라우드 스토리지 경로가 UC에 <code>External Location</code>으로 등록되어 있어야 합니다.</li>
<li><strong>Managed의 삭제 규정</strong>: Managed 타입은 &quot;UC가 데이터의 생명주기를 책임진다&quot;는 뜻이므로, <code>DROP</code> 시 데이터가 물리적으로 삭제됩니다. 단, <strong>7일 이내</strong>라면 <code>UNDROP</code> 명령어로 복구가 가능합니다.</li>
<li><strong>External의 복구</strong>: External 테이블은 <code>DROP</code> 시 데이터가 유지되므로 언제든 재연결이 가능하지만, <code>UNDROP</code>을 사용하면 테이블 권한 등 메타데이터까지 함께 복구됩니다.</li>
<li><strong>스키마 위치 상속</strong>: 스키마에 <code>LOCATION</code>을 주면 그 아래의 Managed 객체들은 부모 스키마의 경로를 기본값으로 사용합니다.</li>
</ol>
<hr>
<h1 id="7-설계-시-권장-사항best-practices">7. 설계 시 권장 사항(Best Practices)</h1>
<ol>
<li><strong>환경 분리</strong>: <code>dev</code>, <code>staging</code>, <code>prod</code> 카탈로그를 만들고 각각 다른 <code>MANAGED LOCATION</code>(S3 버킷 등)을 지정하여 물리적으로 데이터를 격리하세요.</li>
<li><strong>Managed 우선</strong>: 특별한 이유(외부 시스템 공유, 기존 데이터 등)가 없다면 성능과 관리 편의성을 위해 <strong>Managed Table(Delta)</strong> 사용을 권장합니다.</li>
<li><strong>Volume 활용</strong>: <code>.csv</code>, <code>.json</code> 원본 파일이나 머신러닝 모델, 로그 등 비정형 파일은 Table이 아닌 <strong>Volume</strong>으로 관리하여 보안과 추적성을 확보합니다.</li>
<li><strong>권한 최소화</strong>: <code>EXTERNAL LOCATION</code>을 직접 참조하는 권한은 데이터 엔지니어 등 관리자에게만 부여하고, 일반 분석가는 <strong>Managed Table</strong>을 통해서만 데이터에 접근하도록 설계하세요.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GCP] Pub/Sub을 활용하여 BigQuery 테이블에 실시간으로 데이터 적재하기.]]></title>
            <link>https://velog.io/@newnew_daddy/GCP09</link>
            <guid>https://velog.io/@newnew_daddy/GCP09</guid>
            <pubDate>Mon, 25 Aug 2025 05:51:00 GMT</pubDate>
            <description><![CDATA[<h2 id="🔹-0-intro">🔹 0. INTRO</h2>
<ul>
<li>이전 글 &#39;<a href="https://velog.io/@newnew_daddy/GCP08">Google Cloud Pub/Sub 서비스의 핵심 개념과 실습 튜토리얼</a>&#39;에서는 Pub/Sub 서비스의 기초적인 내용을 살펴보았습니다. 이번 글에서는 Pub/Sub 토픽으로 전송된 메시지를 읽어 BigQuery 테이블에 직접 저장하는 방법을 다뤄보겠습니다.</li>
</ul>
<hr>
<h2 id="🔹-1-bigquery-테이블-생성">🔹 1. BigQuery 테이블 생성</h2>
<h3 id="▪-1-단일-스키마">▪ 1) 단일 스키마</h3>
<ul>
<li><p>토픽으로 전송되는 메세지를 저장할 수 있는 빅쿼리 테이블을 생성합니다. 이 때 구독 유형 중 <code>스키마 사용 안함</code> 옵션을 선택하게 되면 <code>data</code>라는 컬럼 하나가 있는 테이블의 row에 토픽에서 읽은 메세지가 저장됩니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/20a7a5df-04e8-479d-875a-d84ef1f48bd5/image.png" alt=""></p>
</li>
<li><p>BigQuery의 <code>bq_data</code> 데이터셋에 <code>data</code> 컬럼 하나만 있는 <code>pubsub_tbl_simple</code> 테이블을 생성합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/0bac9fc6-0162-47d3-af30-fe38ed2e0769/image.png" alt=""></p>
</li>
</ul>
<h3 id="▪-2-커스텀-스키마">▪ 2) 커스텀 스키마</h3>
<ul>
<li>구독 유형에서 <code>테이블 스키마 사용</code> 옵션을 선택하면, 생성한 BigQuery 테이블의 스키마에 맞춰 토픽으로 데이터를 전송할 수 있고, 구독은 해당 데이터를 읽어 BigQuery 테이블에 적재합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/da36af02-ab1c-40b3-a3a2-994c55ac88b9/image.png" alt=""></li>
<li>이번 실습에 사용할 테이블은 &#39;<a href="https://www.kaggle.com/datasets/thedevastator/us-baby-names-by-year-of-birth">Baby Names by year</a>&#39; 데이터셋이며, 스키마는 아래와 같습니다.(살짝 수정)<pre><code>id         int64
name      object
year       int64
gender    object
count      int64</code></pre></li>
<li>실습을 위해 동일한 스키마를 가지는 <code>pubsub_tbl_names</code> BigQuery 테이블을 생성합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/646ac0e3-d6f5-4d35-9188-db844099302f/image.png" alt=""></li>
</ul>
<hr>
<h2 id="🔹-2-구독-생성">🔹 2. 구독 생성</h2>
<ul>
<li>구독의 유형에는 아래 네가지 종류가 있습니다.<ul>
<li><code>가져오기</code> : 메세지를 읽어오기</li>
<li><code>푸시</code> : 메세지를 다른 Endpoint URL로 전송</li>
<li><code>BigQuery에 쓰기</code> : 메세지를 BigQuery 테이블에 저장</li>
<li><code>Cloud Storage에 쓰기</code> : 메세지를 GCS 파일 객체로 저장</li>
</ul>
</li>
</ul>
<p>그 중 <code>BigQuery에 쓰기</code> 유형의 구독을 생성하여 토픽에 들어오는 데이터를 실시간으로 읽어와 BigQuery 테이블에 저장해보도록 하겠습니다.
Google Pub/Sub 서비스의 토픽·발행자·구독자의 개념 및 토픽 생성 방법은 이전 글(<a href="https://velog.io/@newnew_daddy/GCP08">링크</a>)을 참고하시면 됩니다.
생성할 구독은 총 2가지로,</p>
<p>1) 단일 스키마 테이블인 <code>pubsub_tbl_simple</code>로 전송하는 구독
2) 커스텀 스키마 테이블인 <code>pubsub_tbl_names</code>로 전송하는 구독 
이렇게 생성합니다.</p>
<h3 id="▪-1-단일-스키마-테이블에-쓰기">▪ 1) 단일 스키마 테이블에 쓰기</h3>
<ul>
<li>위에서 생성한 테이블 중 <code>data</code>라는 컬럼 하나를 가진 <code>pubsub_tbl_simple</code> 테이블로 데이터를 전송하는 구독을 생성해보겠습니다.<ul>
<li>구독ID : <code>dev_bq_subscription</code></li>
<li>전송 유형 : <code>BigQuery에 쓰기</code></li>
<li>스키마 구성 : <code>스키마 사용 안함</code></li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/newnew_daddy/post/49395bed-0b3f-4e82-93bc-0538f31297ca/image.png" alt=""></p>
<h3 id="▪-2-커스텀-스키마-테이블에-쓰기">▪ 2) 커스텀 스키마 테이블에 쓰기</h3>
<ul>
<li>5개의 컬럼을 가진 <code>pubsub_tbl_names</code> 테이블로 데이터를 전송하는 구독을 생성합니다.<ul>
<li>구독ID : <code>dev_bq_names_subscription</code></li>
<li>전송 유형 : <code>BigQuery에 쓰기</code></li>
<li>스키마 구성 : <code>테이블 스키마 사용</code></li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/newnew_daddy/post/6ba6910c-af9d-4faa-838a-60803bffff54/image.png" alt=""></p>
<p>토픽에 들어온 데이터를 받아 각 테이블들로 저장해줄 구독 2개 생성이 완료되었습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/48f94ffa-10fb-4776-bbbd-8bcb7e280c1c/image.png" alt=""></p>
<hr>
<h2 id="🔹-3-실시간-메세지-전송">🔹 3. 실시간 메세지 전송</h2>
<ul>
<li>위에서 생성한 토픽과 구독, BigQuery 테이블의 관계는 아래와 같습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/c1b4fc91-2a80-448d-8a8e-2a4dabdd0d51/image.png" alt=""></li>
<li>names 데이터셋을 JSON 형식으로 <code>dev_topic</code> 토픽에 전송하였을 때, 빅쿼리 테이블들에 어떤식으로 데이터가 적재되는지 확인해보도록 하겠습니다.</li>
</ul>
<h3 id="▪-1-토픽에-메세지-전송">▪ 1) 토픽에 메세지 전송</h3>
<ul>
<li>파이썬 코드를 통해 <code>dev_topic</code> 토픽으로 데이터가 3초에 한 번씩 전송되도록 합니다.</li>
</ul>
<pre><code class="language-python">import pandas as pd
from google.cloud import pubsub_v1
from google.oauth2 import service_account
import json, time
from faker import Faker

## 1. PubSub 토픽 관련 설정
PROJECT_ID = &quot;[프로젝트 ID]&quot;
KEY_PATH = &quot;[서비스 계정 JSON KEY 경로]&quot;
CREDENTIALS = service_account.Credentials.from_service_account_file(KEY_PATH)

TOPIC_ID = &quot;dev_topic&quot;
publisher = pubsub_v1.PublisherClient(credentials=CREDENTIALS)
TOPIC_PATH = publisher.topic_path(PROJECT_ID, TOPIC_ID)

# 2. 보낼 데이터셋을 읽어 JSON 형식으로 변환
df = pd.read_csv(&#39;names.csv&#39;)
df_dict = df.to_dict(orient=&#39;records&#39;)

# 3. dev_topic으로 데이터를 3초에 한 번씩 전송
for row in df_dict[:10]:
    future = publisher.publish(
        topic=TOPIC_PATH,
        data=json.dumps(row).encode(&quot;utf-8&quot;)
    )
    print(f&quot;보낸 메시지: {row} / 결과: {future.result()}&quot;)
    time.sleep(3)


--- 출력 결과 ---
보낸 메시지: {&#39;id&#39;: 1, &#39;name&#39;: &#39;Mary&#39;, &#39;year&#39;: 1880, &#39;gender&#39;: &#39;F&#39;, &#39;count&#39;: 7065} / 결과: 16021080392424521
보낸 메시지: {&#39;id&#39;: 2, &#39;name&#39;: &#39;Anna&#39;, &#39;year&#39;: 1880, &#39;gender&#39;: &#39;F&#39;, &#39;count&#39;: 2604} / 결과: 16021545983015478
보낸 메시지: {&#39;id&#39;: 3, &#39;name&#39;: &#39;Emma&#39;, &#39;year&#39;: 1880, &#39;gender&#39;: &#39;F&#39;, &#39;count&#39;: 2003} / 결과: 16022743523116683
보낸 메시지: {&#39;id&#39;: 4, &#39;name&#39;: &#39;Elizabeth&#39;, &#39;year&#39;: 1880, &#39;gender&#39;: &#39;F&#39;, &#39;count&#39;: 1939} / 결과: 16022824001573059
보낸 메시지: {&#39;id&#39;: 5, &#39;name&#39;: &#39;Minnie&#39;, &#39;year&#39;: 1880, &#39;gender&#39;: &#39;F&#39;, &#39;count&#39;: 1746} / 결과: 16022962397816275
보낸 메시지: {&#39;id&#39;: 6, &#39;name&#39;: &#39;Margaret&#39;, &#39;year&#39;: 1880, &#39;gender&#39;: &#39;F&#39;, &#39;count&#39;: 1578} / 결과: 16022437774262030
보낸 메시지: {&#39;id&#39;: 7, &#39;name&#39;: &#39;Ida&#39;, &#39;year&#39;: 1880, &#39;gender&#39;: &#39;F&#39;, &#39;count&#39;: 1472} / 결과: 16021982500229449
보낸 메시지: {&#39;id&#39;: 8, &#39;name&#39;: &#39;Alice&#39;, &#39;year&#39;: 1880, &#39;gender&#39;: &#39;F&#39;, &#39;count&#39;: 1414} / 결과: 16022962752184659
보낸 메시지: {&#39;id&#39;: 9, &#39;name&#39;: &#39;Bertha&#39;, &#39;year&#39;: 1880, &#39;gender&#39;: &#39;F&#39;, &#39;count&#39;: 1320} / 결과: 16022046604942106
보낸 메시지: {&#39;id&#39;: 10, &#39;name&#39;: &#39;Sarah&#39;, &#39;year&#39;: 1880, &#39;gender&#39;: &#39;F&#39;, &#39;count&#39;: 1288} / 결과: 16022147167489566</code></pre>
<h3 id="▪-2-단일-스키마-테이블-확인">▪ 2) 단일 스키마 테이블 확인</h3>
<ul>
<li><code>pubsub_tbl_simple</code> 테이블의 경우 전송한 JSON 형식의 데이터가 <code>data</code>라는 단일 컬럼 안에 STRING 형식으로 저장되는 것을 확인할 수 있습니다. 이렇게 <code>스키마 사용 안함</code> 유형의 구독은 추후 분석을 위해 테이블에 대한 추가적인 가공이 필요하며, 정형 데이터보다는 비정형이나 TEXT 형태의 데이터를 실시간으로 저장할 때 유용하게 쓰일 수 있을 것 같습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/88cb2ba5-3805-419e-a76f-832994680548/image.png" alt=""></li>
</ul>
<h3 id="▪-3-커스텀-스키마-테이블-확인">▪ 3) 커스텀 스키마 테이블 확인</h3>
<ul>
<li><code>pubsub_tbl_names</code> 테이블의 경우 테이블 스키마에 맞게 데이터를 전송하면 각 컬럼에 해당 값이 실시간으로 저장되어 분석에 용이한 정형 데이터 테이블 형태로 관리할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/ca5537c1-56a2-4475-809c-831f40287c1f/image.png" alt=""></li>
</ul>
<hr>
<h2 id="🔹-4-outro">🔹 4. OUTRO</h2>
<ul>
<li>이번 글에서는 토픽으로 받은 메시지를 BigQuery 테이블로 직접 적재하는 방식과 스키마 적용 여부에 따른 차이까지 살펴보았습니다. Pub/Sub의 경우 GCP의 서비스이기 때문에 GCS, 빅쿼리, Data Fusion 등 클라우드 내 서비스들과 원활하게 통합되어 사용된다면 훨씬 더 큰 시너지를 낼 수 있을 것이라 생각합니다.</li>
<li>앞으로는 Pub/Sub의 실시간성을 어떻게 데이터 파이프라인 아키텍처에 반영할지 고민해 보는 것이 중요할 것 같습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GCP] Google Cloud Pub/Sub 서비스의 핵심 개념과 실습 튜토리얼 (UI / Python)]]></title>
            <link>https://velog.io/@newnew_daddy/GCP08</link>
            <guid>https://velog.io/@newnew_daddy/GCP08</guid>
            <pubDate>Wed, 20 Aug 2025 12:52:17 GMT</pubDate>
            <description><![CDATA[<h2 id="🔹-0-intro">🔹 0. INTRO</h2>
<ul>
<li>현대 소프트웨어 아키텍처에서 시스템 간의 효율적인 통신은 필수적입니다. 특히 마이크로서비스 환경에서는 각 서비스가 독립적으로 동작하면서도 서로 간의 데이터를 주고받아야 하는 상황이 자주 발생합니다. 이 때 사용할 수 있는 개념으로 Queue 라는 자료구조가 있습니다. 
Queue를 통해 우리는 시스템 간의 느슨한 결합 가지는 아키텍처를 구성할 수 있습니다.
우리에게 익숙한 AWS 클라우드에서 가장 먼저 런칭된 서비스가 AWS SQS라고 하는 비동기 메세징 서비스였습니다. GCP에도 이와 동일한 서비스가 있는데 바로 <strong>&#39;Pub/Sub&#39;</strong>이라는 서비스입니다.
이번 글에서는 Google Cloud Pub/Sub 서비스의 개념에 대해 알아보고 python을 활용하여 어떻게 다룰 수 있는지 실습해보도록 하겠습니다.</li>
</ul>
<hr>
<h2 id="🔹-1-google-cloud-pubsub이란">🔹 1. Google Cloud Pub/Sub이란?</h2>
<ul>
<li>Google Cloud Pub/Sub은 완전 관리형의 비동기 메시징 서비스로, 발행자(Publisher)와 구독자(Subscriber) 간의 메시지 전달을 할 수 있습니다. 전통적인 동기식 통신 방식과 달리, 메시지를 보내는 쪽과 받는 쪽이 서로를 직접 기다리지 않고도 데이터를 교환할 수 있게 하여 서비스간의 느슨한 결합을 가능하게 해줍니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/e5219988-c28f-4b4c-a9e3-65c57964f905/image.png" alt=""></li>
</ul>
<h3 id="▪-핵심-개념-설명">▪ 핵심 개념 설명</h3>
<h4 id="토픽topic">토픽(Topic)</h4>
<ul>
<li>메세지가 저장되는 저장소로, 발행자가 보낸 메세지를 임시로 보관하는 역할을 합니다. </li>
<li>저장되는 메세지의 종류에 따라 여러 토픽 생성이 가능하며, GCP 프로젝트 내에서는 고유한 이름으로 식별되어야 합니다.</li>
</ul>
<h4 id="발행자publisher">발행자(Publisher)</h4>
<ul>
<li>데이터를 생성하고, 이 생성된 데이터를 특정 토픽에 전송하는 역할을 하는 컴포넌트입니다.</li>
</ul>
<h4 id="구독자subscriber">구독자(Subscriber)</h4>
<ul>
<li>특정 토픽을 구독하여 발행자가 보낸 메세지를 수신하고 처리하는 역할을 담당하는 컴포넌트입니다. </li>
<li>단순히 메세지를 읽어올 수도 있고, 읽어온 메세지를 BigQuery나 GCS에 저장하도록 설정할 수도 있습니다.</li>
</ul>
<h4 id="발행자---토픽---구독자의-관계">발행자 - 토픽 - 구독자의 관계</h4>
<p><img src="https://velog.velcdn.com/images/newnew_daddy/post/5c9adfdb-ec73-48f9-86e6-de46491ab7a1/image.png" alt=""></p>
<h2 id="🔹-2-pubsub-실습---ui">🔹 2. Pub/Sub 실습 - UI</h2>
<h3 id="▪-1-토픽-생성">▪ 1) 토픽 생성</h3>
<ul>
<li>가장 먼저 메세지를 발행하여 저장할 토픽을 생성해줍니다. <code>Pub/Sub → 주제 → +주제 만들기</code> 탭에 들어가서 토픽의 이름만 설정해주면 바로 생성이 가능합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/f39a05b1-fcd5-4e65-859e-4bf4360758da/image.png" alt=""></li>
</ul>
<h3 id="▪-2-구독-생성">▪ 2) 구독 생성</h3>
<ul>
<li>토픽을 생성하였다면 해당 토픽으로 들어오는 메세지를 받아서 소비할 구독을 생성해야 합니다.</li>
<li><code>Pub/Sub → 구독 → +구독 만들기</code> 탭에서 구독 생성이 가능합니다. 구독의 유형으로는 아래 4가지를 선택할 수 있습니다.<ul>
<li><code>가져오기</code> : 메세지를 읽어오기</li>
<li><code>푸시</code> : 메세지를 다른 Endpoint URL로 전송</li>
<li><code>BigQuery에 쓰기</code> : 메세지를 BigQuery 테이블에 저장</li>
<li><code>Cloud Storage에 쓰기</code> : 메세지를 GCS 파일 객체로 저장(TEXT or AVRO 포맷)</li>
</ul>
</li>
<li><code>구독 ID</code> 와 메세지를 읽어올 토픽을 선택하고, 가장 기본이 되는 <code>가져오기</code> 유형으로 구독을 생성합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/25c63521-f96e-461e-94f3-c457c0eda5e7/image.png" alt=""></li>
</ul>
<h3 id="▪-3-메세지-발행">▪ 3) 메세지 발행</h3>
<ul>
<li>위에서 생성한 토픽에서 테스트 메세지 생성이 가능합니다. 아래와 같이 토픽에 들어가서 <code>메세지 → 메세지 게시</code> 탭을 선택하고,
<img src="https://velog.velcdn.com/images/newnew_daddy/post/79604ad0-4a8a-44e3-8081-9ec0459d7000/image.png" alt="">
<code>메세지 본분</code>에 간략히 내용을 적어서 게시하면 해당 토픽에서 메세지를 발행할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/b22dd573-52e9-4de3-b8c6-1306ee54ab14/image.png" alt=""></li>
</ul>
<h3 id="▪-4-메세지-가져오기">▪ 4) 메세지 가져오기</h3>
<ul>
<li>토픽에서 발행한 메세지는 구독에서 가져올 수 있습니다. 위에서 <code>dev_topic</code>을 구독하는 <code>dev_subscription</code>에서 발행된 메세지 확인이 가능합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/9c3044e8-c73e-48ba-a9fd-81b296ae083d/image.png" alt=""></li>
</ul>
<h2 id="🔹-3-pubsub-실습---python">🔹 3. Pub/Sub 실습 - Python</h2>
<ul>
<li>위에서 처럼 직관적인 GUI 환경에서 설정할 수도 있지만 Google의 Pub/Sub 관련 라이브러리를 설치하면 Python 코드로도 해당 기능 구현이 가능합니다.</li>
</ul>
<h3 id="▪-1-라이브러리-설치-및-기본-설정">▪ 1) 라이브러리 설치 및 기본 설정</h3>
<ul>
<li>라이브러리 설치 → <code>pip install google-cloud-pubsub</code></li>
<li>아래 코드에서 GCP와 통신할 Client 및 토픽 객체를 생성합니다.<pre><code class="language-python">from google.cloud import pubsub_v1
from google.oauth2 import service_account
</code></pre>
</li>
</ul>
<p>PROJECT_ID = &quot;[프로젝트 ID]&quot;
KEY_PATH = &quot;[서비스 계정 JSON KEY 경로]&quot;
CREDENTIALS = service_account.Credentials.from_service_account_file(KEY_PATH)</p>
<p>TOPIC_ID = &quot;dev_topic&quot;
publisher = pubsub_v1.PublisherClient(credentials=CREDENTIALS)
TOPIC_PATH = publisher.topic_path(PROJECT_ID, TOPIC_ID)</p>
<p>print(TOPIC_PATH)</p>
<p>--- 출력 결과 ---
projects/codeit-hyunsoo/topics/dev_topic</p>
<pre><code>
### ▪ 2) 토픽 생성
- 이미 생성된 토픽이라면 pass 되도록 예외처리까지 포함하여 구성합니다.
```python
# Pub/Sub 토픽 생성 코드
from google.api_core.exceptions import AlreadyExists

try:
    topic = publisher.create_topic(name=TOPIC_PATH)
    print(f&quot;토픽이 생성되었습니다: {topic.name}&quot;)
except AlreadyExists:
    print(f&quot;이미 존재하는 토픽입니다: {TOPIC_PATH}&quot;)


--- 출력 결과 ---
토픽이 생성되었습니다: projects/codeit-hyunsoo/topics/dev_topic</code></pre><h3 id="▪-3-샘플-메세지-전송">▪ 3) 샘플 메세지 전송</h3>
<ul>
<li><code>Faker</code> 라이브러리를 활용해 랜덤한 문장 생성 후 <code>dev_topic</code>으로 전송하는 발행자를 만들어줍니다. (3초에 한 번씩 총 10개의 메세지 전송)<pre><code class="language-python">from faker import Faker
import time
</code></pre>
</li>
</ul>
<p>fake = Faker()</p>
<p>for _ in range(10):
    msg = fake.sentence()
    future = publisher.publish(
        topic=TOPIC_PATH,
        data=msg.encode(&quot;utf-8&quot;),
        source=&quot;app1&quot;
    )
    print(f&quot;보낸 메시지: {msg} / 결과: {future.result()}&quot;)
    time.sleep(3)</p>
<p>--- 출력 결과 ---
보낸 메시지: Include business head send friend him final. / 결과: 15936265876075595
보낸 메시지: Check example left chance approach large. / 결과: 15936256995631331
보낸 메시지: Heart still property. / 결과: 15936267081923148
보낸 메시지: Can production admit with business moment future. / 결과: 15936297693105990
보낸 메시지: Reflect national government between bag part with mission. / 결과: 15936288400415598
보낸 메시지: Meeting drive anyone note. / 결과: 15936290797590549
보낸 메시지: Particularly fact see far election. / 결과: 15936291128627334
보낸 메시지: Hair energy tax whole model head hit. / 결과: 15936290594708674
보낸 메시지: Right range trade score half. / 결과: 15936291572828675
보낸 메시지: Effect where sign popular family media. / 결과: 15936285434903098</p>
<pre><code>
### ▪ 4) 구독 생성
- 구독 역시 토픽 생성과 비슷하게 서비스계정 JSON KEY를 기반으로 Client 및 구독 객체를 생성하고, `가져오기` 유형의 구독을 python으로 생성합니다.
```python
from google.cloud import pubsub_v1
from google.oauth2 import service_account
import time
from google.api_core.exceptions import AlreadyExists

PROJECT_ID = &quot;[프로젝트 ID]&quot;
KEY_PATH = &quot;[서비스 계정 JSON KEY 경로]&quot;
CREDENTIALS = service_account.Credentials.from_service_account_file(KEY_PATH)

# 토픽 정보
TOPIC_ID = &quot;dev_topic&quot;
publisher = pubsub_v1.PublisherClient(credentials=CREDENTIALS)
TOPIC_PATH = publisher.topic_path(PROJECT_ID, TOPIC_ID)

# 구독 정보
SUBSCRIPTION_ID = &quot;dev_subscription&quot;
subscriber = pubsub_v1.SubscriberClient(credentials=CREDENTIALS)
SUBSCRIPTION_PATH = subscriber.subscription_path(PROJECT_ID, SUBSCRIPTION_ID)

try:
    subscription = subscriber.create_subscription(
        name=SUBSCRIPTION_PATH,
        topic=TOPIC_PATH
    )
    print(f&quot;구독이 생성되었습니다: {SUBSCRIPTION_PATH}&quot;)
except AlreadyExists:
    print(f&quot;이미 존재하는 구독입니다: {SUBSCRIPTION_PATH}&quot;)


--- 출력 결과 ---
구독이 생성되었습니다: projects/codeit-hyunsoo/subscriptions/dev_subscription</code></pre><h3 id="▪-5-토픽에-저장된-메세지-읽기">▪ 5) 토픽에 저장된 메세지 읽기</h3>
<ul>
<li><code>dev_topic</code>에 저장된 메세지들은 구독을 통해 읽어올 수 있습니다. 아래 코드는 1초에 한 번씩 메세지를 확인하여 출력해줍니다.<pre><code class="language-python">def callback(message):
  print(f&quot;받은 메시지: {message.data.decode(&#39;utf-8&#39;)}&quot;)
  message.ack()
</code></pre>
</li>
</ul>
<p>streaming_pull_future = subscriber.subscribe(SUBSCRIPTION_PATH, callback=callback)
print(f&quot;구독을 시작합니다: {SUBSCRIPTION_PATH}&quot;)</p>
<p>try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    streaming_pull_future.cancel()
    print(&quot;구독을 중단합니다.&quot;)
finally:
    streaming_pull_future.cancel()
    subscriber.close()</p>
<p>--- 출력 결과 ---
구독을 시작합니다: projects/codeit-hyunsoo/subscriptions/dev_subscription
받은 메시지: This is Sample Message
구독을 중단합니다.</p>
<p>```</p>
<h2 id="🔹-4-outro">🔹 4. OUTRO</h2>
<ul>
<li>지금까지 Google Cloud Pub/Sub의 기본 개념부터 실제 구현까지 단계별로 살펴보았습니다. 토픽과 프로듀서, 컨슈머 개념 등 사용해보면서 Kafka와 상당히 비슷하다는 느낌을 받을 수 있었습니다. </li>
<li>다음 글에서는 토픽으로 들어온 메세지를 BigQuery와 GCS에 객체로 저장하는 방법에 대해 다뤄보도록 하겠습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[DuckLake 초기 세팅 및 기초 사용 튜토리얼! (PostgreSQL/MySQL + 클라우드 객체 저장소)]]></title>
            <link>https://velog.io/@newnew_daddy/data10</link>
            <guid>https://velog.io/@newnew_daddy/data10</guid>
            <pubDate>Thu, 10 Jul 2025 05:00:10 GMT</pubDate>
            <description><![CDATA[<h2 id="🔹-0-intro">🔹 0. INTRO</h2>
<ul>
<li>요즘 데이터 엔지니어링의 핵심 트렌드 중 하나는 단연 레이크하우스(Lakehouse)입니다. Apache Iceberg, Delta Lake, Apache Hudi 등 오픈 테이블 형식(Open Table Format)의 등장으로, 파일 기반 데이터 레이크에서도 ACID 트랜잭션, 스키마 진화, 타임 트래블과 같은 데이터베이스급 기능을 사용할 수 있게 되었죠. 하지만 이러한 솔루션들은 복잡한 <strong>JSON/Avro 기반의 메타데이터 시스템을 사용</strong>하기 때문에, 작은 변경사항 처리나 트랜잭션 관리에서 한계를 드러내기도 합니다.</li>
<li>이러한 문제를 해결하고자 DuckDB 팀은 DuckLake라는 새로운 오픈 테이블 형식을 제안했습니다. 이는 기존 레이크하우스와 유사한 구조를 가지면서도, 핵심적인 차별점은 <strong>모든 메타데이터를 표준 SQL 데이터베이스에 저장한다</strong>는 점입니다.</li>
<li>이번 글에서는 기존의 솔루션들과 차별화되는 DuckLake의 핵심적인 특징들을 살펴보고 python을 이용해 어떻게 ducklake로 관리되는 환경을 구성할 수 있는지에 대한 실습까지 다뤄보도록 하겠습니다.</li>
</ul>
<h2 id="🔹-1-ducklake의-핵심-특징">🔹 1. DuckLake의 핵심 특징</h2>
<ul>
<li>DuckLake는 메타데이터를 데이터베이스에 저장함으로써, 파일 기반 레이크하우스가 안고 있던 복잡성과 성능 병목 문제를 해결하고 더 단순하고 유연한 대안을 제공합니다.</li>
</ul>
<h4 id="1-성능-최적화">1) 성능 최적화</h4>
<ul>
<li>메타데이터 쿼리가 단일 SQL로 처리되어 빠름</li>
<li>수많은 HTTP 파일 요청 대신 메타데이터 인라인(inline) 저장</li>
<li>서브 밀리초 단위의 쓰기 및 작은 파일 문제 해결</li>
</ul>
<h4 id="2-강력한-일관성과-트랜잭션-지원">2) 강력한 일관성과 트랜잭션 지원</h4>
<ul>
<li>관계형 DB의 ACID 속성 활용</li>
<li>다중 테이블 트랜잭션 및 트랜잭션 DDL 지원</li>
</ul>
<h4 id="3-유연한-메타데이터-및-저장소-구성">3) 유연한 메타데이터 및 저장소 구성</h4>
<ul>
<li>PostgreSQL, MySQL, DuckDB 등 다양한 DB 지원</li>
<li>S3, GCS, 로컬 디스크 등 다양한 저장소와 호환</li>
</ul>
<h4 id="4-스냅샷-및-보안-기능-내장">4) 스냅샷 및 보안 기능 내장</h4>
<ul>
<li>데이터베이스 테이블 기반으로 수백만 개의 스냅샷을 효율적 관리</li>
<li>기본 내장 암호화로 지속적인 검증(Zero Trust) 환경 구현</li>
</ul>
<h2 id="🔹-2-설치-및-환경-구성">🔹 2. 설치 및 환경 구성</h2>
<ul>
<li>DuckLake를 활용하기 위해서는 DuckDB가 설치되어 있어야 하고, 메타 테이블이 저장될 데이터베이스가 준비되어 있어야 합니다.</li>
<li>DuckDB의 경우 pip으로 간단하게 설치하여 파이썬에서 활용이 가능하며, 메타 DB의 경우 Docker Compose를 이용하여 컨테이너 환경에서 운영될 수 있도록 구성할 것입니다.</li>
</ul>
<h3 id="1-duckdb-및-관련-패키지-설치">1) DuckDB 및 관련 패키지 설치</h3>
<ul>
<li>DuckDB 설치 → <a href="https://duckdb.org/docs/installation/?version=stable&amp;environment=cli&amp;platform=macos&amp;download_method=direct">공식 문서</a></li>
<li>파이썬으로 간단히 설치 → <code>pip install duckdb</code> (반드시 1.3 버전 이상 설치)</li>
<li>DuckDB에서 Extension들을 사용하기 위해서는 각 패키지들에 대한 설치를 해야합니다. 이번 실습에서는 아래 내용들에 대한 설치가 필요합니다.<pre><code class="language-python">import duckdb as dd
</code></pre>
</li>
</ul>
<h1 id="in-memory-상태에서-데이터베이스-연결-생성">In Memory 상태에서 데이터베이스 연결 생성</h1>
<p>mem_con = dd.connect()</p>
<h1 id="extension들-설치">Extension들 설치</h1>
<p>mem_con.execute(&quot;INSTALL postgres&quot;)
mem_con.execute(&quot;INSTALL mysql&quot;)
mem_con.execute(&quot;INSTALL ducklake&quot;)
mem_con.execute(&quot;INSTALL httpfs&quot;)</p>
<pre><code>
### 2) Meta DB 구성(Docker Compose)
- DuckLake 메타 테이블들이 저장될 데이터베이스는 현재까지는 아래 4가지를 사용할 수 있습니다.
  - `DuckDB`, `Sqlite`, `PostgreSQL`, `MySQL`
- 이번 실습에서는 `PostgreSQL`과 `MySQL`을 각각 활용해보도록 하겠습니다.
- 아래 Docker Compose 파일을 실행해주면 `PostgreSQL`, `MySQL` 컨테이너가 생성됩니다.
```yaml
services:
  mysql:
    image: mysql
    container_name: mysql_db
    ports:
      - &quot;3306:3306&quot;
    environment:
      - MYSQL_ROOT_PASSWORD=123456
      - MYSQL_USER=hyunsoo
      - MYSQL_PASSWORD=velog
      - MYSQL_DATABASE=ducklake_catalog
  postgres:
    image: postgres:16
    container_name: postgres_db
    ports:
      - &quot;5432:5432&quot;
    environment:
      - POSTGRES_USER=hyunsoo
      - POSTGRES_PASSWORD=velog
      - POSTGRES_DB=ducklake_catalog</code></pre><blockquote>
<p>실행 명령 → docker compose up -d</p>
</blockquote>
<h2 id="🔹-3-ducklake-연동">🔹 3. DuckLake 연동</h2>
<ul>
<li>DuckDB 설치와 데이터베이스 컨테이너 생성이 완료되었다면 기본적인 준비는 다 되었습니다. 이제부터는 아래 두 과정을 추가적으로 진행해주고 본격적인 연동 작업을 진행하면 됩니다.<ul>
<li>1) 데이터가 저장될 클라우드 객체 저장소와의 자격 증명 내용 저장</li>
<li>2) 메타 데이터가 저장될 데이터베이스 연결 내용 저장</li>
</ul>
</li>
</ul>
<h3 id="1-객체-저장소-secret-생성">1) 객체 저장소 SECRET 생성</h3>
<ul>
<li><p>GCS(Google Cloud Storage)의 경우 아래 공식 문서를 참고하여 &#39;HMAC Key&#39; 발급 후 SECRET 생성이 가능합니다.</p>
<ul>
<li><a href="https://duckdb.org/docs/stable/guides/network_cloud_storage/gcs_import.html">DuckDB-GCS 공식문서</a><pre><code class="language-python">secret_gcs = &quot;&quot;&quot;
CREATE SECRET (
TYPE GCS,
KEY_ID &#39;ABCDE&#39;,
SECRET &#39;ABCDESECRET&#39;
);
&quot;&quot;&quot;
mem_con.execute(secret_gcs)</code></pre>
</li>
</ul>
</li>
<li><p>AWS S3의 경우 IAM 장기 자격증명 발급 후 SECRET 생성이 가능합니다.</p>
<ul>
<li><a href="https://duckdb.org/docs/stable/core_extensions/httpfs/s3api">DuckDB-S3 공식문서</a><pre><code class="language-python">secret_s3 = &quot;&quot;&quot;
CREATE SECRET (
TYPE s3,
PROVIDER config,
KEY_ID &#39;AKIAIOSFODNN7EXAMPLE&#39;,
SECRET &#39;wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY&#39;,
REGION &#39;ap-northeast-2&#39;
);
&quot;&quot;&quot;
mem_con.execute(secret_s3)</code></pre>
</li>
</ul>
</li>
<li><p>생성한 SECRET 목록 확인</p>
<pre><code class="language-python">mem_con.execute(&quot;FROM duckdb_secrets()&quot;).df()</code></pre>
<h3 id="2-meta-db-secret-생성">2) Meta DB SECRET 생성</h3>
</li>
<li><p>DuckLake의 메타 테이블 저장소로 사용되는 <code>PostgreSQL</code>과 <code>MySQL</code> 역시 SECRET으로 등록해주어야 합니다.</p>
</li>
<li><p>이번 글에서는 데이터베이스 각각에 대해서 연결하는 코드를 모두 작성하였지만 실제로 DuckLake 사용을 위해서는 하나의 데이터베이스만 선택해서 사용하면 됩니다.</p>
</li>
</ul>
<h4 id="postgresql-등록-→-공식-문서">PostgreSQL 등록 → <a href="https://duckdb.org/docs/stable/core_extensions/postgres">공식 문서</a></h4>
<pre><code class="language-python">pg_conn = &quot;&quot;&quot;
CREATE SECRET (
    TYPE postgres,
    HOST &#39;127.0.0.1&#39;,
    PORT 5432,
    DATABASE ducklake_catalog,
    USER &#39;codeit&#39;,
    PASSWORD &#39;sprint&#39;
);
&quot;&quot;&quot;
mem_con.execute(pg_conn)</code></pre>
<h4 id="mysql-등록-→-공식-문서">MySQL 등록 → <a href="https://duckdb.org/docs/stable/core_extensions/mysql.html">공식 문서</a></h4>
<pre><code class="language-python">mysql_conn = &quot;&quot;&quot;
CREATE SECRET (
    TYPE mysql,
    HOST &#39;127.0.0.1&#39;,
    PORT 3306,
    DATABASE ducklake_catalog,
    USER &#39;codeit&#39;,
    PASSWORD &#39;sprint&#39;
);
&quot;&quot;&quot;
mem_con.execute(mysql_conn)</code></pre>
<h3 id="3-ducklake-연동">3) DuckLake 연동</h3>
<ul>
<li><code>ATTACH</code> 명령어를 통해 DuckLake 설정이 가능하며, <code>DATA_PATH</code> 다음에는 DuckLake에서 관리되는 데이터가 저장될 경로를 넣어주시면 됩니다. 아래 예시와 같이 로컬 경로, 클라우드 스토리지 경로 등 사용 환경에 맞게 설정이 가능합니다.<ul>
<li>로컬 경로 예시 : <code>/home/ducklake/data/</code> </li>
<li>AWS S3 예시 : <code>s3://aws_ducklake_bucket/data_dir/</code></li>
<li>GCS 예시 : <code>gs://gcs_ducklake_bucket/data_dir/</code></li>
</ul>
</li>
<li>아래 실습에서는 Google Cloud Storage의 <code>gs://hyunsoo_de_bucket/ducklake/postgres/</code> 경로를 데이터 저장소로 사용해보겠습니다.</li>
<li>형식은 아래와 같습니다.<pre><code class="language-sql">ATTACH &#39;ducklake:&lt;연결 DB 종류&gt;:dbname=&lt;데이터베이스 이름&gt;&#39;
AS &lt;생성할 DB 이름&gt;(DATA_PATH &lt;객체 저장소 경로&gt;);</code></pre>
</li>
</ul>
<h4 id="postgresql을-메타-db로-사용하는-경우">PostgreSQL을 메타 DB로 사용하는 경우</h4>
<pre><code class="language-python">ducklake_conn = &quot;&quot;&quot;
ATTACH &#39;ducklake:postgres:dbname=ducklake_catalog&#39; 
AS my_ducklake(DATA_PATH &#39;gs://hyunsoo_de_bucket/ducklake/postgres/&#39;);
&quot;&quot;&quot;
mem_con.execute(ducklake_conn)</code></pre>
<h4 id="mysql을-메타-db로-사용하는-경우">MySQL을 메타 DB로 사용하는 경우</h4>
<pre><code class="language-python">ducklake_conn = &quot;&quot;&quot;
ATTACH &#39;ducklake:mysql:db=ducklake_catalog&#39; 
AS my_ducklake(DATA_PATH &#39;gs://hyunsoo_de_bucket/ducklake/mysql/&#39;);
&quot;&quot;&quot;
mem_con.execute(ducklake_conn)</code></pre>
<ul>
<li>위의 코드가 에러 없이 잘 실행이 되었다면 DuckLake 사용환경 설정이 완료가 되었습니다. 선택한 데이터베이스의 <code>ducklake_catalog</code> DB를 조회해보면 아래와 같이 DuckLake 관련 메타 테이블들이 세팅되어 있는 것을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/0e9164e0-d845-49d9-bdd0-d41d0977ecc7/image.png" alt=""></li>
</ul>
<h2 id="🔹-4-데이터-조회-및-테이블-생성">🔹 4. 데이터 조회 및 테이블 생성</h2>
<h3 id="1-cloud-객체-저장소-데이터-조회">1) Cloud 객체 저장소 데이터 조회</h3>
<ul>
<li>등록한 객체 저장소 SECRET을 활용해 객체 저장소에 있는 파일(JSON, parquet, CSV 등) 데이터를 DuckDB로 바로 조회할 수 있습니다.<pre><code class="language-python">_query = &quot;&quot;&quot;
FROM read_parquet(&#39;gs://hyunsoo_de_bucket/dataset/emp.parquet&#39;)
&quot;&quot;&quot;
</code></pre>
</li>
</ul>
<p>mem_con.execute(_query).df()</p>
<pre><code>
### 2) 데이터베이스 조회
- 데이터베이스를 조회해보면 DuckLake로 관리되는 데이터베이스가 목록에 포함되어 있는 것을 확인할 수 있습니다.
```python
mem_con.execute(&quot;SHOW DATABASES&quot;).df()</code></pre><p><img src="https://velog.velcdn.com/images/newnew_daddy/post/01b28b63-bd3b-4cc4-9d5c-fe76747d6cf1/image.png" alt=""></p>
<h3 id="3-ducklake에-테이블-저장">3) DuckLake에 테이블 저장</h3>
<ul>
<li>먼저 위에서 생성한 <code>my_ducklake</code> 데이터베이스를 선택한 후 <code>CREATE TABLE ~ AS</code> 쿼리를 통해 기존에 있는 테이블을 기반으로 새로운 테이블을 생성합니다.</li>
</ul>
<pre><code class="language-python">## my_ducklake 데이터베이스 사용 설정
mem_con.execute(&quot;USE my_ducklake&quot;)

## 새로운 테이블 생성
create_table = &quot;&quot;&quot;
CREATE TABLE duck_emp
AS 
SELECT * FROM read_parquet(&#39;gs://hyunsoo_de_bucket/dataset/emp.parquet&#39;)
&quot;&quot;&quot;

mem_con.execute(create_table)</code></pre>
<ul>
<li><p>테이블 생성 후 등록한 <code>DATA_PATH</code> 경로를 확인해보면 <code>main/&lt;테이블 이름&gt;</code> 디렉토리가 새롭게 생성되면서 데이터가 <code>parquet</code> 파일 형태로 저장이 된 것을 확인할 수 있습니다.
(‼️ 위 저장되는 디렉토리 구조는 MacOS 기준이며, 실습 결과 Windows와 Linux 플랫폼에서는 디렉토리 구조가 약간 달랐습니다. ‼️)
<img src="https://velog.velcdn.com/images/newnew_daddy/post/dec8536d-fede-4d69-bb95-86b3a35d94ff/image.png" alt=""></p>
</li>
<li><p>또한 메타 테이블의 <code>ducklake_table</code>을 조회해보면 아래와 같이 방금 생성한 <code>duck_emp</code> 테이블을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/0c749260-1b22-434a-bd9f-2e59eec6ee72/image.png" alt=""></p>
</li>
</ul>
<h2 id="🔹-5-버전-확인-및-time-travel">🔹 5. 버전 확인 및 Time Travel</h2>
<h3 id="1-테이블-수정">1) 테이블 수정</h3>
<ul>
<li>Time Travel 쿼리를 작성하기에 앞서 데이터를 수정해보도록 하겠습니다.<pre><code class="language-python">## 1) 새로운 컬럼 추가(new_col)
mem_con.execute(&quot;ALTER TABLE duck_emp ADD COLUMN new_col INT DEFAULT 0&quot;)
</code></pre>
</li>
</ul>
<h2 id="2-update">2) UPDATE</h2>
<p>mem_con.execute(&quot;UPDATE duck_emp SET new_col = 1 WHERE Salary &gt;= 100000&quot;)</p>
<h2 id="3-delete">3) DELETE</h2>
<p>mem_con.execute(&quot;DELETE FROM duck_emp WHERE Salary &lt; 50000&quot;)</p>
<pre><code>- 테이블 수정 발생시 연결된 객체 저장소에도 parquet 파일이 추가적으로 생성되어 아래와 같이 확인이 가능합니다.
![](https://velog.velcdn.com/images/newnew_daddy/post/da14f06b-4666-4cc5-bf35-01e35f64f5e4/image.png)

### 2) 테이블 버전 확인
- 테이블 수정 내용 및 버전 확인을 위해서는 메타테이블의 `ducklake_snapshot`, `ducklake_snapshot_changes` 테이블을 조회해보면 됩니다.
#### ducklake_snapshot 테이블
![](https://velog.velcdn.com/images/newnew_daddy/post/7cf66df8-a9c0-4d64-b7c7-884561f7c939/image.png)

#### ducklake_snapshot_changes 테이블
![](https://velog.velcdn.com/images/newnew_daddy/post/54fc4b72-acd1-4579-a358-abf968f9d8dd/image.png)


- 또한 아래 파이썬 코드를 통해서도 확인이 가능합니다.
```python
mem_con.execute(&quot;FROM my_ducklake.snapshots()&quot;).df()</code></pre><p><img src="https://velog.velcdn.com/images/newnew_daddy/post/e5b7b9c4-3d26-456e-8081-89a91fad960e/image.png" alt=""></p>
<h3 id="3-time-travel-쿼리">3) Time Travel 쿼리</h3>
<ul>
<li><a href="https://ducklake.select/docs/stable/duckdb/usage/time_travel">Time Travel 관련 공식 문서 내용</a></li>
</ul>
<h4 id="버전-기반-time-travel">버전 기반 Time Travel</h4>
<ul>
<li>테이블 스냅샷 기준 3버전 상태로 되돌립니다.<pre><code class="language-python">mem_con.execute(&quot;SELECT * FROM duck_emp AT (VERSION =&gt; 3)&quot;).df()</code></pre>
</li>
</ul>
<h4 id="시간-기반-time-travel">시간 기반 Time Travel</h4>
<ul>
<li><code>2025-07-04 05:43:49.371+00</code> → 이 시점에서의 테이블 데이터를 보여줍니다.<pre><code class="language-python">mem_con.execute(&quot;SELECT * FROM duck_emp AT (TIMESTAMP =&gt; &#39;2025-07-04 05:43:49.371+00&#39;)&quot;).df()</code></pre>
</li>
</ul>
<h2 id="🔹-6-outro">🔹 6. OUTRO</h2>
<ul>
<li>기존에는 Iceberg나 Delta Lake 같은 Open Table Format(OTF)을 사용할 때, 그 구조에 큰 의문을 가지지 않았습니다. 파일 단위로 저장된 데이터가 ACID 트랜잭션, 롤백, 그리고 Time Travel까지 지원된다는 점이 그저 신기하게 느껴졌죠. 동일한 경로에 메타데이터가 파일 형식으로 저장된다는 것도 &quot;이력 관리를 위해서는 당연히 이렇게 저장이 되어야지!&quot; 하고 넘어갔습니다.</li>
<li>하지만 DuckLake를 접하고 기존 도구들과의 차이점에 대해 알게 되면서 이런 생각이 들었습니다.</li>
<li><em><em>&quot;메타데이터를 꼭 파일로만 저장해야 할까? 왜 DB로 관리할 수 있다고 생각을 못했을까?&quot;</em>*</em>
DuckLake처럼 메타데이터를 데이터베이스에 저장하면, 이력 조회나 관리 작업이 훨씬 직관적이고 유연해질 수 있겠다는 가능성을 느꼈습니다.</li>
<li>물론 아직까지는 기존 OTF만큼 성숙한 생태계를 갖췄다고 보긴 어렵습니다. 하지만 DuckDB의 빠른 발전 속도와 함께 DuckLake 역시 함께 성장한다면, 향후에는 널리 사용되는 차세대 테이블 포맷으로 자리 잡을 수도 있겠다는 생각을 하게 되었습니다.</li>
</ul>
<h2 id="🔹-7-참고-자료">🔹 7. 참고 자료</h2>
<ul>
<li><a href="https://ducklake.select/">DuckLake 공식 문서</a></li>
<li><a href="https://duckdb.org/docs/stable/core_extensions/ducklake.html">DuckDB DuckLake Extensions</a></li>
<li><a href="https://youtu.be/hrTjvvwhHEQ?si=I45TLxk2JXt1rE0x">Understanding DuckLake: A Table Format with a Modern Architecture</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python UV를 활용한 효율적이고 빠른 패키지 관리 방법]]></title>
            <link>https://velog.io/@newnew_daddy/PYTHON11</link>
            <guid>https://velog.io/@newnew_daddy/PYTHON11</guid>
            <pubDate>Wed, 25 Jun 2025 05:00:48 GMT</pubDate>
            <description><![CDATA[<h2 id="🔹-1-uv란">🔹 1. UV란?</h2>
<p><a href="https://docs.astral.sh/uv/">UV 공식 문서</a></p>
<ul>
<li><p>Rust로 작성된 고성능 Python 패키지 및 프로젝트 관리 도구로 pip, virtualenv, poetry 등을 대체할 수 있는 빠르고 효율적인 패키지 관리 도구입니다.</p>
</li>
<li><p>흔히 사용되는 pip이나 poetry 같은 다른 도구들에 비해 패키지 설치가 엄청나게 빠른 것이 특징입니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/92d36fb2-e600-415c-a1fb-4bd0bcdf1ae0/image.png" alt=""></p>
</li>
<li><p><code>uv init</code>만으로 가상환경이 자동으로 구성되어, 별도의 Python 가상환경 설치 없이 즉시 개발을 시작할 수 있습니다.</p>
</li>
<li><p><code>pyproject.toml</code>, <code>uv.lock</code> 파일을 활용해 의존성 명시 및 재현 가능한 환경 구성 보장합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/32bfc284-826c-4d64-987b-a8c2f8299fd1/image.png" alt=""></p>
</li>
</ul>
<h2 id="🔹-2-uv-설치-및-초기-설정">🔹 2. UV 설치 및 초기 설정</h2>
<h3 id="1-설치">1) 설치</h3>
<ul>
<li>pip 설치가 되어 있지 않은 경우<pre><code class="language-shell"># macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
</code></pre>
</li>
</ul>
<h1 id="windows-powershell">Windows (PowerShell)</h1>
<p>powershell -ExecutionPolicy ByPass -c &quot;irm <a href="https://astral.sh/uv/install.ps1">https://astral.sh/uv/install.ps1</a> | iex&quot;</p>
<pre><code>- pip 설치가 되어 있는 경우
```shell
pip install uv</code></pre><ul>
<li>설치 확인<pre><code class="language-shell">uv --version
</code></pre>
</li>
</ul>
<p>uv 0.7.14 (e7f596711 2025-06-23)</p>
<pre><code>### 2) 작업 디렉토리 초기 설정
Git에서 `git init` 명령어를 통해 디렉토리를 Git으로 관리하겠다고 선언하듯, uv 역시 `uv init` 명령어를 통해 작업 디렉토리를 초기화해야 uv 기능을 사용할 수 있습니다.
즉, uv를 사용하기 위해서는 최초에 `uv init` 명령어로 해당 디렉토리를 설정해주는 과정이 필요합니다.
```shell
# 작업 디렉토리 설정 초기화
uv init

# 디렉토리 생성 + 초기화
uv init my-project</code></pre><p><img src="https://velog.velcdn.com/images/newnew_daddy/post/c66f3226-905a-407d-bc4a-3dbcbb76e826/image.png" alt=""></p>
<ul>
<li><strong><code>.python-version</code></strong>: 프로젝트에서 사용할 Python 버전이 명시된 파일입니다.</li>
<li><strong><code>main.py</code></strong>: 실행 또는 테스트를 위한 기본 Python 스크립트 파일입니다.</li>
<li><strong><code>pyproject.toml</code></strong>: 프로젝트의 메타데이터와 빌드 설정, 의존성 등을 정의하는 표준 구성 파일입니다.</li>
<li><strong><code>README.md</code></strong>: 프로젝트의 목적과 사용법 등을 설명하는 문서 파일입니다.</li>
</ul>
<h2 id="🔹-3-패키지-관리-및-실행">🔹 3. 패키지 관리 및 실행</h2>
<h3 id="1-패키지-설치--삭제">1) 패키지 설치 &amp; 삭제</h3>
<ul>
<li>기존에 <code>pip install</code> 명령어를 통해 설치했던 파이썬 라이브러리들 역시 uv에서는 uv 문법에 맞게 설치 및 삭제를 진행해주어야 합니다.<pre><code class="language-shell"># 패키지 추가
uv add [패키지 명]
uv add requests pandas faker
</code></pre>
</li>
</ul>
<h1 id="패키지-제거">패키지 제거</h1>
<p>uv remove [패키지 명]</p>
<pre><code>- `requirements.txt` 파일에 정리된 라이브러리들을 한 번에 설치하고자 한다면 아래 명령어로 가능합니다.
```shell
uv pip install -r requirements.txt</code></pre><ul>
<li>추가된 패키지는 <code>pyproject.toml</code> 파일의 <code>dependencies</code> 항목에 저장되며, 삭제시엔 해당 항목에서 사라집니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/94b7cfd6-326e-4663-9933-1fb853d13ef3/image.png" alt=""></li>
</ul>
<h3 id="2-python-파일-실행">2) python 파일 실행</h3>
<ul>
<li>uv로 패키지 관리가 되는 디렉토리의 python 파일을 실행할 경우 <code>uv run</code> 명령어를 활용하는 것이 가장 좋습니다.</li>
<li>물론 python 명령어를 통해 실행하는 것도 가능하지만 터미널에서 가상환경을 따로 활성화시킨 후 실행을 해야합니다.</li>
</ul>
<table>
<thead>
<tr>
<th>실행 방식</th>
<th>가상환경 적용</th>
<th>권장 여부</th>
</tr>
</thead>
<tbody><tr>
<td><code>uv run main.py</code></td>
<td>✅ 자동 적용</td>
<td>✅ 추천</td>
</tr>
<tr>
<td><code>source .venv/bin/activate</code><br><code>&amp;&amp; python main.py</code></td>
<td>✅ 수동 적용</td>
<td>⭕ 가능하나 불편</td>
</tr>
<tr>
<td><code>python main.py</code></td>
<td>❌ 기본 환경 사용</td>
<td>❌ 권장하지 않음</td>
</tr>
</tbody></table>
<h4 id="uv를-통한-실행">uv를 통한 실행</h4>
<p><img src="https://velog.velcdn.com/images/newnew_daddy/post/17f86344-0ca1-4143-80c3-561447d35cec/image.png" alt=""></p>
<h4 id="python을-통한-실행-가상환경-활성화--실행">python을 통한 실행 (가상환경 활성화 + 실행)</h4>
<ul>
<li>uv로 생성한 가상환경 활성화시 <code>uv init</code> 때 명시한 작업 디렉토리 이름으로 가상환경이 활성화됩니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/ffa5570a-7a09-4dc6-bf2c-c222f09e0d0e/image.png" alt=""></li>
</ul>
<h2 id="🔹-4-python-버전-변경">🔹 4. Python 버전 변경</h2>
<ul>
<li>pip을 사용하는 경우, 중간에 가상환경의 Python 버전을 변경하려면 <code>기존 가상환경 삭제 → 새로운 버전의 가상환경 생성 → 라이브러리 설치 → python 파일 실행</code> 이 순서로 작업이 진행되어야 합니다. </li>
<li>특히 기존 가상환경이 무겁거나 복잡하게 구성된 경우, 삭제하거나 새로 설정하는 데 시간이 오래 걸려 Python 버전 변경 작업이 매우 번거롭고 귀찮게 느껴질 수 있습니다.</li>
<li>하지만 uv를 사용하는 경우, 작업 디렉토리 내의 <code>.python-version</code>과 <code>pyproject.toml</code> 파일만 수정하면, 기존 디렉토리를 그대로 유지한 채 가상환경의 Python 버전만 간편하게 변경할 수 있습니다.<h4 id="python-version">.python-version</h4>
<img src="https://velog.velcdn.com/images/newnew_daddy/post/8bf90b8d-f90a-4c64-b00e-557514385126/image.png" alt=""></li>
</ul>
<h4 id="pyprojecttoml">pyproject.toml</h4>
<p><img src="https://velog.velcdn.com/images/newnew_daddy/post/c9b0213f-9c9d-4108-b882-c9cc14f7b7b7/image.png" alt=""></p>
<ul>
<li>위 두 파일에서 Python 버전 변경 후 <code>uv run main.py</code> 명령어를 치게되면 아래의 과정을 자동으로 수행하게 됩니다.<ul>
<li><code>기존 가상환경 제거 → 새로운 가상환경 생성 → 패키지 설치 → 실행</code></li>
</ul>
</li>
</ul>
<h2 id="🔹-5-pip-vs-uv">🔹 5. pip vs uv</h2>
<p><img src="https://velog.velcdn.com/images/newnew_daddy/post/3e90f3de-39ef-4550-86b6-e525d2768fa7/image.png" alt=""></p>
<h2 id="🔹-6-outro">🔹 6. OUTRO</h2>
<ul>
<li>기존에는 대부분의 Python 프로젝트에서 <code>pip</code>과 <code>venv</code> 조합을 사용했고, 지금도 많이 사용되고 있습니다. 하지만 느린 속도, Python 버전 변경의 어려움 등 <code>pip</code>이 가지는 명확한 한계점이 있었습니다.</li>
<li><code>uv</code>는 이를 보완하고 Python의 패키지 관리를 더욱 효율적이고 빠르게 할 수 있도록 개발된 도구입니다. 특히 Python 버전 관리나 가상환경 설정, 의존성 설치 속도 등에서 <code>uv</code>는 매우 강력한 이점을 제공합니다. </li>
<li><code>uv</code>는 독자적인 디렉토리 구조를 가지고 있지만 기존 <code>pip</code>로 설치된 디렉토리와도 높은 호환성을 유지해, 기존 프로젝트를 완전히 갈아엎지 않아도 자연스럽게 도입할 수 있습니다. 이처럼 부담 없이 도입이 가능하면서도 더 나은 개발 환경을 제공한다는 점에서 매우 매력적입니다.</li>
<li>최근에는 MCP를 비롯해 다양한 오픈소스 프로젝트와 팀들이 <code>uv</code>를 표준 툴로 채택해가고 있는 추세입니다.</li>
<li>이제 Python 프로젝트를 시작하거나 기존 환경을 개선할 계획이 있다면, <code>uv</code>를 적극 고려해보세요. 빠르고 가벼우며, 무엇보다 개발자의 귀찮음을 줄여주는 도구입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AIRFLOW 3.0, 클라우드 객체 스토리지를 Xcom 저장소로 설정하기(xcom backend)]]></title>
            <link>https://velog.io/@newnew_daddy/AIRFLOW04</link>
            <guid>https://velog.io/@newnew_daddy/AIRFLOW04</guid>
            <pubDate>Thu, 12 Jun 2025 06:45:41 GMT</pubDate>
            <description><![CDATA[<h2 id="0-intro">0. INTRO</h2>
<ul>
<li>Airflow의 <strong>XCom(Cross Communication)</strong>은 DAG 내의 태스크 간 데이터를 공유하기 위한 기능입니다. Airflow 시스템이 실행되면 기본 메타데이터 저장소인 PostgreSQL에 다양한 메타 정보가 저장되며, XCom 데이터 역시 이 PostgreSQL 데이터베이스에 기록됩니다.</li>
<li>하지만 XCom은 어디까지나 <strong>경량 메시지 전달 용도</strong>로 설계된 기능이기 때문에, 사용 시 몇 가지 <strong>권장사항</strong>을 반드시 고려해야 합니다.<ul>
<li><code>str</code>, <code>int</code>, <code>float</code>, <code>bool</code>, <code>list</code>, <code>dict</code> 등의 <strong>단순하고 직렬화 가능한 데이터 타입</strong>만 저장</li>
<li><strong>수 MB 이하 수준의 작고 가벼운 데이터</strong>만 저장</li>
<li>실제 데이터 전달보다는 <strong>상태 또는 신호 전달 용도로 사용</strong> (경로, 처리 여부, 간단한 통계치 등)</li>
</ul>
</li>
<li>Python 함수 간 데이터 전달은 <strong>같은 세션의 메모리를 공유</strong>하기 때문에 <code>pandas.DataFrame</code>, <code>numpy.ndarray</code>, 이미지 등 <strong>대용량 복합 객체</strong>도 무리 없이 전달이 가능합니다. 하지만 XCom은 메타DB를 공유하므로, 대용량 데이터를 그대로 저장하면 <strong>DB 성능과 안정성에 악영향</strong>을 줄 수 있습니다.</li>
<li>데이터 파이프라인에서는 함수 간 DataFrame 객체 전달이 자주 발생하는데 이런 경우는 어떻게 해야 할까요? Airflow는 이를 위해 Xcom 데이터를 PostgreSQL이 아닌 AWS S3, Google GCS, Azure Blob Storage 같은 클라우드 객체 저장소에 저장할 수 있는 <code>XComObjectStorageBackend</code> 설정을 제공합니다.</li>
<li>이번 글에서는 Airflow 3.0 버전에서 Xcom을 AWS S3와 Google Cloud Storage로 설정하는 방법을 각각 다뤄보도록 하겠습니다. (Airflow 3.0 버전을 Docker Compose로 설치 후 진행하므로 설치 관련해서는 <a href="https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html">공식 문서</a> 참고 바랍니다.)<h2 id="1-airflow-30-설정-파일-확인">1. Airflow 3.0 설정 파일 확인</h2>
</li>
<li>Airflow에는 각종 세팅들을 저장해놓는 <code>airflow.cfg</code> 파일이 있습니다. 2.10 버전까지는 Airflow의 도커 컨테이너 내부 <code>/opt/airflow/airflow.cfg</code> 경로에 존재했었는데 3 버전이 되면서 <code>/opt/airflow/config/airflow.cfg</code> 경로로 위치가 바뀌었습니다.</li>
<li>사용자의 로컬 디렉토리와 볼륨 매핑이 되어있는 경로로 위치가 바뀌어 예전이라면 도커 컨테이너 내부에 들어가서 수정해야 했었던 것이 이제 로컬 디렉토리에서 바로 수정이 가능해졌습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/c0d6282c-d5f3-4dbd-b0b6-673da4aa9d7f/image.png" alt=""></li>
<li><code>XComObjectStorageBackend</code> 설정시 <code>airflow.cfg</code> 파일을 수정하고 <code>docker compose restart</code> 명령으로 시스템 재시작을 해주어야 제대로 적용이 됩니다.</li>
</ul>
<h2 id="2-connections-설정">2. Connections 설정</h2>
<ul>
<li>Airflow UI에서 AWS와 GCP에 대한 connections 설정을 해주도록 하겠습니다. (<code>Admin &gt; Connections</code>)<h3 id="1-aws-connection">1) AWS Connection</h3>
</li>
<li><code>Connection Type</code> : aws</li>
<li><code>AWS Access Key ID</code> : IAM USER로 발급받은 Key의 ID 부분</li>
<li><code>AWS Secret Access Key</code> : IAM USER로 발급받은 Key의 Secret 부분</li>
<li><code>Extra Fields</code> : {&quot;region_name&quot; : &quot;ap-northeast-2&quot;}</li>
<li><img src="https://velog.velcdn.com/images/newnew_daddy/post/a56616ea-fcb7-4de9-bb59-eed8cdf879b8/image.png" alt=""></li>
</ul>
<h3 id="2-gcp-connection">2) GCP Connection</h3>
<ul>
<li><code>Connection Type</code> : google_cloud_platform</li>
<li><code>Project Id</code> : GCP 프로젝트 ID</li>
<li><code>Keyfile Path</code> : 서비스 계정 JSON 키 경로
<img src="https://velog.velcdn.com/images/newnew_daddy/post/3eb46746-67ff-48a3-a234-72ed89e08f31/image.png" alt=""></li>
</ul>
<h2 id="3-dag-작성">3. DAG 작성</h2>
<ul>
<li>실습 DAG의 경우 아래 두 가지 Task로 구성되어 있습니다.<ul>
<li><code>1번 Task(upload_df)</code> &gt; 컬럼 4개, 행 10개짜리 샘플 DataFrame을 생성하여 이를 Xcom에 저장</li>
<li><code>2번 Task(read_df)</code> &gt; Xcom에 저장된 DataFrame을 읽어와 로그에 출력<pre><code class="language-python">import pendulum
from datetime import timedelta
import datetime
from airflow.sdk import DAG, task, get_current_context, ObjectStoragePath, Param
from airflow.operators.python import PythonOperator
import pandas as pd
</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="샘플-데이터프레임-생성">샘플 데이터프레임 생성</h2>
<p>def create_sample_df():
    data = {
        &#39;name&#39;: [&#39;John&#39;, &#39;Emma&#39;, &#39;Michael&#39;, &#39;Sarah&#39;, &#39;David&#39;, &#39;Lisa&#39;, &#39;James&#39;, &#39;Emily&#39;, &#39;Daniel&#39;, &#39;Anna&#39;],
        &#39;age&#39;: [25, 32, 28, 35, 41, 29, 33, 27, 38, 31],
        &#39;city&#39;: [&#39;Seoul&#39;, &#39;Boston&#39;, &#39;London&#39;, &#39;Paris&#39;, &#39;Tokyo&#39;, &#39;Sydney&#39;, &#39;Berlin&#39;, &#39;Toronto&#39;, &#39;Rome&#39;, &#39;Madrid&#39;],
        &#39;score&#39;: [85, 92, 78, 95, 88, 83, 91, 87, 94, 89]
    }
    return pd.DataFrame(data)</p>
<p>with DAG(
        dag_id=&quot;xcom_backend_dag&quot;,
        schedule=&quot;@once&quot;,
        start_date=pendulum.datetime(2025, 6, 1, tz=&quot;Asia/Seoul&quot;),
        catchup=False
) as dag:
    ## DataFrame을 Xcom에 저장
    @task(task_id=&#39;upload_df&#39;)
    def upload_df():
        df = create_sample_df()
        return df</p>
<pre><code>## 저장된 Xcom을 가져와 로그에 출력
@task(task_id=&#39;read_df&#39;)
def read_df():
    context = get_current_context()
    df = context[&#39;task_instance&#39;].xcom_pull(task_ids=&#39;upload_df&#39;)
    print(df.head())
    return df

upload_df() &gt;&gt; read_df()</code></pre><pre><code>
## 4. airflow.cfg 파일 수정 후 적용
### 1) AWS S3
- AWS S3를 Xcom Backend로 설정하고자 하는 경우, `airflow.cfg` 파일의 내용을 아래와 같이 설정하면 됩니다.</code></pre><p>xcom_backend = airflow.providers.common.io.xcom.backend.XComObjectStorageBackend</p>
<h1 id="예시-xcom_objectstorage_path--s3aws_connectionairflow-hyunsoo-bucketxcom">예시) xcom_objectstorage_path = s3://aws_connection@airflow-hyunsoo-bucket/xcom</h1>
<p>xcom_objectstorage_path = s3://[AWS Connection 명]@[S3 버킷 경로]</p>
<p>xcom_objectstorage_threshold = 0</p>
<p>xcom_objectstorage_compression = gzip</p>
<pre><code>- `xcom_objectstorage_path` : Xcom 데이터가 저장될 객체 스토리지 경로 지정
- `xcom_objectstorage_threshold` : Xcom 데이터가 객체 스토리지에 저장되지 위한 최소값 지정
  - `threshold 값 미만` : 메타DB에 저장
  - `threshold 값 이상` : 객체 스토리지에 저장
  - `-1` : 항상 메타DB에 저장
  - `0` : 항상 객체 스토리지에 저장
- `xcom_objectstorage_compression` : Xcom 데이터의 압축 방식을 지정
### 2) Google Cloud Storage
- 위 설정과 비슷하며 `xcom_objectstorage_path`만 GCS 경로로 바꾸면 됩니다.</code></pre><p>xcom_backend = airflow.providers.common.io.xcom.backend.XComObjectStorageBackend</p>
<h1 id="예시-xcom_objectstorage_path--gsmy_gcp_conncodeit_sprintxcom">예시) xcom_objectstorage_path = gs://my_gcp_conn@codeit_sprint/xcom</h1>
<p>xcom_objectstorage_path = gs://[GCP Connection 명]@[GCS 버킷 경로]</p>
<p>xcom_objectstorage_threshold = 0</p>
<p>xcom_objectstorage_compression = gzip</p>
<p>```</p>
<h3 id="3-설정-내용-적용">3) 설정 내용 적용</h3>
<ul>
<li><code>airflow.cfg</code> 파일의 위 4가지 Key값을 설정하였다면 시스템 재시작이 필요합니다. docker compose로 설치되어 있다면 <code>docker compose restart</code> 명령어를 통해 재시작해주세요.</li>
<li>재시작 후 위의 DAG를 실행하면 아래와 같이 객체 저장소에 <code>버킷 경로/[DAG ID]/[작업 시간]/[TASK ID]/[Xcom 데이터]</code>와 같이 디렉토리 구조가 생성되며 Task에서 return한 데이터가 저장됩니다.<ul>
<li>AWS S3
<img src="https://velog.velcdn.com/images/newnew_daddy/post/fad9f713-861e-494b-9a8b-3ef1e09db3da/image.png" alt=""></li>
<li>Google Cloud Storage
<img src="https://velog.velcdn.com/images/newnew_daddy/post/b4d52d4e-1e13-4e4a-b936-6b6e88b89deb/image.png" alt=""></li>
</ul>
</li>
</ul>
<h2 id="5-참고-자료">5. 참고 자료</h2>
<ul>
<li><a href="https://kyeongseo.tistory.com/entry/S3%EC%97%90-XCom-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0-XComObjectStorageBackend-%EC%84%A4%EC%A0%95-%EA%B0%80%EC%9D%B4%EB%93%9C">S3에 XCom 저장하기: XComObjectStorageBackend 설정 가이드</a></li>
<li><a href="https://www.astronomer.io/docs/learn/xcom-backend-tutorial/?tab=gcp#step-4-configure-your-custom-xcom-backend">Set up a custom XCom backend using object storage</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AIRFLOW] Airflow의 Setup과 Teardown에 대해 알아보자!]]></title>
            <link>https://velog.io/@newnew_daddy/AIRFLOW03</link>
            <guid>https://velog.io/@newnew_daddy/AIRFLOW03</guid>
            <pubDate>Wed, 11 Jun 2025 05:34:00 GMT</pubDate>
            <description><![CDATA[<h2 id="o-intro">O. INTRO</h2>
<ul>
<li>이번 블로그 글에서 다뤄볼 내용은 Airflow Dag 내의 Task 실행시 선행 작업과 후행 작업을 설정할 수 있는 <code>setup</code> 과 <code>teardown</code> 기능입니다.</li>
<li>예를 들어 아래와 같은 흐름의 파이프라인을 구성해보겠습니다.
1) MySQL 데이터베이스에 연결
2) 테이블 데이터 조회 후 집계 테이블로 변환하여 저장
3) 데이터베이스 연결 종료
여기서 실질적인 작업을 하는 Task는 2번이고 1,3번은 2번 Task 앞뒤에 따라서 실행되는 작업입니다.</li>
<li>이런 경우, 1~3까지의 로직을 각각의 Task로 구성해도 상관없지만 <code>setup</code> 과 <code>teardown</code> 기능을 활용한다면 2번 작업에 대한 선행, 후행 작업으로 선언하는 것이 이후에 이어질 파이프라인 작업을 선언할 때도 훨씬 직관적으며, 이후 Clear 같은 Task 단위의 재실행 작업시에도 깔끔하게 실행이 가능해집니다.</li>
</ul>
<h2 id="1-기본-작업-구성">1. 기본 작업 구성</h2>
<pre><code class="language-python">import pendulum
from airflow import DAG
from airflow.sdk import DAG, task

with DAG(
        dag_id=&quot;setup_n_teardown&quot;,
        schedule=&quot;@once&quot;,
        start_date=pendulum.datetime(2025, 6, 1, tz=&quot;Asia/Seoul&quot;),
        catchup=False,
) as dag:
    @task(task_id=&#39;pre_task&#39;)
    def pre_task():
        print(&#39;INITIALIZE&#39;)

    @task(task_id=&#39;real_task&#39;)
    def real_task():
        MSG = &quot;&quot;&quot;
        HELLO WORLD!

        THIS IS REAL PIPELINE TASK!
        &quot;&quot;&quot;
        print(MSG)

    @task(task_id=&#39;post_task&#39;)
    def post_task():
        print(&#39;FINALIZE&#39;)

pre_task().as_setup() &gt;&gt; real_task() &gt;&gt; post_task().as_teardown()</code></pre>
<blockquote>
<ul>
<li><code>pre_task</code> : <code>real_task</code> 이전에 선행되는 작업</li>
</ul>
</blockquote>
<ul>
<li><p><code>real_task</code> : DAG의 실질적인 작업</p>
</li>
<li><p><code>post_task</code> : <code>real_task</code>가 끝나고 후행되는 작업</p>
</li>
<li><p>위와 같이 <code>real_task</code>에 대한 선행 작업은 <code>as_setup()</code>, 후행 작업은 <code>as_teardown()</code> 메소드를 통해 각각 설정이 가능합니다.</p>
</li>
<li><p>Airflow UI의 그래프를 확인해보면 일반적인 Task와는 다르게 <code>setup</code>이 설정된 Task는 <code>↗</code>, <code>teardown</code>이 설정된 작업은 <code>↘</code> 모양의 화살표가 붙어있는 것을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/784553da-dbd1-4c1a-8c4a-bcdaf432e098/image.png" alt=""></p>
</li>
<li><p>또한 위의 경우, <code>real_task</code>만을 Clear로 재실행하면 해당 Task의 선후행 작업이 모두 같이 실행되는 것을 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/de7e9885-96e6-47b6-b569-f4125872dc98/image.png" alt=""></p>
</li>
</ul>
<h2 id="2-task-group-작업-구성">2. Task Group 작업 구성</h2>
<ul>
<li>이번엔 Task Group으로 묶인 Task 내에서 <code>setup</code> 과 <code>teardown</code> 선언시 어떻게 작업이 처리되는지 보도록 하겠습니다.<pre><code class="language-python">import pendulum
from airflow.sdk import DAG, task, task_group

</code></pre>
</li>
</ul>
<p>with DAG(
        dag_id=&quot;setup_n_teardown_tg&quot;,
        schedule=&quot;@once&quot;,
        start_date=pendulum.datetime(2025, 6, 1, tz=&quot;Asia/Seoul&quot;),
        catchup=False,
) as dag:</p>
<pre><code>@task_group(group_id=&#39;first_group&#39;)
def first_group():
    @task(task_id=&#39;pre_task&#39;)
    def pre_task():
        print(&#39;INITIALIZE&#39;)

    @task(task_id=&#39;real_task&#39;)
    def real_task():
        MSG = &quot;&quot;&quot;
        HELLO WORLD!
        THIS IS REAL PIPELINE TASK!
        &quot;&quot;&quot;
        print(MSG)

    @task(task_id=&#39;post_task&#39;)
    def post_task():
        print(&#39;FINALIZE&#39;)

    pre_task().as_setup() &gt;&gt; real_task() &gt;&gt; post_task().as_teardown()

@task(task_id=&#39;task_outer_tg&#39;)
def task_outer_tg():
    print(&#39;outer tg&#39;)

@task_group(group_id=&#39;second_group&#39;)
def second_group():
    @task(task_id=&#39;pre_task&#39;)
    def pre_task():
        print(&#39;INITIALIZE&#39;)

    @task(task_id=&#39;real_task&#39;)
    def real_task():
        MSG = &quot;&quot;&quot;
        HELLO WORLD!
        THIS IS REAL PIPELINE TASK!
        &quot;&quot;&quot;
        print(MSG)

    @task(task_id=&#39;post_task&#39;)
    def post_task():
        print(&#39;FINALIZE&#39;)

    pre_task().as_setup() &gt;&gt; real_task() &gt;&gt; post_task().as_teardown()

first_group() &gt;&gt; task_outer_tg() &gt;&gt; second_group()</code></pre><p>```</p>
<ul>
<li><code>first_group</code>과 <code>second_group</code> 내에 각각 선후행 작업이 있는 Task가 존재합니다. 이 두 Task Group 사이에는 <code>task_outer_tg</code>라는 Task가 연결되어 있습니다.</li>
<li>아래 그래프에서 볼 수 있듯 Task Group 내에서 <code>setup</code>과 <code>teardown</code>이 설정된 경우, 이후에 따라오는 작업(<code>task_outer_tg</code>)은 이전 작업의 <code>real_task</code>와 곧바로 연결되는 것을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/af10a275-1917-476e-aac4-4e88c1b3534d/image.png" alt=""></li>
<li>즉, 그룹이 이후 다른 Task과 연결될 때, 그 Task는 그룹내에 있는 선후행 작업이 아닌 실제 작업과 연결되어 실행되고, 후행 작업은 그와 독립적으로 실행되는 것을 확인할 수 있습니다.</li>
</ul>
<h2 id="3-teardown-작업-옵션-설정">3. teardown 작업 옵션 설정</h2>
<ul>
<li>1에서 다룬 예제와 같이 선후행 작업이 설정되어 있을 때, <img src="https://velog.velcdn.com/images/newnew_daddy/post/8f5736ec-b603-4e78-9b41-6ea6ec59f4ad/image.png" alt="">
선행 작업, 본작업, 후행 작업 각각 Fail시 어떻게 진행되는지 보도록 하겠습니다.</li>
</ul>
<h4 id="1-선행-작업-실패시">1) 선행 작업 실패시</h4>
<ul>
<li><code>선행 작업 실패 → 본작업 실패 + 전체 DAG 실패 → 후행 작업은 성공 or 실패</code></li>
<li>선행 작업이 실패하면 본작업 역시 제대로 진행되지 못하여 전체 DAG가 FAILED 됩니다. 하지만 후행 작업은 성공될 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/93917dda-c55a-4756-9d4e-2cf4b0b2fdb0/image.png" alt=""></li>
</ul>
<h4 id="2-본작업-실패시">2) 본작업 실패시</h4>
<ul>
<li><code>선행 작업 성공 → 본작업 실패 + 전체 DAG 실패 → 후행 작업 성공 or 실패</code></li>
<li>본작업이 실패하면 전체 DAG는 FAILED되지만 후행 작업은 성공될 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/6b1dcc86-6bbe-4837-b8e8-662ea6657d4f/image.png" alt=""></li>
</ul>
<h4 id="3-후행-작업-실패시">3) 후행 작업 실패시</h4>
<ul>
<li><p><code>선행 작업 성공 → 본작업 성공 + 전체 DAG 성공 → 후행 작업 성공</code></p>
</li>
<li><p>후행 작업의 경우 성공, 실패 여부와 상관 없이 본작업이 성공하게 되면 전체 DAG는 SUCCESS로 표시됩니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/f422458b-e4ea-4983-8db9-4d40c319acfc/image.png" alt=""></p>
</li>
<li><p>하지만, 아래 설정을 통해 후행 작업 실패시 전체 DAG가 FAILED 되도록 설정할 수도 있습니다.</p>
<ul>
<li><code>as_teardown(on_failure_fail_dagrun=True)</code>
<img src="https://velog.velcdn.com/images/newnew_daddy/post/26801c9b-3ea9-414c-931c-ba6271b938d8/image.png" alt=""></li>
</ul>
</li>
</ul>
<h2 id="4-참고-자료">4. 참고 자료</h2>
<ul>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/howto/setup-and-teardown.html">Setup and Teardown 공식 문서</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GCP] BigQuery Dataform으로 증분(Incremental) 데이터 처리하기]]></title>
            <link>https://velog.io/@newnew_daddy/GCP07</link>
            <guid>https://velog.io/@newnew_daddy/GCP07</guid>
            <pubDate>Mon, 09 Jun 2025 08:16:33 GMT</pubDate>
            <description><![CDATA[<h2 id="0-intro">0. INTRO</h2>
<ul>
<li>GCS, S3와 같은 클라우드 객체 스토리지를 Data Lake로 활용하는 경우, 지속적으로 유입되는 데이터를 디렉토리 단위로 파티셔닝하여 저장하는 방식이 일반적입니다.
예를 들어, 일 단위로 적재되는 데이터는 <code>yyyy=/mm=/dd=</code> 형식으로, 시간 단위 배치 데이터는 <code>yyyy=/mm=/dd=/hh=</code> 와 같은 구조로 저장할 수 있습니다. 이러한 디렉토리 구조는 데이터의 배치 주기와 일치하도록 설계되어 효율적인 관리와 조회를 가능하게 합니다.</li>
<li>이처럼 주기적으로 누적되는 데이터를 분석용 테이블로 적재할 때는, 증가된 데이터만 APPEND 방식으로 적재하는 방법이 리소스와 처리 시간 면에서 훨씬 효율적입니다. 단, 이 방식은 원천 데이터에 삭제나 수정 없이 오직 데이터가 추가만 되는 경우에 안정적으로 작동할 수 있으며, 로그나 이벤트 기록처럼 이력성 데이터를 처리할 때 특히 유용한 전략입니다.</li>
<li>이번 글에서는 GCS(Google Cloud Storage)에 일 단위로 적재되는 데이터를 중분(Incremental) 방식으로 테이블에 적재하는 방법에 대해 다뤄보도록 하겠습니다.</li>
</ul>
<h2 id="1-gcs-적재-데이터-구조">1. GCS 적재 데이터 구조</h2>
<ul>
<li><p>GCS에 적재되는 데이터의 형상은 아래와 같습니다.</p>
</li>
<li><p>컬럼 4개로 이루어진 테이블이며, 그 중 <code>proc_ymd</code> 컬럼이 적재된 날짜(년월일)를 알려줍니다.</p>
</li>
<li><p>적재 기간은 2023-04-01 ~ 2023-04-30 입니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/498388f0-7717-4f09-b8ea-2c92d1ebff15/image.png" alt=""></p>
</li>
<li><p>GCS에는 <code>yyyy=2023/mm=04/dd=01/data.parquet</code> 이런 형식으로 일단위로 디렉토리가 나뉘어 적재됩니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/9a96aced-9ef4-48cb-9b5d-7943973682b0/image.png" alt=""></p>
</li>
</ul>
<h2 id="2-external-table-활용하기">2. EXTERNAL TABLE 활용하기</h2>
<h3 id="1-external-table이란">1) External Table이란?</h3>
<ul>
<li>External Table은 BigQuery에서 외부 저장소(GCS, Google Drive 등)에 있는 데이터를 직접 참조하여 쿼리할 수 있는 가상 테이블입니다. 즉, 데이터를 BigQuery 테이블로 만들어 저장해놓지 않고도, 마치 BigQuery 내부 테이블처럼 SQL을 사용하여 조회할 수 있도록 해주는 기능입니다.</li>
<li>따라서 GCS에 parquet 파일 형식(CSV, JSON 등 다양한 형식 지원)으로 데이터가 적재되는 경우, 해당 디렉토리를 BigQuery External Table로 등록해놓으면 새롭게 유입된 데이터를 바로 조회해볼 수 있습니다.</li>
<li>하지만 BigQuery 내부 테이블에 비해서는 조회 속도가 느리며, 파티셔닝/클러스터링 기능 활용이 불가능하여 성능 최적화에 한계가 있습니다.</li>
</ul>
<h3 id="2-사용-방법">2) 사용 방법</h3>
<ul>
<li>GCS에 적재되어 있는 데이터를 External Table로 등록하려면 아래와 같은 형식으로 SQL 쿼리를 실행하면 됩니다.<pre><code class="language-sql">CREATE OR REPLACE EXTERNAL TABLE `프로젝트ID.데이터셋.테이블명`
OPTIONS (
  format = &#39;PARQUET&#39;, -- CSV, JSON 등
  uris = [데이터에 대한 gsutil 주소]
  );</code></pre>
</li>
<li>아래 코드는 GCS의 <code>gs://hyunsoo_sprint_bucket/dataform_data/gcp_part_parquet/</code> 경로 이하에 <code>yyyy=/mm=/dd=</code> 형식으로 일 단위 적재되는 데이터들을 <code>dataform</code> 데이터셋에 <code>demo_source</code> 라는 이름의 테이블로 등록하는 쿼리입니다.<pre><code class="language-sql">  CREATE OR REPLACE EXTERNAL TABLE `codeit-hyunsoo.dataform.demo_source`
  OPTIONS (
    format = &#39;PARQUET&#39;,
    uris = [&#39;gs://hyunsoo_sprint_bucket/dataform_data/gcp_part_parquet/yyyy=2023/mm=04/*&#39;]
    );</code></pre>
</li>
</ul>
<h3 id="3-등록-확인">3) 등록 확인</h3>
<ul>
<li>위의 SQL 쿼리문을 통해 External Table 등록이 완료되면 BigQuery 콘솔 화면에서 확인이 가능합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/4cbc152f-1095-486c-ad68-3e04cb741e72/image.png" alt=""></li>
<li>이렇게 등록된 External Table의 경우 연결된 GCS 경로에 특정 날짜의 추가 데이터가 적재되면, 해당 적재분에 대한 조회가 바로 가능합니다.</li>
</ul>
<h2 id="3-dataform으로-증분-데이터-처리하기">3. DataForm으로 증분 데이터 처리하기</h2>
<ul>
<li>위와 같이 External Table을 생성하면, BigQuery는 GCS에 저장된 데이터를 직접 참조하기 때문에 별도의 증분 처리 없이도 최신 데이터를 즉시 조회할 수 있습니다. 그러나 이 데이터를 기반으로 다른 테이블을 생성하거나 가공된 결과를 저장해야 하는 경우, 해당 결과 테이블은 증분 처리가 필요합니다.</li>
<li>적재되는 데이터의 양이 많지 않다면 매번 전체 데이터를 다시 적재하는 full-refresh 방식도 고려할 수 있습니다. 하지만, 불필요한 계산과 비용을 줄이고 쿼리 성능을 최적화하려면 신규 데이터만 처리하는 증분 방식을 선택하는 것이 훨씬 효율적입니다.</li>
<li>이러한 증분 처리를 SQL 기반의 선언형 방식으로 손쉽게 구현할 수 있도록 도와주는 도구가 바로 Dataform입니다. Dataform을 활용하면 BigQuery 위에서 안정적이고 확장성 있는 증분 데이터 파이프라인을 간편하게 구성할 수 있습니다.</li>
</ul>
<h3 id="1-증분-데이터-처리-코드-작성">1) 증분 데이터 처리 코드 작성</h3>
<p><code>definition/demo_increment.sqlx</code></p>
<pre><code class="language-sql">config {
  type: &quot;incremental&quot;,
  name: &quot;demo_increment&quot;
}

SELECT
  *
FROM codeit-hyunsoo.dataform.demo_source

${when(incremental(), `WHERE proc_ymd &gt; (SELECT MAX(proc_ymd) FROM ${self()})`)}</code></pre>
<ul>
<li><p><code>type: &quot;incremental&quot;</code>
→ 이 모델은 증분 처리 대상 테이블임을 선언합니다.</p>
</li>
<li><p><code>name: &quot;demo_increment&quot;</code>
→ 생성될 테이블의 이름은 demo_increment 입니다.</p>
</li>
<li><p><code>SELECT 쿼리문</code>
→ <code>demo_source</code> 테이블의 전체 컬럼을 조회합니다.</p>
</li>
<li><p><code>when(incremental() 쿼리문</code>
→ 증분 실행 조건으로, <code>demo_source</code> 테이블의 <code>proc_ymd</code> 컬럼에 새로운 날짜 데이터가 들어오면 해당 데이터를 <code>demo_increment</code>로 삽입합니다.</p>
</li>
</ul>
<blockquote>
<p>즉, 위의 스크립트는 <code>demo_source</code> 테이블에 새로운 데이터가 들어오면 추가된 데이터만 <code>demo_increment</code> 테이블에 적재합니다.</p>
</blockquote>
<h3 id="2-실행-확인-확인">2) 실행 확인 확인</h3>
<ul>
<li><code>type: &quot;incremental&quot; 조건의 증분 처리 스크립트의 경우</code>Run` 버튼을 누르면 아래와 같이 증분 조건일 경우와 아닐 경우, 두 가지 경우에 대해 실행 테스트를 진행해볼 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/76715f83-8d9d-4c00-8f28-8e822865cc02/image.png" alt=""></li>
</ul>
<h2 id="4-outro">4. OUTRO</h2>
<blockquote>
<p><a href="https://cloud.google.com/dataform/docs/incremental-tables?hl=ko">Dataform Incremental Table 구성 공식 문서</a></p>
</blockquote>
<ul>
<li><p>이번 글에서는 GCS에 일 단위로 데이터가 파일 형식으로 적재되는 경우, BigQuery에서 이를 효율적으로 분석하기 위한 두 가지 증분 처리 방식에 대해 살펴보았습니다.</p>
<ul>
<li><strong>External Table :</strong> 데이터를 BigQuery로 로드하지 않고도 GCS의 최신 데이터를 직접 조회할 수 있는 방식으로, 별도의 적재 작업 없이 빠르게 분석을 시작할 수 있다는 장점이 있습니다. 단, 대용량 데이터나 정교한 가공이 필요한 경우에는 성능이나 비용 측면에서 주의가 필요합니다.</li>
<li><strong>Dataform incremental 모델 :</strong> 데이터가 쌓이는 구조를 반영하여, 새로운 데이터만을 선택적으로 BigQuery 테이블에 추가하는 방식입니다. 스케줄링과 병합 조건을 선언형으로 관리할 수 있어, 지속적인 데이터 파이프라인 운영에 적합합니다.</li>
</ul>
</li>
<li><p>데이터 엔지니어링에서 증분 처리는 비용 효율성과 쿼리 성능을 동시에 잡기 위한 핵심 전략입니다. 데이터의 속성, 쿼리 목적, 사용 빈도 등을 고려하여 상황에 맞는 방식을 선택하는 것이 중요합니다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GCP] BigQuery Dataform과 Github 연동하기( + 심화 내용)]]></title>
            <link>https://velog.io/@newnew_daddy/GCP06</link>
            <guid>https://velog.io/@newnew_daddy/GCP06</guid>
            <pubDate>Thu, 05 Jun 2025 08:46:53 GMT</pubDate>
            <description><![CDATA[<h2 id="🔹-0-intro">🔹 0. INTRO</h2>
<ul>
<li>앞선 글(<a href="https://velog.io/@newnew_daddy/GCP05">BigQuery Dataform으로 빅쿼리 데이터 플로우 자동화하기!</a>)에서는 BigQuery에서 제공해주는 Dataform 이라는 서비스에 대해 알아보고 간단한 실습까지 진행해 보았습니다.
빅쿼리 데이터를 기반으로 자동화된 파이프라인을 만들어준다는 것 외에도 Dataform에는 매력적인 기능들이 많이 있는데요, 대표적인 것이 Github Repo와의 연동입니다.</li>
<li>Dataform에 작성한 코드를 Github Repo와 연결하여 UI에서 바로 commit이나 push 작업이 가능하며, 특정 브랜치에 push하는 것도 가능합니다. </li>
<li>이번 글에서는 Github Repo와 연동하는 방법에 대해 알아보고, <code>.sqlx</code>의 문법에 대해 추가적으로 다뤄보도록 하겠습니다.</li>
</ul>
<h2 id="🔹-1-github와-연동">🔹 1. Github와 연동</h2>
<h3 id="🔸-1-github-repo-생성-및-토큰-발행">🔸 1) Github Repo 생성 및 토큰 발행</h3>
<ul>
<li><p><code>dataform-practice</code> 라는 이름의 깃허브 Repo를 생성합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/99043c90-3462-439e-a08b-1d37afbd0617/image.png" alt=""></p>
</li>
<li><p><code>Settings &gt; Developer settings &gt; Personal access tokens &gt; Fine-grained tokens</code> 메뉴로 가서 토큰을 발행합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/78e36d27-30e3-4699-85fd-b305cffc3553/image.png" alt=""></p>
</li>
<li><p>토큰 발행시 아래와 같이 설정하고 발행합니다.</p>
<ul>
<li><p>토큰 이름 : dataform-prac-token</p>
</li>
<li><p>Repository access : <code>Only select repositories</code> 선택 후 위에서 생성한 Repo를 선택합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/e5d3469b-a90f-47a1-b658-99f8eff547b8/image.png" alt=""></p>
</li>
<li><p>Repository permissions : <code>Contents</code> 선택 후 <code>Read and write</code>로 변경
<img src="https://velog.velcdn.com/images/newnew_daddy/post/81f6ada1-d707-47a0-a345-dedc82a93d12/image.png" alt=""></p>
</li>
</ul>
</li>
<li><p>발행이 완료되면 <code>github_pat_xxxxx</code> 와 같은 토큰값을 얻을 수 있습니다.</p>
</li>
</ul>
<h3 id="🔸-2-secret-manager에-토큰-등록">🔸 2) Secret Manager에 토큰 등록</h3>
<ul>
<li><p>위에서 발행한 토큰을 비밀이나 민감정보를 관리해주는 GCP 서비스인 Secret Manager에 등록해줘야 Dataform에서 github와 연동시 사용이 가능합니다.</p>
</li>
<li><p><code>Secret Manager &gt; +보안 비밀 만들기</code>를 선택합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/98ac0de9-ba9f-4b60-895d-35ec7f2c83f4/image.png" alt=""></p>
</li>
<li><p>이름을 정한 후 <code>보안 비밀 값</code> 항목에 위에서 발급받은 토큰값을 붙여넣고 보안 비밀을 생성합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/b239845a-0391-4d16-8a1d-8ef78b7e279f/image.png" alt="">
<img src="https://velog.velcdn.com/images/newnew_daddy/post/d99d869d-fa9c-441e-ba3d-ec80e8feaf34/image.png" alt=""></p>
</li>
</ul>
<h3 id="🔸-3-dataform-저장소-생성-및-연동">🔸 3) Dataform 저장소 생성 및 연동</h3>
<ul>
<li>위에서 생성한 github Repo와 연동될 Dataform 저장소를 생성합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/314c90ed-e0e8-4728-bdff-f3ac961e0bb6/image.png" alt=""></li>
<li>생성한 저장소로 들어가 <code>Settings &gt; Git과 연결</code> 을 누르면 github Repo와 연동할 수 있는 설정창이 나옵니다.
아래 사진과 같이 github Repo 저장소 URL, 기본 브랜치 이름, 그리고 위에서 등록한 secret manager 키를 선택해주고 <code>링크</code>를 누르면 연동이 완료됩니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/6baa3e85-7778-4335-83f7-2bfe53e61ccf/image.png" alt=""></li>
</ul>
<h3 id="🔸-4-작업-공간workspace-생성-후-초기화">🔸 4) 작업 공간(workspace) 생성 후 초기화</h3>
<ul>
<li>연동된 Dataform 저장소에 작업 공간을 생성합니다. 이 작업 공간은 이후 Github Repo와 연동될 때 브랜치명으로 사용됩니다. 즉, <code>dataform-github-repo</code>라는 저장소 내에 작업 공간을 A, B, C 이렇게 3개를 만들었다면, 이들 각각이 연동된 github Repo의 브랜치가 되는 것입니다.</li>
<li><code>tutorial</code>이라는 작업 공간을 생성하고 <code>작업공간 초기화</code> 버튼을 클릭하면 초기 세팅 파일들이 생성됩니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/4ac606e6-b9dd-4a2f-bef7-585f7e70b8b0/image.png" alt=""></li>
</ul>
<h3 id="🔸-5-github-repo로-push">🔸 5) Github Repo로 push</h3>
<ul>
<li><code>tutorial</code> 작업 공간에 생성된 초기 파일들을 연동된 Github Repo로 push 할 수 있습니다. UI에서 버튼 몇 번만 클릭하면 간단하고 직관적으로 commit 후 push가 완료됩니다.</li>
<li><code>tutorial</code> 브랜치로 push하는 것이 디폴트지만 설정을 통해 <code>master(혹은 main)</code> 브랜치로 바로 push하는 것도 가능합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/48866f31-9161-4fbe-a475-21fafb1443d9/image.png" alt=""></li>
<li>Github에서 확인해보면 앞에서 생성한 <code>dataform-practice</code> Repo의 <code>tutorial</code> 브랜치에 해당 파일들이 잘 올라가있는 것을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/d3c069f8-60d4-4dce-9c4d-ff3fbd78daf3/image.png" alt=""></li>
<li>이후 Dataform 작업시 파일이나 디렉토리에 변경이 일어나는 경우, 위와 같이 UI를 통해 github 저장소로 바로 push를 할 수 있어 코드 형상관리가 아주 편리해진다는 장점이 있습니다.</li>
</ul>
<h2 id="🔹-2-config-설정">🔹 2. config 설정</h2>
<ul>
<li><code>.sqlx</code> 파일의 문법이 일반 SQL과 다른점은 상단에 정의되는 config 때문입니다. 이 설정을 통해 쿼리의 의존성, 실행 순서, 파티셔닝, 태그 지정, 품질 검사 등 파이프라인 작업에 필요한 다양한 세부 사항들을 정의할 수 있어, 단순한 SQL 쿼리를 넘어 복잡한 데이터 워크플로우를 구성이 가능하게 해줍니다.</li>
</ul>
<h3 id="🔸-1-기본-설정-오버라이딩">🔸 1) 기본 설정 오버라이딩</h3>
<ul>
<li><code>workflow_settings.yaml</code> 파일에 작성했던 기본 설정값들을 파일 내에서 재정의 할 수 있습니다. 설정을 따로 하지 않으면, 기본 설정값에 명시된 사항에 따라 테이블이 생성됩니다.<pre><code class="language-json">config {
type: &quot;table&quot;,
description : &quot;This is Sample Table&quot;
database: &quot;my-gcp-project-id&quot;,
schema: &quot;education&quot;,
name: &quot;sample_tbl&quot;,
columns: {
    user_id: {
      description: &quot;Unique identifier for the user&quot;
    event_timestamp: {
      description: &quot;Timestamp of the battle event&quot;
    }
}</code></pre>
</li>
<li><code>type</code> : 파일이 어떤 방식으로 실행되고 결과를 생성할지 유형을 정의<ul>
<li><code>table</code> : 물리적 테이블 생성</li>
<li><code>incremental</code> : 테이블에 증분 방식으로 데이터를 추가하거나 업데이트</li>
<li><code>view</code> : 논리적 뷰 생성</li>
<li><code>operations</code> : 테이블이나 뷰를 생성하지 않고, 정의된 SQL 작업을 실행</li>
<li><code>assertion</code> : 데이터 품질 검사를 정의(조건 불충족시 작업 실패)</li>
</ul>
</li>
<li><code>description</code> : 생성될 테이블에 대한 설명 추가</li>
<li><code>database</code> : 작업이 저장될 project ID 설정</li>
<li><code>schema</code> : 저장될 빅쿼리 dataset 이름 설정</li>
<li><code>name</code> : 저장될 테이블 이름 설정(없다면 <code>sqlx</code> 파일명으로 저장)</li>
<li><code>columns</code> : 테이블의 컬럼에 대한 메타데이터(설명, 태그 등)를 정의</li>
</ul>
<h3 id="🔸-2-추가-설정">🔸 2) 추가 설정</h3>
<pre><code class="language-json">config {
  type: &quot;table&quot;,
  database: &quot;my-gcp-project-id&quot;,
  schema: &quot;education&quot;,
  name: &quot;sample_tbl&quot;,
  columns: {
      user_id: {
        description: &quot;Unique identifier for the user&quot;
      },
      event_timestamp: {
        description: &quot;Timestamp of the battle event&quot;
      }
    },
  disabled: true,
  hasOutput: true,
  dependencies: [&quot;raw_battle_data&quot;, &quot;user_profile&quot;]
  tags : [&#39;dev&#39;]
    }</code></pre>
<ul>
<li><code>disabled</code> : 해당 파일에서의 테이블 생성 비활성화 여부</li>
<li><code>hasOutput</code> : <code>type: &quot;operations&quot;</code>인 SQLX 파일에서 출력 테이블을 생성하도록 지정</li>
<li><code>dependencies</code> : SQLX 파일 간의 명시적 의존성을 정의</li>
<li><code>tags</code> : 특정 작업을 선택적으로 실행하거나 그룹화할 때 사용되는 태그를 설정</li>
</ul>
<h2 id="🔹-3-작업-전후-쿼리-설정">🔹 3. 작업 전후 쿼리 설정</h2>
<ul>
<li><code>pre_operations</code>, <code>post_operations</code> 설정을 통해 파일 본문의 SQL 쿼리 작업이 실행되기 전과 후에 실행할 SQL 문을 정의할 수 있습니다.<ul>
<li><code>pre_operations</code> : 테이블 생성 전에 실행할 SQL 문을 정의</li>
<li><code>post_operations</code> : 테이블 생성 후에 실행할 SQL 문을 정의</li>
</ul>
</li>
</ul>
<pre><code>config {
  type: &quot;table&quot;,
  description : &quot;This is Sample Table&quot;
  database: &quot;my-gcp-project-id&quot;,
  schema: &quot;education&quot;,
  name: &quot;sample_tbl&quot;,
  columns: {
      user_id: {
        description: &quot;Unique identifier for the user&quot;
      event_timestamp: {
        description: &quot;Timestamp of the battle event&quot;
      }
}

-- 본문의 SQL 쿼리 실행 전 작업 정의
pre_operations {
    CREATE OR REPLACE TABLE sprintda05-hyunsoo.dataform.pre AS SELECT * FROM codeit-hyunsoo.dataform.source
  }

-- 본문의 SQL 쿼리 실행 이후 작업 정의
post_operations {
    CREATE OR REPLACE TABLE sprintda05-hyunsoo.dataform.post AS SELECT * FROM codeit-hyunsoo.dataform.source
}

-- SQL 쿼리
SELECT 1 AS number;</code></pre><h2 id="🔹-4-assertions-설정">🔹 4. Assertions 설정</h2>
<ul>
<li><a href="https://cloud.google.com/dataform/docs/assertions?hl=ko">Assertions 공식 문서</a></li>
<li>작업시 생성되는 테이블의 품질 검사를 정의하는 항목으로, 지정된 조건이 충족되지 않으면 워크플로우 실행이 실패하게 됩니다.</li>
<li>정의에 따른 쿼리의 결과값이 0이면 통과, 한 행 이상을 반환하게되면 실패로 간주됩니다.</li>
<li>일반적으로 아래와 같은 내용들을 체크할 때 많이 활용됩니다.<ul>
<li><code>데이터 무결성</code> : 필수 필드에 null 값이 없는지 확인.</li>
<li><code>중복 데이터</code> : 고유 키(unique key)에 중복이 없는지 확인.</li>
<li><code>데이터 범위 검증</code> : 값이 예상 범위 내에 있는지 확인.</li>
<li><code>데이터 최신성 확인</code> : 데이터가 특정 시간 내에 적재되었는지 확인.</li>
</ul>
</li>
<li>Assertions 작업이 실패하게 되면 빅쿼리에 <code>dataform_assertions</code>이라는 dataset이 만들어지고 작업 파일명과 동일한 이름의 VIEW가 생성되어 실패한 행 확인이 가능합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/a2986d72-98be-4aa4-b7dc-1e7051d4b007/image.png" alt=""></li>
</ul>
<h3 id="🔸-1-수동-assertions-정의">🔸 1) 수동 Assertions 정의</h3>
<ul>
<li><p>수동 Assertions는 독립적인 SQLX 파일에 정의되며, 특정 테이블의 품질을 테스트하는 데 사용됩니다.</p>
</li>
<li><p>예를 들어 <code>demo_table</code> 이라는 테이블의 <code>status</code> 행에 null값이 있는지 확인하는 Assertions를 작성할 수 있습니다.</p>
</li>
<li><p><code>definitions/single_assert.sqlx</code></p>
<pre><code class="language-sql">config { type: &quot;assertion&quot; }

SELECT status
FROM dataform.demo_table
WHERE status IS NULL</code></pre>
<ul>
<li><code>config { type: &quot;assertion&quot; }</code> : 이 파일이 Assertion임을 지정.</li>
<li>쿼리는 status 컬럼에서 null인 행을 찾음.</li>
<li>쿼리가 0행을 반환하면 Assertion 성공, 1행 이상 반환하면 실패.</li>
<li>실패 시 <code>dataform_assertions.single_assert</code> 뷰를 BigQuery에 생성하여 실패한 행을 확인할 수 있도록 함.</li>
</ul>
</li>
<li><p>수동으로 Assertions를 정의하는 경우, 워크플로우 그래프를 보게되면 아래와 같이 하나의 작업만 보이는 것을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/c96e5f4c-0224-4057-a322-b7ba89929fb6/image.png" alt=""></p>
</li>
</ul>
<h3 id="🔸-2-테이블-내-assertions-정의">🔸 2) 테이블 내 Assertions 정의</h3>
<ul>
<li><p>테이블 정의 내에서 Assertions를 설정하면, 해당 테이블 생성 후 자동으로 데이터 품질 테스트를 수행합니다. config 블록에 assertions 속성을 추가하여 정의합니다.</p>
</li>
<li><p><code>definitions/multi_assert.sqlx</code></p>
<pre><code class="language-sql">config {
  type: &quot;table&quot;,
  assertions: {
    uniqueKey: [&quot;user_id&quot;],  // user_id가 고유해야 함
    nonNull: [&quot;user_id&quot;, &quot;customer_id&quot;],  // user_id, customer_id가 null이 아니어야 함
    rowConditions: [
      &quot;create_date &gt; &#39;2019-01-01&#39;&quot;,  // create_date 조건
      &quot;email LIKE &#39;%@%.%&#39;&quot;  // 이메일 형식 검증
    ]
  }
}

SELECT
  user_id,
  customer_id,
  create_date,
  email
FROM dataform.assertion_table</code></pre>
<ul>
<li><code>uniqueKey</code> : user_id 열에 중복 값이 없어야 함.</li>
<li><code>nonNull</code> : user_id와 customer_id가 null이 아니어야 함.</li>
<li><code>rowConditions</code> : 컬럼의 값들이 지정된 SQL 조건을 만족해야 함</li>
</ul>
</li>
<li><p>아래 정의된 SQL 문을 바탕으로 파일명과 동일한 <code>multi_assert</code> 테이블 생성 후 <code>assertions</code> 항목에 정의된 검증을 실행합니다.</p>
</li>
<li><p><code>assertions</code> 항목에 정의된 내용의 통과 유무는 이후 연결된 작업에 영향을 미치기 때문에 <strong>해당 파일 하나로는 품질 검증에 의미가 크게 없고, 각 검증 절차 이후 연결되는 또 다른 작업 파일이 있을 때 활용성이 높아집니다.</strong></p>
</li>
<li><p>워크플로우 그래프를 확인해보면, 아래와 같이 <code>multi_assert</code> 작업 이후 <code>assertions</code> 검증 항목이 연결되어 보이는 것을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/f5feaa23-602a-4ff2-949f-3db2774351d3/image.png" alt=""></p>
</li>
</ul>
<h2 id="🔹-5-outro">🔹 5. OUTRO</h2>
<ul>
<li>이번 글에서는 Dataform의 저장소를 Github와 연동하고, <code>.sqlx</code> 파일에 정의할 수 있는 config 요소들에 대해 상세히 알아보았습니다. 위의 기능들만 잘 활용해도 BigQuery 테이블을 바탕으로 효율적인 ETL 파이프라인을 구성할 수 있지 않을까 싶습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GCP] BigQuery Dataform으로 빅쿼리 데이터 플로우 자동화하기!]]></title>
            <link>https://velog.io/@newnew_daddy/GCP05</link>
            <guid>https://velog.io/@newnew_daddy/GCP05</guid>
            <pubDate>Tue, 03 Jun 2025 09:36:12 GMT</pubDate>
            <description><![CDATA[<h2 id="🔹-0-intro">🔹 0. INTRO</h2>
<ul>
<li>Apache Airflow, dbt, Step Functions, Databricks Workflow 등 데이터 관련 여러 작업들을 자동화하고 관리해주는 다양한 오케스트레이션 도구들이 있습니다. 그 중 dbt와 유사한 방식으로 BigQuery 환경에서 활용할 수 있는 도구가 있습니다. 바로 Dataform입니다.</li>
<li>Dataform은 Google BigQuery에 저장된 데이터를 변환하고 워크플로우를 관리할 수 있도록 도와주는 도구입니다. BigQuery에 저장된 테이블들을 대상으로 자동화된 데이터 파이프라인 구축이 가능하죠.</li>
<li>SQL 쿼리문에 config가 더해진 SQLX라는 확장된 SQL 문법과 JavaScript를 활용해 데이터 변환 로직을 정의할 수 있으며, 개발 작업공간을 통해 팀원들이 독립적으로 작업하고 변경 사항을 버전 관리(Git)로 관리할 수 있도록 지원합니다.</li>
<li>이번 글에서는 BigQuery 기반의 Dataform을 처음 사용하는 분들을 위해, 기본적인 사용법을 차근차근 소개해보려고 합니다.</li>
</ul>
<hr>
<h2 id="🔹-1-기본-세팅">🔹 1. 기본 세팅</h2>
<h3 id="▪-1-저장소-만들기">▪ 1) 저장소 만들기</h3>
<ul>
<li>Dataform에서 저장소(Repository)는 가장 상위 계층에 위치하며 파이프라인 작업들을 논리적으로 구분하는 단위입니다.</li>
<li>작업 공간(Workspace)은 저장소 내에 위치하며 저장소의 작업에 대해 버전 관리나 분기가 필요할 때 이를 구분하는 데 사용됩니다.</li>
<li>BigQuery Dataform UI에서 <code>+저장소 만들기</code> 버튼을 누르고 저장소 이름과 리전(Region)을 선택하면 생성이 가능합니다.</li>
<li><code>quickstart-repository</code> 라는 이름으로 저장소를 생성하였습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/newnew_daddy/post/d8eeaac7-7f30-44c0-8ae8-ea37f3a6b086/image.png" alt=""></p>
<h3 id="▪-2-서비스-계정-권한-부여">▪ 2) 서비스 계정 권한 부여</h3>
<ul>
<li>저장소 생성이 되면 이어서 Dataform 서비스가 사용할 서비스 계정에 대한 권한 확인 작업이 진행됩니다. 만약 Dataform을 최초로 이용하는 것이라면 생성된 서비스 계정에 대해 최소한 <code>roles/bigquery.user</code> 역할 추가가 필요합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/newnew_daddy/post/e498ab23-1bca-456c-9823-a7e2bdb172f6/image.png" alt=""></p>
<h3 id="▪-3-작업-공간-만들기">▪ 3) 작업 공간 만들기</h3>
<ul>
<li>작업 공간 역시 위에서 만든 저장소에 들어가 <code>+개발 작업공간 만들기</code> 버튼을 눌러 생성이 가능합니다.</li>
<li><code>quickstart-workspace</code>라는 이름으로 작업 공간을 생성하였습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/8664b0fd-abc4-486d-967a-333566078ee6/image.png" alt=""></li>
</ul>
<h3 id="4-작업-공간-초기화">4) 작업 공간 초기화</h3>
<ul>
<li>위에서 생성한 <code>quickstart-workspace</code>에 들어가 <code>작업공간 초기화</code> 버튼을 누르게 되면 Dataform 작업에 필요한 초기 파일들이 생성되게 됩니다.</li>
<li>초기 세팅 파일/디렉토리 설명<ul>
<li><code>definitions/</code> : Dataform 프로젝트의 SQLX 파일과 JavaScript 파일을 저장하는 디렉토리로, 본격적인 데이터 ETL 파이프라인 코드가 작성되는 디렉토리입니다.</li>
<li><code>includes/</code> : 재사용 가능한 JavaScript 함수나 공통 SQL 로직을 저장하는 디렉토리로, 프로젝트 전반에서 참조되는 파일들이 위치하는 디렉토리입니다.
<code>workflow_settings.yaml</code> : Dataform 워크플로의 실행 설정(예: 스케줄, BigQuery 위치, 기본 스키마 등)을 정의하는 구성 파일입니다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/newnew_daddy/post/2aa20f54-ea41-4f59-a2db-0b81f762f972/image.png" alt=""></p>
<hr>
<h2 id="🔹-2-단일-테이블-작업-진행">🔹 2. 단일 테이블 작업 진행</h2>
<h3 id="▪-1-workflow_settingsyaml">▪ 1) workflow_settings.yaml</h3>
<ul>
<li>기본적으로 아래와 같이 5개의 키들이 세팅되어 있고 그 중 중요한 키는 3번째 <code>defaultDataset</code>으로, Dataform에서 작성한 <code>.sqlx</code> 파일내의 쿼리의 결과물이 빅쿼리의 어떤 Dataset에 저장될지를 명시하는 키입니다.<pre><code class="language-yaml">defaultProject: codeit-hyunsoo
defaultLocation: asia-northeast3
defaultDataset: dataform
defaultAssertionDataset: dataform_assertions
dataformCoreVersion: 3.0.0</code></pre>
</li>
<li>workflow_settings.yaml 관련 더 추가적인 내용은 <a href="https://cloud.google.com/dataform/docs/configure-dataform?hl=ko">공식 문서</a>에서 확인할 수 있습니다.</li>
</ul>
<h3 id="▪-2-sqlx-파일-구성">▪ 2) <code>.sqlx</code> 파일 구성</h3>
<ul>
<li><p><code>.sqlx</code> 파일은 상단의 <code>config</code> 설정 부분과 하단의 <code>SQL 쿼리</code> 작성 부분으로 나뉩니다.</p>
</li>
<li><p><strong>🛠 config 설정부</strong></p>
<ul>
<li><code>.sqlx</code> 파일의 최상단에 위치하며, 중괄호 {} 안에 작성됩니다.</li>
<li>이 부분은 해당 파일에서 생성하는 테이블, 뷰, 혹은 선언 등에 대한 메타데이터 및 실행 설정을 정의합니다.</li>
<li>주요 설정 내용<ul>
<li>테이블 이름, 타입(view/table/assertion 등) 지정</li>
<li>파티션/클러스터링 설정</li>
<li>태그, 설명, 라벨 추가</li>
<li>의존성 설정(dependencies)</li>
<li>외부 쿼리 옵션 지정</li>
<li><code>workflow_settings.yaml</code> 내용 오버라이딩</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>📄 SQL 쿼리 작성부</strong></p>
</li>
<li><p>config에 정의한 설정을 기반으로 실제로 실행될 SQL 쿼리문을 작성합니다.</p>
</li>
</ul>
<h3 id="▪-3-sourcesqlx-파일-작성">▪ 3) source.sqlx 파일 작성</h3>
<ul>
<li><p>컬럼이 2개(<code>fruit</code>, <code>count</code>)있는 샘플 테이블을 생성하는 쿼리를 작성합니다.</p>
</li>
<li><p><code>definitions/</code> 디렉토리 아래에 <code>source.sqlx</code> 파일 생성 후 아래 내용을 작성하면 됩니다.</p>
</li>
<li><p>아래 파일은 빅쿼리의 <code>dataform</code> 데이터셋에 <code>source</code>라는 테이블로 쿼리의 내용을 저장합니다. <code>config</code>상에 별도로 명시가 되어있지 않다면, <code>.sqlx</code> 파일의 이름과 동일하게 테이블의 이름이 정해집니다.</p>
<pre><code class="language-sql">config {
  type: &quot;table&quot;, // SQL을 통해 생성될 테이블의 종류 (table/view)
  description: &quot;Description of the table&quot;, // 쿼리에 대한 설명
}

SELECT
  &quot;apples&quot; AS fruit,
  3 AS count
UNION ALL
SELECT
  &quot;oranges&quot; AS fruit,
  5 AS count
UNION ALL
SELECT
  &quot;pears&quot; AS fruit,
  1 AS count
UNION ALL
SELECT
  &quot;bananas&quot; AS fruit,
  0 AS count</code></pre>
</li>
</ul>
<h3 id="▪-4-작업-실행-및-확인">▪ 4) 작업 실행 및 확인</h3>
<ul>
<li><p>쿼리 작성이 완료되면 자동으로 해당 <code>.sqlx</code> 파일의 문법을 체크하여 틀린 부분이 없는지 보여줍니다. config나 SQL 문법상 오류가 없다면 아래 사진과 같이 초록색 체크 표시가 나옵니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/757a182a-1a0d-4c3e-b6b1-27e0da1c3b2f/image.png" alt=""></p>
</li>
<li><p>해당 쿼리를 실행하려면 <code>실행 시작 &gt; 작업 실행</code> 버튼을 누른 후 실행할 작업을 선택하여 시작할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/817570c8-d25b-4b6a-844b-1973acf9bd7c/image.png" alt=""></p>
</li>
<li><p><code>Compiled graph</code> 탭으로 가면 작성한 <code>.sqlx</code> 파일의 워크플로우를 시각적으로 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/882c8f5a-fb32-4547-bb01-09a63370f92e/image.png" alt=""></p>
</li>
<li><p>실행이 정상적으로 완료되면 기본 설정과 같이 <code>dataform.source</code> 테이블이 생성되어 빅쿼리에서 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/5a700c17-09a8-4e4f-ab99-724ad86efca1/image.png" alt=""></p>
</li>
</ul>
<hr>
<h2 id="🔹-3-여러-테이블-작업-진행">🔹 3. 여러 테이블 작업 진행</h2>
<ul>
<li>이번에는 위에 작성한 <code>source.sqlx</code> 파일로 생성된 테이블을 대상으로 집계를 하는 <code>aggregation.sqlx</code> 파일을 추가로 생성하여 2개의 <code>.sqlx</code> 파일이 연동되어 작업되는 파이프라인을 구성해볼 것입니다.</li>
</ul>
<h3 id="▪-1-aggregationsqlx-파일-작성">▪ 1) aggregation.sqlx 파일 작성</h3>
<ul>
<li><p><code>source</code> 테이블의 <code>count</code> 컬럼 값을 SUM 하는 쿼리입니다.</p>
</li>
<li><p><code>dataform</code> 데이터셋에     파일명과 동일한 <code>aggregation</code>이라는 이름의 VIEW로 저장되도록 config를 구성하였습니다.</p>
</li>
<li><p><code>${ref(&quot;source&quot;)}</code> 코드를 통해 같은 디렉토리 내에서 생성되는 테이블을 참조할 수 있습니다. 참조 정의가 되면 파일의 실행 순서가 자연스럽게 정해집니다.</p>
<pre><code class="language-sql">config {
  type: &quot;view&quot;, // SQL을 통해 생성될 테이블의 종류 (table/view)
  description: &quot;Aggregation of source table&quot;, // 쿼리에 대한 설명
}

SELECT
    SUM(count) AS fruit_cnt
FROM ${ref(&quot;source&quot;)}</code></pre>
</li>
<li><p><code>Compiled graph</code> 탭을 확인하면 작업 순서에 맞게 UI로 표시가 되는 것을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/44e7035f-7c80-4b24-8a26-1e456d2f70fa/image.png" alt=""></p>
<h3 id="▪-2-작업-실행-및-확인">▪ 2) 작업 실행 및 확인</h3>
</li>
<li><p><code>실행 시작 &gt; 작업 실행</code> 버튼을 누른 후 실행할 작업(2개)을 선택하여 작업을 실행합니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/30d64b21-a3e2-4da6-9a72-7f669bd4ab81/image.png" alt=""></p>
</li>
<li><p>실행이 완료되면 BigQuery 스튜디오에서 생성된 테이블을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/5d96358d-23da-4449-9f9f-98ee94373493/image.png" alt=""></p>
</li>
</ul>
<h2 id="🔹-4-outro">🔹 4. OUTRO</h2>
<ul>
<li>지금까지 Dataform의 기본 개념부터 실제 사용법까지, 간단한 실습을 통해 함께 살펴보았습니다. 처음에는 다소 생소하게 느껴질 수 있지만, 익숙해지면 SQL 기반으로 깔끔하고 체계적인 데이터 파이프라인을 구축할 수 있다는 점에서 매우 매력적인 도구입니다. 특히 dbt를 사용해본 경험이 있다면, Dataform의 구조와 사용 방식에 훨씬 빠르게 익숙해질 수 있을 것입니다.</li>
<li>Dataform 서비스 자체는 무료이며 <code>.sqlx</code> 파일을 통한 빅쿼리 엔진 사용에 대한 비용만 발생합니다. Github와의 호환성, 시각적인 워크플로우 관리 기능 등도 지원되어, BigQuery를 데이터 웨어하우스로 사용 중이라면 충분히 활용해볼 만한 자동화 도구라고 생각됩니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[🌊 Delta Lake 입문자를 위한 가이드 - 실전편(Part 2. delta-spark 라이브러리 활용)]]></title>
            <link>https://velog.io/@newnew_daddy/data09</link>
            <guid>https://velog.io/@newnew_daddy/data09</guid>
            <pubDate>Tue, 01 Apr 2025 06:48:12 GMT</pubDate>
            <description><![CDATA[<ul>
<li>Delta Lake 이론 - <a href="https://velog.io/@newnew_daddy/data07">🌊 Delta Lake 입문자를 위한 가이드 - 이론편</a></li>
<li>Delta Lake 실전 Part 1 - <a href="https://velog.io/@newnew_daddy/data08">🌊 Delta Lake 입문자를 위한 가이드 - 실전편(Part 1. 로컬 환경)</a></li>
<li>Delta Lake 실전 Part 2 - <a href="https://velog.io/@newnew_daddy/data09">🌊 Delta Lake 입문자를 위한 가이드 - 실전편(Part 2. delta-spark 라이브러리 활용)</a></li>
</ul>
<hr>
<h2 id="0-intro">0. INTRO</h2>
<ul>
<li>앞선 글 <a href="https://velog.io/@newnew_daddy/data08">🌊 Delta Lake 입문자를 위한 가이드 - 실전편(Part 1. 로컬 환경)</a>에서는 Pyspark Docker Container 환경에서 Pyspark를 활용하여 delta 유형의 파일들을 생성하고 다뤄보는 실습을 진행하였습니다.</li>
<li>이번 Part 2 실습에서는 세부 내용은 유사하지만, Delta 형식의 데이터를 Spark로 보다 간편하게 다룰 수 있도록 도와주는 <code>delta-spark</code> 라이브러리를 활용해보는 내용을 담았습니다.</li>
<li>실습 환경의 경우 <code>hyunsoolee0506/pyspark-cloud:3.5.1</code> 이미지로 컨테이너를 생성하시는 것을 권장드립니다만 이번 실습의 경우는 Google Colab에서 진행해도 무방합니다.</li>
<li>실습에서는 아래 <code>delta_data.zip</code>파일 안에 있는 두 가지 CSV 파일 데이터를 사용하였습니다.<blockquote>
<p>👉 <a href="https://github.com/user-attachments/files/19412310/delta_data.zip">delta_data.zip</a></p>
</blockquote>
</li>
</ul>
<hr>
<h2 id="1️⃣-기본-환경-세팅">1️⃣ 기본 환경 세팅</h2>
<h3 id="▪-1-docker-container-생성">▪ 1) Docker Container 생성</h3>
<ul>
<li>사용자의 컴퓨터에서 volume으로 사용할 디렉토리와 컨테이너 내부 <code>/workspace/spark</code> 디렉토리가 매핑되도록 설정합니다.<pre><code class="language-shell">docker run -d \
  --name pyspark \
  -p 8888:8888 \
  -p 4040:4040 \
  -v [사용자 디렉토리]:/workspace/spark \
  hyunsoolee0506/pyspark-cloud:3.5.1</code></pre>
</li>
<li>위 명령어 실행 후 <code>8888</code> 포트로 접속하면 juypter lab 개발 환경에 접속할 수 있습니다.<h3 id="▪-2-라이브러리-설치">▪ 2) 라이브러리 설치</h3>
</li>
<li>실습에 필요한 라이브러리들을 설치합니다. <code>hyunsoolee0506/pyspark-cloud:3.5.1</code> 이미지에는 이미 설치되어 있지만 colab의 경우 아래 코드 실행을 통해 라이브러리들을 설치해야 합니다.<pre><code class="language-shell">pip install pyspark==3.5.1 delta-spark==3.2.0 pyarrow findspark</code></pre>
</li>
</ul>
<h3 id="▪-3-pyspark-delta-lake-환경-설정">▪ 3) Pyspark delta lake 환경 설정</h3>
<ul>
<li><a href="https://docs.delta.io/latest/quick-start.html#set-up-apache-spark-with-delta-lake">https://docs.delta.io/latest/quick-start.html#set-up-apache-spark-with-delta-lake</a></li>
<li>pyspark에서 delta lake를 사용하기 위해서 SparkSession 생성시 관련 extension들에 대한 설정을 합니다.<pre><code class="language-python">from delta import *
from pyspark.sql import SparkSession
import pyspark.sql.functions as F
</code></pre>
</li>
</ul>
<p>builder = SparkSession.builder.appName(&quot;DeltaLakeLocal&quot;) <br>    .enableHiveSupport() <br>    .config(&quot;spark.sql.extensions&quot;, &quot;io.delta.sql.DeltaSparkSessionExtension&quot;) <br>    .config(&quot;spark.sql.catalog.spark_catalog&quot;, &quot;org.apache.spark.sql.delta.catalog.DeltaCatalog&quot;)</p>
<p>spark = configure_spark_with_delta_pip(builder).getOrCreate()</p>
<pre><code>---
## 2️⃣ delta 형식 테이블 생성
### ▪ 1) 데이터베이스 생성
```python
spark.sql(&quot;CREATE DATABASE IF NOT EXISTS deltalake_db&quot;)

spark.sql(&quot;SHOW DATABASES&quot;).show()

---
+------------+
|   namespace|
+------------+
|     default|
|deltalake_db|
+------------+</code></pre><h3 id="▪-2-일반-테이블-생성">▪ 2) 일반 테이블 생성</h3>
<pre><code class="language-python">&quot;&quot;&quot;
테이블 이름 : trainer
스키마 : 
  - id → INT
  - name → STRING
  - age → INT
  - hometown → STRING
  - prefer_type → STRING
  - badge_count → INT
  - level → STRING
&quot;&quot;&quot; 

spark.sql(f&quot;&quot;&quot;
CREATE TABLE IF NOT EXISTS deltalake_db.trainer (
  id INT,
  name STRING,
  age INT,
  hometown STRING,
  prefer_type STRING,
  badge_count INT,
  level STRING
)
USING csv
OPTIONS (
  path &#39;/workspace/spark/deltalake/dataset/trainer_data.csv&#39;,
  header &#39;true&#39;,
  inferSchema &#39;true&#39;,
  delimiter &#39;,&#39;
)
&quot;&quot;&quot;)

spark.sql(&quot;SHOW TABLES FROM deltalake_db&quot;).show()

---
+------------+-------------+-----------+
|   namespace|    tableName|isTemporary|
+------------+-------------+-----------+
|deltalake_db|      trainer|      false|
+------------+-------------+-----------+</code></pre>
<h3 id="▪-3-delta-테이블-생성">▪ 3) delta 테이블 생성</h3>
<p><code>LOCAL_DELTA_PATH</code> 변수에 <code>delta</code> 테이블이 저장될 디렉토리를 저장합니다. 이 디렉토리는 로컬 디렉토리가 될 수도 있고 s3나 GCS 같은 클라우드 스토리지의 경로가 될 수도 있습니다.</p>
<pre><code class="language-python">LOCAL_DELTA_PATH = &#39;/workspace/spark/deltalake/delta_local/trainer_delta/&#39;

query = f&quot;&quot;&quot;
CREATE TABLE IF NOT EXISTS deltalake_db.trainer_delta (
  id INT,
  name STRING,
  age INT,
  hometown STRING,
  prefer_type STRING,
  badge_count INT,
  level STRING
)
USING delta
LOCATION &#39;{LOCAL_DELTA_PATH}&#39;
&quot;&quot;&quot;
spark.sql(query)

spark.sql(&quot;SHOW TABLES FROM deltalake_db&quot;).show()

---
+------------+-------------+-----------+
|   namespace|    tableName|isTemporary|
+------------+-------------+-----------+
|deltalake_db|      trainer|      false|
|deltalake_db|trainer_delta|      false|
+------------+-------------+-----------+</code></pre>
<h3 id="▪-4-delta-테이블에-데이터-삽입">▪ 4) delta 테이블에 데이터 삽입</h3>
<ul>
<li>CSV로 생성한 일반 테이블의 데이터를 <code>delta</code> 테이블에 삽입합니다.<pre><code class="language-python">query = &quot;&quot;&quot;
INSERT INTO deltalake_db.trainer_delta
SELECT * FROM deltalake_db.trainer;
&quot;&quot;&quot;
spark.sql(query)
</code></pre>
</li>
</ul>
<p>spark.sql(&#39;SELECT * FROM deltalake_db.trainer_delta&#39;).show(5)</p>
<hr>
<p>+---+-----------+---+--------+-----------+-----------+------------+
| id|       name|age|hometown|prefer_type|badge_count|       level|
+---+-----------+---+--------+-----------+-----------+------------+
|  1|      Brian| 28|   Seoul|   Electric|          8|      Master|
|  2|    Sabrina| 23|   Busan|      Water|          6|    Advanced|
|  3|      Susan| 18| Gwangju|       Rock|          7|      Expert|
|  4|     Martin| 20| Incheon|      Grass|          5|    Advanced|
|  5|  Gabrielle| 30|   Daegu|     Flying|          6|    Advanced|
+---+-----------+---+--------+-----------+-----------+------------+</p>
<pre><code>- 디렉토리를 확인하면 아래와 같이 `_delta_log` 폴더가 생성된 것을 확인할 수 있습니다.
![](https://velog.velcdn.com/images/newnew_daddy/post/e771ecd0-cc66-4fa0-b429-887ba999dabd/image.png)

---
## 3️⃣ DeltaTable로 데이터 읽기
### ▪ 1) delta 형식인지 확인
- 디렉토리에 저장된 파일이 `delta` 형식인지 확인합니다.
- 여기서 파라미터에 들어가는 `spark`는 위에 생성한 SparkSession에 대한 값이 담겨있는 변수입니다.
```python
DeltaTable.isDeltaTable(spark, LOCAL_DELTA_PATH)

---
True</code></pre><h3 id="▪-2-delta-테이블-읽기">▪ 2) delta 테이블 읽기</h3>
<ul>
<li>delta 테이블을 읽은 방식은 두 가지가 있습니다.</li>
</ul>
<p>1) 저장된 테이블 이름으로 읽어오기</p>
<pre><code class="language-python">dt = DeltaTable.forName(spark, &quot;deltalake_db.trainer_delta&quot;)</code></pre>
<p>2) 테이블이 저장된 경로로 읽어오기</p>
<pre><code class="language-python">dt = DeltaTable.forPath(spark, LOCAL_DELTA_PATH)</code></pre>
<h3 id="▪-3-deltatable을-spark-dataframe으로-변환">▪ 3) DeltaTable을 spark dataframe으로 변환</h3>
<ul>
<li>위의 코드로 읽어오게 되면 <code>delta.tables.DeltaTable</code> 타입으로 저장됩니다. 따라서 데이터를 조회하기 위해서는 spark dataframe으로 변환 후 <code>show()</code> 메소드로 조회합니다.<pre><code class="language-python">dt.toDF().show(5)
</code></pre>
</li>
</ul>
<hr>
<p>+---+---------+---+--------+-----------+-----------+--------+
| id|     name|age|hometown|prefer_type|badge_count|   level|
+---+---------+---+--------+-----------+-----------+--------+
|  1|    Brian| 28|   Seoul|   Electric|          8|  Master|
|  2|  Sabrina| 23|   Busan|      Water|          6|Advanced|
|  3|    Susan| 18| Gwangju|       Rock|          7|  Expert|
|  4|   Martin| 20| Incheon|      Grass|          5|Advanced|
|  5|Gabrielle| 30|   Daegu|     Flying|          6|Advanced|
+---+---------+---+--------+-----------+-----------+--------+</p>
<pre><code>---
## 4️⃣ DeltaTable 형식 테이블 생성
### ▪ 1) create
- `delta.tables.DeltaTable` 타입의 비어있는 테이블을 생성합니다.
- create 관련해서는 아래 세 가지 종류가 있는데 활용법이 비슷하므로 `create` 메소드만 실습해보도록 하겠습니다.
  - `create` : 새로운 DeltaTable을 생성합니다. 테이블이 이미 존재하면 오류가 발생합니다.
  - `createIfNotExists` : 새로운 DeltaTable을 생성합니다. 테이블이 이미 존재해도 오류가 나지 않습니다.
  - `createOrReplace` : 새로운 DeltaTable을 생성하거나 동일한 이름의 기존 테이블을 대체합니다.
```python
my_dt = DeltaTable.create(spark) \
          .tableName(&quot;my_table&quot;) \
          .addColumn(&quot;id&quot;, &quot;INT&quot;) \
          .addColumn(&quot;name&quot;, &quot;STRING&quot;) \
          .addColumn(&quot;age&quot;, &quot;INT&quot;) \
          .execute()

my_dt.toDF().show()

---
+---+----+---+
| id|name|age|
+---+----+---+
+---+----+---+</code></pre><h3 id="▪-2-replace">▪ 2) replace</h3>
<ul>
<li>위의 <code>createOrReplace</code> 메소드와 비슷하게 기존 DeltaTable을 새로운 스키마의 테이블로 대체할 때 사용합니다.<pre><code class="language-python">df = spark.createDataFrame([(&#39;Ryan&#39;, 31), (&#39;Alice&#39;, 27), (&#39;Ruby&#39;, 24)], [&quot;name&quot;, &quot;age&quot;])
</code></pre>
</li>
</ul>
<p>my_dt = DeltaTable.replace(spark) <br>    .tableName(&quot;my_table&quot;) <br>    .addColumns(df.schema) <br>    .execute()</p>
<p>my_dt.show()</p>
<hr>
<p>+----+---+
|name|age|
+----+---+
+----+---+</p>
<pre><code>## 5️⃣ DeltaTable 업데이트, 삭제, 병합
### ▪ 1) UPDATE
- dataframe에서 id가 5~10에 해당하는 row의 level 컬럼을 &#39;Delta_Update&#39;로 변경합니다.
```python
dt.update(
    condition=&quot;id &gt;= 5 AND id &lt;= 10&quot;,
    set={&#39;level&#39; : &quot;&#39;Delta_Update&#39;&quot;}
)</code></pre><h3 id="▪-2-delete">▪ 2) DELETE</h3>
<p>prefer_type 컬럼에서 데이터가 `Rock&#39;인 행을 모두 삭제합니다.</p>
<pre><code class="language-python">dt.delete(
    condition=&quot;prefer_type = &#39;Rock&#39;&quot;
)</code></pre>
<h3 id="▪-3-merge">▪ 3) MERGE</h3>
<ul>
<li><code>merge()</code>는 테이블에 데이터를 upsert(업데이트 또는 삽입)하거나 삭제하는 데 매우 유용한 기능입니다.</li>
<li>merge 메소드의 옵션<ul>
<li><code>whenMatchedDelete</code> : 소스와 대상 테이블의 레코드가 매칭될 때, 해당 레코드를 삭제</li>
<li><code>whenMatchedUpdate</code> : 소스와 대상 테이블의 레코드가 매칭될 때, 해당 레코드를 업데이트</li>
<li><code>whenMatchedUpdateAll</code> : 소스와 대상 테이블의 레코드가 매칭될 때, 모든 컬럼을 소스 데이터로 업데이트</li>
<li><code>whenNotMatchedBySourceDelete</code> : 소스 데이터에 없는 대상 테이블의 레코드를 삭제</li>
<li><code>whenNotMatchedBySourceUpdate</code> : 소스 데이터에 없는 대상 테이블의 레코드를 업데이트</li>
<li><code>whenNotMatchedInsert</code> : 소스 데이터가 대상 테이블에 없는 경우, 새로운 레코드를 삽입</li>
<li><code>whenNotMatchedInsertAll</code> : 소스 데이터가 대상 테이블에 없는 경우, 모든 컬럼을 삽입</li>
<li><code>withSchemaEvolution</code> : 스키마가 변경된 경우(예: 소스 데이터에 새로운 컬럼이 추가됨), 대상 테이블의 스키마를 자동으로 업데이트</li>
</ul>
</li>
</ul>
<p>1) 대상 테이블과 소스 테이블 생성</p>
<pre><code class="language-python"># 대상 테이블 → 기존 trainer 테이블에서 5개 행만 추출
dt.delete(
    condition=&quot;id &gt; 5&quot;
)

# 소스 테이블
data = [
    (1, &quot;Brian&quot;, 29, &quot;Seoul&quot;, &quot;Electric&quot;, 9, &quot;GrandMaster&quot;),
    (3, &quot;Susan&quot;, 19, &quot;Gwangju&quot;, &quot;Rock&quot;, 8, &quot;Master&quot;),
    (7, &quot;Alex&quot;, 25, &quot;Jeju&quot;, &quot;Fire&quot;, 3, &quot;Beginner&quot;),
    (8, &quot;Emily&quot;, 22, &quot;Ulsan&quot;, &quot;Psychic&quot;, 5, &quot;Intermediate&quot;)
]

columns = [&quot;id&quot;, &quot;name&quot;, &quot;age&quot;, &quot;hometown&quot;, &quot;prefer_type&quot;, &quot;badge_count&quot;, &quot;level&quot;]
source_df = spark.createDataFrame(data, columns)</code></pre>
<p>2) merge 작업 수행</p>
<ul>
<li><code>source_df</code> 테이블 데이터 중 기존 <code>dt</code>와 id가 겹치는 행은 업데이트, id가 없는 행은 추가하는 작업을 수행합니다.<pre><code class="language-python">dt.alias(&quot;target&quot;) \
  .merge(
      source=source_df.alias(&quot;source&quot;),
      condition=&quot;target.id = source.id&quot;
  ) \
  .whenMatchedUpdate(
      set={
          &quot;name&quot;: &quot;source.name&quot;,
          &quot;age&quot;: &quot;source.age&quot;,
          &quot;hometown&quot;: &quot;source.hometown&quot;,
          &quot;prefer_type&quot;: &quot;source.prefer_type&quot;,
          &quot;badge_count&quot;: &quot;source.badge_count&quot;,
          &quot;level&quot;: &quot;source.level&quot;
      }
  ) \
  .whenNotMatchedInsert(
      values={
          &quot;id&quot;: &quot;source.id&quot;,
          &quot;name&quot;: &quot;source.name&quot;,
          &quot;age&quot;: &quot;source.age&quot;,
          &quot;hometown&quot;: &quot;source.hometown&quot;,
          &quot;prefer_type&quot;: &quot;source.prefer_type&quot;,
          &quot;badge_count&quot;: &quot;source.badge_count&quot;,
          &quot;level&quot;: &quot;source.level&quot;
      }
  ) \
  .execute()
</code></pre>
</li>
</ul>
<p>dt.toDF().show()</p>
<hr>
<p>+---+---------+---+--------+-----------+-----------+------------+
| id|     name|age|hometown|prefer_type|badge_count|       level|
+---+---------+---+--------+-----------+-----------+------------+
|  1|    Brian| 29|   Seoul|   Electric|          9| GrandMaster|
|  2|  Sabrina| 23|   Busan|      Water|          6|    Advanced|
|  3|    Susan| 19| Gwangju|       Rock|          8|      Master|
|  4|   Martin| 20| Incheon|      Grass|          5|    Advanced|
|  5|Gabrielle| 30|   Daegu|     Flying|          6|    Advanced|
|  7|     Alex| 25|    Jeju|       Fire|          3|    Beginner|
|  8|    Emily| 22|   Ulsan|    Psychic|          5|Intermediate|
+---+---------+---+--------+-----------+-----------+------------+</p>
<pre><code>## 6️⃣ DeltaTable 메타데이터 조회
### ▪ 1) detail
- Delta 테이블의 상세 정보(스키마, 속성, 메타데이터 등)를 확인할 때 사용합니다.
```python
dt.detail().show(truncate=False)

---
+------+------------------------------------+----+-----------+---------------------------------------------------------+-----------------------+-----------------------+----------------+-----------------+--------+-----------+----------+----------------+----------------+------------------------+
|format|id                                  |name|description|location                                                 |createdAt              |lastModified           |partitionColumns|clusteringColumns|numFiles|sizeInBytes|properties|minReaderVersion|minWriterVersion|tableFeatures           |
+------+------------------------------------+----+-----------+---------------------------------------------------------+-----------------------+-----------------------+----------------+-----------------+--------+-----------+----------+----------------+----------------+------------------------+
|delta |e3db23a9-cc4c-4700-95ee-a8b4e06dfbf9|NULL|NULL       |file:/workspace/spark/deltalake/delta_local/trainer_delta|2025-03-28 07:31:56.941|2025-04-01 01:15:22.165|[]              |[]               |1       |3984       |{}        |1               |2               |[appendOnly, invariants]|
+------+------------------------------------+----+-----------+---------------------------------------------------------+-----------------------+-----------------------+----------------+-----------------+--------+-----------+----------+----------------+----------------+------------------------+</code></pre><h3 id="▪-2-history">▪ 2) history</h3>
<ul>
<li>Delta 테이블에 수행된 작업 기록(쓰기, 업데이트, 삭제 등)을 확인할 때 사용합니다.<pre><code class="language-python">dt.history().show(truncate=False)
</code></pre>
</li>
</ul>
<hr>
<p>+-------+-----------------------+------+--------+------------+-----------------------------------------------------------------------------------------------+----+--------+---------+-----------+--------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------+-----------------------------------+
|version|timestamp              |userId|userName|operation   |operationParameters                                                                            |job |notebook|clusterId|readVersion|isolationLevel|isBlindAppend|operationMetrics                                                                                                                                                                                                                                                                                                              |userMetadata|engineInfo                         |
+-------+-----------------------+------+--------+------------+-----------------------------------------------------------------------------------------------+----+--------+---------+-----------+--------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------+-----------------------------------+
|3      |2025-04-01 01:15:22.165|NULL  |NULL    |DELETE      |{predicate -&gt; [&quot;(prefer_type#3178 = Rock)&quot;]}                                                   |NULL|NULL    |NULL     |2          |Serializable  |false        |{numRemovedFiles -&gt; 1, numRemovedBytes -&gt; 3997, numCopiedRows -&gt; 89, numDeletionVectorsAdded -&gt; 0, numDeletionVectorsRemoved -&gt; 0, numAddedChangeFiles -&gt; 0, executionTimeMs -&gt; 789, numDeletionVectorsUpdated -&gt; 0, numDeletedRows -&gt; 1, scanTimeMs -&gt; 494, numAddedFiles -&gt; 1, numAddedBytes -&gt; 3984, rewriteTimeMs -&gt; 294} |NULL        |Apache-Spark/3.5.1 Delta-Lake/3.2.0|
|2      |2025-04-01 01:13:45.637|NULL  |NULL    |UPDATE      |{predicate -&gt; [&quot;((id#3174 &gt;= 5) AND (id#3174 &lt;= 10))&quot;]}                                        |NULL|NULL    |NULL     |1          |Serializable  |false        |{numRemovedFiles -&gt; 1, numRemovedBytes -&gt; 3980, numCopiedRows -&gt; 84, numDeletionVectorsAdded -&gt; 0, numDeletionVectorsRemoved -&gt; 0, numAddedChangeFiles -&gt; 0, executionTimeMs -&gt; 1604, numDeletionVectorsUpdated -&gt; 0, scanTimeMs -&gt; 984, numAddedFiles -&gt; 1, numUpdatedRows -&gt; 6, numAddedBytes -&gt; 3997, rewriteTimeMs -&gt; 618}|NULL        |Apache-Spark/3.5.1 Delta-Lake/3.2.0|
|1      |2025-03-28 07:32:09.347|NULL  |NULL    |WRITE       |{mode -&gt; Append, partitionBy -&gt; []}                                                            |NULL|NULL    |NULL     |0          |Serializable  |true         |{numFiles -&gt; 1, numOutputRows -&gt; 90, numOutputBytes -&gt; 3980}                                                                                                                                                                                                                                                                  |NULL        |Apache-Spark/3.5.1 Delta-Lake/3.2.0|
|0      |2025-03-28 07:31:57.505|NULL  |NULL    |CREATE TABLE|{partitionBy -&gt; [], clusterBy -&gt; [], description -&gt; NULL, isManaged -&gt; false, properties -&gt; {}}|NULL|NULL    |NULL     |NULL       |Serializable  |true         |{}                                                                                                                                                                                                                                                                                                                            |NULL        |Apache-Spark/3.5.1 Delta-Lake/3.2.0|
+-------+-----------------------+------+--------+------------+-----------------------------------------------------------------------------------------------+----+--------+---------+-----------+--------------+-------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------+-----------------------------------+</p>
<pre><code>### ▪ 3) generate
- Delta Lake 테이블의 데이터를 외부 시스템(예: Apache Hive, Presto, Amazon Athena 등)에서 읽을 수 있도록 매니페스트 파일을 생성하는 함수입니다.
- `generate` 함수를 실행하면 delta 테이블 디렉토리에 `_symlink_format_manifest` 디렉토리가 생성되고, 그 안에 현재 버전의 parquet 파일을 가리키는 메니페스트 파일이 작성됩니다.
- Presto, Trino, Amazon Athena, Apache Hive와 같은 엔진은 Delta Lake의 로그 기반 트랜잭션 시스템을 직접 지원하지 않습니다. 하지만 `_symlink_format_manifest`를 사용하면 delta 테이블을 parquet 파일로 표현한 매니페스트를 제공하므로, 이러한 엔진에서 테이블을 쿼리할 수 있습니다.
```python
dt.generate(&quot;symlink_format_manifest&quot;)</code></pre><p><img src="https://velog.velcdn.com/images/newnew_daddy/post/65b45d1c-8338-4031-a683-dc467f739891/image.png" alt=""></p>
<hr>
<h2 id="7️⃣-time-travel-쿼리">7️⃣ Time Travel 쿼리</h2>
<ul>
<li>데이터의 과거 버전을 조회하는 Time Travel 쿼리의 경우 특정 버전을 기준으로 조회하는 경우와 특정 시간(timestamp)을 기준으로 조회하는 경우, 이렇게 두 가지가 가능합니다.<h3 id="▪-1-version">▪ 1) Version</h3>
<pre><code class="language-python">dt.restoreToVersion(1)
</code></pre>
</li>
</ul>
<p>dt.toDF().show(10)</p>
<pre><code>### ▪ 2) Timestamp
- `SEARCH_TIME`에 저장된 시간에 존재했던 테이블 모습을 조회합니다.
```python
SEARCH_TIME = &#39;2025-03-28 07:32:00&#39;

dt.restoreToTimestamp(SEARCH_TIME)

dt.toDF().show(10)</code></pre><h2 id="8️⃣-파일-상태-최적화">8️⃣ 파일 상태 최적화</h2>
<ul>
<li>작게 나눠서 저장된 parquet 파일들을 합쳐 용량은 크게, 파일 수는 적게 만들어 데이터를 관리합니다. 이 과정을 통해 Delta 테이블의 쿼리 성능을 향상시킬 수 있습니다.</li>
<li><code>executeZOrderBy</code> 옵션은 지정된 컬럼에 대해 Z-Order 클러스터링을 적용하여 데이터를 물리적으로 재배치시킵니다. 쿼리에서 해당 컬럼에 대한 필터링이나 조인이 빈번할 때 효과적입니다.<h3 id="▪-1-optimize">▪ 1) OPTIMIZE</h3>
<pre><code class="language-python"># 표준 최적화 방법
dt.optimize().executeCompaction()
</code></pre>
</li>
</ul>
<h1 id="특정-컬럼을-대상으로-최적화">특정 컬럼을 대상으로 최적화</h1>
<p>dt.optimize().executeZOrderBy(&#39;level&#39;)</p>
<pre><code>### ▪ 2) VACUUM
- 더 이상 필요 없는 오래된 데이터 버전을 삭제하여 저장 공간을 확보할 때 사용합니다.
- 기본은 7일(168시간)이며, `retentionHours` 파라미터를 통해 특정 시간 이후의 데이터를 지우도록 할 수 있습니다.
```python
dt.vacuum(
    retentionHours=10
)</code></pre><h2 id="9️⃣-parquet-to-delta-변환">9️⃣ Parquet to Delta 변환</h2>
<h3 id="▪-1-parquet-파일-저장">▪ 1) parquet 파일 저장</h3>
<ul>
<li><code>trainer_data.csv</code> 파일을 읽어와 테이블과 경로에 <code>parquet</code> 타입으로 저장합니다.<pre><code class="language-python"># 1. CSV 파일을 DataFrame으로 읽기
DATA_PATH = &#39;/workspace/spark/deltalake/dataset/trainer_data.csv&#39;
SAVE_PATH = &#39;/workspace/spark/deltalake/delta_local/spark-warehouse/trainer_parquet/&#39;
</code></pre>
</li>
</ul>
<p>df = spark.read.csv(DATA_PATH, header=True, inferSchema=True)</p>
<h1 id="2-테이블-생성">2. 테이블 생성</h1>
<p>query = f&quot;&quot;&quot;
CREATE TABLE IF NOT EXISTS deltalake_db.trainer_parquet (
  id INT,
  name STRING,
  age INT,
  hometown STRING,
  prefer_type STRING,
  badge_count INT,
  level STRING
)
USING parquet
LOCATION &#39;{SAVE_PATH}&#39;
&quot;&quot;&quot;
spark.sql(query)</p>
<h1 id="3-dataframe을-테이블과-경로에-저장">3. DataFrame을 테이블과 경로에 저장</h1>
<p>df.write.mode(&quot;overwrite&quot;) <br>    .option(&quot;path&quot;, SAVE_PATH) <br>    .saveAsTable(&quot;deltalake_db.trainer_parquet&quot;)</p>
<pre><code>### ▪ 2) delta로 변환
```python
# 저장된 테이블을 delta로 변환
DeltaTable.convertToDelta(spark, &quot;deltalake_db.trainer_parquet&quot;)

# 디렉토리에 저장된 파일 데이터를 delta로 변환
DeltaTable.convertToDelta(spark, f&quot;parquet.`{SAVE_PATH}`&quot;)</code></pre><hr>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://docs.delta.io/latest/api/python/spark/index.html">Delta Lake’s Python documentation page</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[🌊 Delta Lake 입문자를 위한 가이드 - 실전편(Part 1. 로컬 환경)]]></title>
            <link>https://velog.io/@newnew_daddy/data08</link>
            <guid>https://velog.io/@newnew_daddy/data08</guid>
            <pubDate>Mon, 24 Mar 2025 09:16:32 GMT</pubDate>
            <description><![CDATA[<ul>
<li>Delta Lake 이론 - <a href="https://velog.io/@newnew_daddy/data07">🌊 Delta Lake 입문자를 위한 가이드 - 이론편</a></li>
<li>Delta Lake 실전 Part 1 - <a href="https://velog.io/@newnew_daddy/data08">🌊 Delta Lake 입문자를 위한 가이드 - 실전편(Part 1. 로컬 환경)</a></li>
<li>Delta Lake 실전 Part 2 - <a href="https://velog.io/@newnew_daddy/data09">🌊 Delta Lake 입문자를 위한 가이드 - 실전편(Part 2. delta-spark 라이브러리 활용)</a></li>
</ul>
<hr>
<h2 id="0-intro">0. INTRO</h2>
<ul>
<li>앞선 글 <a href="https://velog.io/@newnew_daddy/data07">🌊 Delta Lake 입문자를 위한 가이드 - 이론편</a>에서는 Delta Lake에 대한 이론적인 내용을 상세하게 다루어 보았습니다. 이번 글에서는 Pyspark Docker Container 환경에서 Delta Lake의 기능을 실습해보도록 하겠습니다. </li>
<li>이번 실습에서는 데이터를 다루는 도구로 Pyspark를 사용하며, delta 유형의 파일들은 로컬 디렉토리에 저장되어 관리됩니다.</li>
<li>도커 컨테이너의 경우 이후 클라우드 환경을 연동한 실습까지 고려하였을 때 제가 따로 생성한 <code>hyunsoolee0506/pyspark-cloud:3.5.1</code> 이미지로 생성하시는 것을 권장드립니다. 하지만 이번 로컬 환경 실습의 경우는 Google Colab에서 진행해도 무방합니다.</li>
<li>실습에서는 아래 <code>delta_data.zip</code>파일 안에 있는 두 가지 CSV 파일 데이터를 사용하였습니다.<blockquote>
<p>👉 <a href="https://github.com/user-attachments/files/19412310/delta_data.zip">delta_data.zip</a></p>
</blockquote>
</li>
</ul>
<hr>
<h2 id="1️⃣-실습-환경-설정">1️⃣ 실습 환경 설정</h2>
<h3 id="▪-1-docker-container-생성">▪ 1) Docker Container 생성</h3>
<ul>
<li>사용자의 컴퓨터에서 volume으로 사용할 디렉토리와 컨테이너 내부 <code>/workspace/spark</code> 디렉토리가 매핑되도록 설정합니다.<pre><code class="language-shell">docker run -d \
  --name pyspark \
  -p 8888:8888 \
  -p 4040:4040 \
  -v [사용자 디렉토리]:/workspace/spark \
  hyunsoolee0506/pyspark-cloud:3.5.1</code></pre>
</li>
<li>위 명령어 실행 후 <code>8888</code> 포트로 접속하면 juypter lab 개발 환경으로 들어올 수 있습니다.<h3 id="▪-2-라이브러리-설치">▪ 2) 라이브러리 설치</h3>
</li>
<li>실습에 필요한 라이브러리들을 설치합니다. <code>hyunsoolee0506/pyspark-cloud:3.5.1</code> 이미지에는 이미 설치되어 있지만 colab의 경우 아래 코드 실행을 통해 라이브러리들을 설치해야 합니다.<pre><code class="language-shell">pip install pyspark==3.5.1 delta-spark==3.2.0 pyarrow findspark</code></pre>
</li>
</ul>
<h3 id="▪-3-pyspark-delta-lake-환경-설정">▪ 3) Pyspark delta lake 환경 설정</h3>
<ul>
<li><a href="https://docs.delta.io/latest/quick-start.html#set-up-apache-spark-with-delta-lake">https://docs.delta.io/latest/quick-start.html#set-up-apache-spark-with-delta-lake</a></li>
<li>pyspark에서 delta lake를 사용하기 위해서 SparkSession 생성시 관련 extension들에 대한 설정을 합니다.<pre><code class="language-python">from delta import *
from pyspark.sql import SparkSession
import pyspark.sql.functions as F
</code></pre>
</li>
</ul>
<p>builder = SparkSession.builder.appName(&quot;DeltaLakeLocal&quot;) <br>    .enableHiveSupport() <br>    .config(&quot;spark.sql.extensions&quot;, &quot;io.delta.sql.DeltaSparkSessionExtension&quot;) <br>    .config(&quot;spark.sql.catalog.spark_catalog&quot;, &quot;org.apache.spark.sql.delta.catalog.DeltaCatalog&quot;)</p>
<p>spark = configure_spark_with_delta_pip(builder).getOrCreate()</p>
<pre><code>
---
## 2️⃣ 데이터베이스 및 테이블 생성
### ▪ 1) 데이터베이스 생성
- `deltalake_db`라는 이름의 새로운 데이터베이스를 생성합니다.
```python
spark.sql(&quot;CREATE DATABASE IF NOT EXISTS deltalake_db&quot;)

spark.sql(&quot;SHOW DATABASES&quot;).show()

---
+------------+
|   namespace|
+------------+
|     default|
|deltalake_db|
+------------+</code></pre><h3 id="▪-2-csv-타입-테이블-생성">▪ 2) csv 타입 테이블 생성</h3>
<ul>
<li><code>trainer_data.csv</code> 파일의 데이터가 저장될 <code>trainer</code> 테이블을 생성합니다.
```</li>
<li>테이블 이름 : trainer</li>
<li>스키마 : <ul>
<li>id → INT</li>
<li>name → STRING</li>
<li>age → INT</li>
<li>hometown → STRING</li>
<li>prefer_type → STRING</li>
<li>badge_count → INT</li>
<li>level → STRING<pre><code>```python
query = f&quot;&quot;&quot;
CREATE TABLE IF NOT EXISTS deltalake_db.trainer (
id INT,
name STRING,
age INT,
hometown STRING,
prefer_type STRING,
badge_count INT,
level STRING
)
USING csv
OPTIONS (
path &#39;[trainer_data.csv 파일 경로]&#39;,
header &#39;true&#39;,
inferSchema &#39;true&#39;,
delimiter &#39;,&#39;
)
&quot;&quot;&quot;
</code></pre></li>
</ul>
</li>
</ul>
<p>spark.sql(query)</p>
<h1 id="테이블-생성-확인">테이블 생성 확인</h1>
<p>spark.sql(&quot;SHOW TABLES FROM deltalake_db&quot;).show()</p>
<hr>
<p>+------------+---------+-----------+
|   namespace|tableName|isTemporary|
+------------+---------+-----------+
|deltalake_db|  trainer|      false|
+------------+---------+-----------+</p>
<pre><code>---
## 3️⃣ delta 타입 테이블 생성
- 기존에 있는 `csv` 파일의 데이터를 바로 `delta` 유형의 테이블 생성과 동시에 넣을 수는 없기 때문에 아래와 같이 두 단계를 거쳐 `delta` 테이블을 생성하여야 합니다.
  1. `delta` 유형 빈 테이블 생성
  2. `delta` 테이블에 `csv` 테이블 데이터 삽입
### ▪ 1) 테이블 생성
- `trainer_delta`라는 이름의 `delta` 테이블을 생성합니다.
- `/workspace/spark/deltalake/delta_local/trainer_delta/` 해당 경로 아래에 `delta` 테이블 관련 데이터가 저장되도록 설정하였습니다.
```python
query = f&quot;&quot;&quot;
CREATE TABLE IF NOT EXISTS deltalake_db.trainer_delta (
  id INT,
  name STRING,
  age INT,
  hometown STRING,
  prefer_type STRING,
  badge_count INT,
  level STRING
)
USING delta
LOCATION &#39;/workspace/spark/deltalake/delta_local/trainer_delta/&#39;
&quot;&quot;&quot;
spark.sql(query)</code></pre><h3 id="▪-2-데이터-삽입">▪ 2) 데이터 삽입</h3>
<ul>
<li>위에서 생성하였던 <code>trainer</code> 테이블의 데이터를 <code>trainer_delta</code> 테이블에 삽입합니다.<pre><code class="language-python">query = &quot;&quot;&quot;
INSERT INTO deltalake_db.trainer_delta
SELECT * FROM deltalake_db.trainer;
&quot;&quot;&quot;
spark.sql(query)</code></pre>
</li>
<li>데이터 삽입이 완료가 되면 delta 테이블 디렉토리에 <code>_delta_log/</code> 폴더와 parquet 파일이 새롭게 생성이 된 것을 확인해볼 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/9f2fd6b5-ba33-45b4-9afe-b0f07f896739/image.png" alt=""></li>
</ul>
<hr>
<h2 id="4️⃣-delta-타입-테이블-읽기">4️⃣ delta 타입 테이블 읽기</h2>
<ul>
<li>pyspark에서는 조금씩 다른 방식으로 저장된 테이블을 읽어올 수 있습니다.<h3 id="▪-1-spark에서-기본-읽기">▪ 1) spark에서 기본 읽기</h3>
<pre><code class="language-python">LOCAL_DELTA_PATH = &#39;/workspace/spark/deltalake/delta_local/trainer_delta&#39;
</code></pre>
</li>
</ul>
<p>df = spark.read.format(&quot;delta&quot;).load(LOCAL_DELTA_PATH)</p>
<p>df.show(5)</p>
<hr>
<p>+---+------+---+--------+-----------+-----------+------------+
| id|  name|age|hometown|prefer_type|badge_count|       level|
+---+------+---+--------+-----------+-----------+------------+
|  1| Brian| 28|   Seoul|   Electric|          8|      Master|
|  3| Susan| 18| Gwangju|       Rock|          7|      Expert|
|  6| Vicki| 17| Daejeon|        Ice|          4|Intermediate|
|  9|Olivia| 45| Incheon|    Psychic|          3|Intermediate|
| 10|  Mark| 16| Gangwon|       Fire|          4|Intermediate|
+---+------+---+--------+-----------+-----------+------------+
only showing top 5 rows</p>
<pre><code>### ▪ 2) `delta.`으로 읽기
```python
query = f&quot;SELECT * FROM delta.`{LOCAL_DELTA_PATH}`&quot;

spark.sql(query)</code></pre><h3 id="▪-3-hive-catalog에서-읽기">▪ 3) hive catalog에서 읽기</h3>
<pre><code class="language-python">spark.table(&#39;deltalake_db.trainer_delta&#39;)</code></pre>
<hr>
<h2 id="5️⃣-테이블-수정-후-저장">5️⃣ 테이블 수정 후 저장</h2>
<ul>
<li>이번에는 테이블 내용을 수정하여 기존에 저장되어 있던 디렉토리에 덮어쓰는 과정을 진행합니다. 이후에 있을 테이블 변경 이력 조회나 delta lake의 핵심 기능인 Time Travel 쿼리를 실습해보기 위한 과정입니다.<h3 id="▪-1-beginner-제외-후-저장">▪ 1) &#39;Beginner&#39; 제외 후 저장</h3>
<pre><code class="language-python"># Beginner 제외한 dataframe 생성
df_1 = df.filter(F.col(&#39;level&#39;) != &#39;Beginner&#39;)
</code></pre>
</li>
</ul>
<h1 id="기존-경로에-덮어쓰기">기존 경로에 덮어쓰기</h1>
<p>df_1.write <br>    .format(&#39;delta&#39;) <br>    .mode(&#39;overwrite&#39;) <br>    .save(LOCAL_DELTA_PATH)</p>
<h1 id="데이터-확인">데이터 확인</h1>
<p>df = spark.read.format(&quot;delta&quot;).load(LOCAL_DELTA_PATH)
df.select(&#39;level&#39;).distinct().show()</p>
<hr>
<p>+------------+
|       level|
+------------+
|      Expert|
|    Advanced|
|      Master|
|Intermediate|
+------------+</p>
<pre><code>### ▪ 2) &#39;Advanced&#39; 제외 후 저장
```python
# Advanced 제외한 dataframe 생성
df_2 = df_1.filter(F.col(&#39;level&#39;) != &#39;Advanced&#39;)

# 기존 경로에 덮어쓰기
df_2.write \
    .format(&#39;delta&#39;) \
    .mode(&#39;overwrite&#39;) \
    .save(LOCAL_DELTA_PATH)

# 데이터 확인
df = spark.read.format(&quot;delta&quot;).load(LOCAL_DELTA_PATH)
df.select(&#39;level&#39;).distinct().show()

---
+------------+
|       level|
+------------+
|      Expert|
|      Master|
|Intermediate|
+------------+</code></pre><ul>
<li>데이터가 덮어씌워짐에 따라 parquet 파일이 추가되고, <code>_delta_log/</code> 폴더 내에 메타데이터(<code>.json</code> 파일) 역시 추가되는 것을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/newnew_daddy/post/de159ad9-7d0f-4935-ab7f-0af41d320e5f/image.png" alt=""></li>
</ul>
<hr>
<h2 id="6️⃣-변경-이력history-조회-및-time-travel-쿼리">6️⃣ 변경 이력(history) 조회 및 Time Travel 쿼리</h2>
<h3 id="▪-1-history-조회">▪ 1) History 조회</h3>
<ul>
<li><code>delta</code> 테이블에 대한 변경 이력을 조회합니다.</li>
<li>현재까지 테이블은 처음 생성(CREATE) 후 WRITE 가 총 3번 발생한 구조입니다. 따라서 version 역시 0,1,2,3 이렇게 존재합니다.<ul>
<li>VERSION 0 → <code>trainer_delta</code> 테이블 생성 상태, 데이터 X</li>
<li>VERSION 1 → <code>trainer</code> 테이블에서 데이터 삽입된 최초 상태</li>
<li>VERSION 2 → &#39;Beginner&#39; 행 제외 후 저장된 상태</li>
<li>VERSION 3 → Advanced&#39; 행 제외 후 저장된 상태<pre><code class="language-python">query = &quot;DESCRIBE HISTORY deltalake_db.trainer_delta&quot;
</code></pre>
</li>
</ul>
</li>
</ul>
<p>spark.sql(query).show(vertical=True, truncate=False)</p>
<hr>
<p>-RECORD 0--------------------------------------------------------------------------------------------------------------
 version             | 3<br> timestamp           | 2025-03-21 02:09:39.085<br> userId              | NULL<br> userName            | NULL<br> operation           | WRITE<br> operationParameters | {mode -&gt; Overwrite, partitionBy -&gt; []}<br> job                 | NULL<br> notebook            | NULL<br> clusterId           | NULL<br> readVersion         | 2<br> isolationLevel      | Serializable<br> isBlindAppend       | false<br> operationMetrics    | {numFiles -&gt; 1, numOutputRows -&gt; 42, numOutputBytes -&gt; 3125}<br> userMetadata        | NULL<br> engineInfo          | Apache-Spark/3.5.1 Delta-Lake/3.2.0<br>-RECORD 1--------------------------------------------------------------------------------------------------------------
 version             | 2<br> timestamp           | 2025-03-21 02:08:32.646<br> userId              | NULL<br> userName            | NULL<br> operation           | WRITE<br> operationParameters | {mode -&gt; Overwrite, partitionBy -&gt; []}<br> job                 | NULL<br> notebook            | NULL<br> clusterId           | NULL<br> readVersion         | 1<br> isolationLevel      | Serializable<br> isBlindAppend       | false<br> operationMetrics    | {numFiles -&gt; 1, numOutputRows -&gt; 85, numOutputBytes -&gt; 3868}<br> userMetadata        | NULL<br> engineInfo          | Apache-Spark/3.5.1 Delta-Lake/3.2.0<br>-RECORD 2--------------------------------------------------------------------------------------------------------------
 version             | 1<br> timestamp           | 2025-03-21 01:46:21.446<br> userId              | NULL<br> userName            | NULL<br> operation           | WRITE<br> operationParameters | {mode -&gt; Append, partitionBy -&gt; []}<br> job                 | NULL<br> notebook            | NULL<br> clusterId           | NULL<br> readVersion         | 0<br> isolationLevel      | Serializable<br> isBlindAppend       | true<br> operationMetrics    | {numFiles -&gt; 1, numOutputRows -&gt; 90, numOutputBytes -&gt; 3980}<br> userMetadata        | NULL<br> engineInfo          | Apache-Spark/3.5.1 Delta-Lake/3.2.0<br>-RECORD 3--------------------------------------------------------------------------------------------------------------
 version             | 0<br> timestamp           | 2025-03-21 01:43:25.471<br> userId              | NULL<br> userName            | NULL<br> operation           | CREATE TABLE<br> operationParameters | {partitionBy -&gt; [], clusterBy -&gt; [], description -&gt; NULL, isManaged -&gt; false, properties -&gt; {}} 
 job                 | NULL<br> notebook            | NULL<br> clusterId           | NULL<br> readVersion         | NULL<br> isolationLevel      | Serializable<br> isBlindAppend       | true<br> operationMetrics    | {}<br> userMetadata        | NULL<br> engineInfo          | Apache-Spark/3.5.1 Delta-Lake/3.2.0                                                    </p>
<pre><code>### ▪ 2) Time Travel - Version
- 테이블 변경시마다 부여된 버전 번호를 기반으로 특정 버전에 해당하는 테이블의 내용을 불러옵니다.

👉 최초 버전(version 0) 테이블 불러오기
```python
df_pre = spark.read \
    .format(&quot;delta&quot;) \
    .option(&quot;versionAsof&quot;, 0) \
    .load(LOCAL_DELTA_PATH)

df_pre.select(&#39;level&#39;).distinct().show()

---
+-----+
|level|
+-----+
+-----+</code></pre><p>👉 version 2 테이블 불러오기</p>
<pre><code class="language-python">df_pre = spark.read \
    .format(&quot;delta&quot;) \
    .option(&quot;versionAsof&quot;, 2) \
    .load(LOCAL_DELTA_PATH)

df_pre.select(&#39;level&#39;).distinct().show()

---
+------------+
|       level|
+------------+
|      Expert|
|    Advanced|
|      Master|
|Intermediate|
+------------+</code></pre>
<p>👉 SQL로 Time Travel 쿼리하기</p>
<pre><code class="language-python">df_pre = spark.sql(&quot;SELECT * FROM deltalake_db.trainer_delta VERSION AS OF 3&quot;)

df_pre.select(&#39;level&#39;).distinct().show()
---
+------------+
|       level|
+------------+
|      Expert|
|      Master|
|Intermediate|
+------------+</code></pre>
<h3 id="▪-3-time-travel---timestamp">▪ 3) Time Travel - Timestamp</h3>
<ul>
<li>테이블 변경 시간을 기준으로 조회하는 방법으로, 지정한 시간(TIMESTAMP) 기준으로 해당 시점에 존재했던 Delta 테이블의 상태(버전)를 조회합니다.</li>
</ul>
<p>👉 지정한 시간대의 테이블 상태 불러오기</p>
<pre><code class="language-python">TABLE_TIMESTAMP = &quot;2025-03-21T02:09:00&quot;

spark.read.format(&quot;delta&quot;) \
    .option(&quot;timestampAsOf&quot;, TABLE_TIMESTAMP) \
    .table(&quot;deltalake_db.trainer_delta&quot;)</code></pre>
<p>👉 SQL로 지정 시간대의 테이블 불러오기</p>
<pre><code class="language-python">TABLE_TIMESTAMP = &quot;2025-03-21T02:09:00&quot;

spark.sql(f&quot;SELECT * FROM deltALake_db.trainer_delta TIMESTAMP AS OF &#39;{TABLE_TIMESTAMP}&#39;&quot;)</code></pre>
<hr>
<h2 id="7️⃣-스키마-변경-작업">7️⃣ 스키마 변경 작업</h2>
<ul>
<li>Delta Lake는 기존 테이블 스키마와 다른 데이터를 쓰려고 하면 에러가 나도록 하는 &#39;스키마 강제(Strict Schema Enforcement)&#39; 옵션을 사용합니다. 따라서 기존 작업하던 테이블의 스키마 변경이 일어났다면, 특정 옵션을 추가해주어야 덮어쓰기가 가능합니다.</li>
<li>실습 내용은 아래와 같습니다.
```</li>
<li>기존 컬럼 : [&#39;id&#39;, &#39;name&#39;, &#39;age&#39;, &#39;hometown&#39;, &#39;prefer_type&#39;, &#39;badge_count&#39;, &#39;level&#39;]</li>
<li>변경된 테이블 컬럼 : [&#39;id&#39;, &#39;name&#39;, &#39;age&#39;, &#39;hometown&#39;, &#39;prefer_type&#39;, &#39;badge_count&#39;, &#39;level&#39;, &#39;dummy_col&#39;]</li>
</ul>
<p>👉 &#39;dummy_col&#39; 이라는 컬럼이 추가되어 스키마가 변경된 테이블 덮어쓰기</p>
<pre><code>### ▪ 1) 표준 쓰기 - 작업 실패
```python
LOCAL_DELTA_PATH = &#39;/workspace/spark/deltalake/delta_local/trainer_delta&#39;

# 테이블 불러오기
df = spark.table(&quot;deltalake_db.trainer_delta&quot;)

# &#39;dummy_col&#39; 컬럼 추가
df_diff = df.withColumn(&#39;dummy_col&#39;, F.lit(1))

# 스키마 합치기 시도
df_diff.write \
    .format(&#39;delta&#39;) \
    .mode(&#39;overwrite&#39;) \
    .save(LOCAL_DELTA_PATH)

# 덮어쓰려는 테이블의 스키마가 달라 아래의 에러 발생
# 👇👇👇👇👇
---
AnalysisException: [_LEGACY_ERROR_TEMP_DELTA_0007] A schema mismatch detected when writing to the Delta table (Table ID: 31dbae5e-d042-467b-9454-e483fdad97bb).
To enable schema migration using DataFrameWriter or DataStreamWriter, please set:
&#39;.option(&quot;mergeSchema&quot;, &quot;true&quot;)&#39;.
For other operations, set the session configuration
spark.databricks.delta.schema.autoMerge.enabled to &quot;true&quot;. See the documentation
specific to the operation for details.

Table schema:
root
-- id: integer (nullable = true)
-- name: string (nullable = true)
-- age: integer (nullable = true)
-- hometown: string (nullable = true)
-- prefer_type: string (nullable = true)
-- badge_count: integer (nullable = true)
-- level: string (nullable = true)


Data schema:
root
-- id: integer (nullable = true)
-- name: string (nullable = true)
-- age: integer (nullable = true)
-- hometown: string (nullable = true)
-- prefer_type: string (nullable = true)
-- badge_count: integer (nullable = true)
-- level: string (nullable = true)
-- dummy_col: integer (nullable = true)


To overwrite your schema or change partitioning, please set:
&#39;.option(&quot;overwriteSchema&quot;, &quot;true&quot;)&#39;.

Note that the schema can&#39;t be overwritten when using
&#39;replaceWhere&#39;.</code></pre><h3 id="▪-2-스키마-합치기-옵션과-함께-쓰기">▪ 2) 스키마 합치기 옵션과 함께 쓰기</h3>
<ul>
<li>스키마가 다른 테이블을 덮어쓰기 위해서는 <code>option(&quot;mergeSchema&quot;, &quot;true&quot;)</code> 옵션을 추가해주어야 합니다.<pre><code class="language-python">df_diff.write \
  .format(&#39;delta&#39;) \
  .mode(&#39;overwrite&#39;) \
  .option(&quot;mergeSchema&quot;, &quot;true&quot;) \
  .save(LOCAL_DELTA_PATH)</code></pre>
</li>
</ul>
<hr>
<h2 id="8️⃣-파일-상태-최적화">8️⃣ 파일 상태 최적화</h2>
<ul>
<li><p><a href="https://docs.databricks.com/aws/en/sql/language-manual/delta-optimize">https://docs.databricks.com/aws/en/sql/language-manual/delta-optimize</a></p>
</li>
<li><p>Delta 테이블은 기본적으로 계속 파일이 적재되는 형식이기 때문에 시간이 지남에 따라 작은 파일들이 많이 생기게 됩니다. 이렇게 되면 쿼리 성능 저하와 읽기 오버헤드 증가가 발생합니다. 이 때 OPTIMIZE를 통해 데이터를 큰 파일로 병합하여 성능을 향상시킬 수 있습니다.</p>
<table>
<thead>
<tr>
<th>최적화 방식</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>기본 Optimize</td>
<td>작은 파일 병합, 읽기 성능 향상</td>
</tr>
<tr>
<td>Z-Ordering</td>
<td>자주 필터링하는 컬럼 기준 정렬 → 스캔 줄여 쿼리 성능 향상</td>
</tr>
<tr>
<td>파티션 기반 Optimize</td>
<td>특정 날짜/지역 등 자주 조회되는 파티션만 선택적 최적화</td>
</tr>
<tr>
<td>### ▪ 1) 표준 최적화</td>
<td></td>
</tr>
</tbody></table>
</li>
<li><p>delta lake가 기본적으로 수행하는 표준 최적화 방식을 적용합니다.</p>
<pre><code class="language-python">query = &quot;OPTIMIZE deltalake_db.trainer_delta&quot;
</code></pre>
</li>
</ul>
<p>spark.sql(query)</p>
<pre><code>### ▪ 2) Z-Ordering 최적화
- 특정 컬럼 기준으로 데이터의 물리적 저장 순서를 최적화하는 기능.
- 자주 필터링하는 컬럼을 기준으로 Z-Order를 걸면 쿼리시 불필요한 파일 스캔을 줄일 수 있습니다.
```python
query = &quot;&quot;&quot;
OPTIMIZE deltalake_db.trainer_delta
ZORDER BY (trainer_id, region)  
&quot;&quot;&quot;

spark.sql(query)</code></pre><h3 id="▪-3-파티션-최적화">▪ 3) 파티션 최적화</h3>
<ul>
<li>전체 테이블을 대상으로 최적화하지 않고, 파티셔닝된 특정 범위의 데이터만 병합합니다.<pre><code class="language-python">query = &quot;&quot;&quot;
OPTIMIZE deltalake_db.trainer_delta
WHERE level = &#39;Master&#39; 
&quot;&quot;&quot;
</code></pre>
</li>
</ul>
<p>spark.sql(query)</p>
<pre><code>
---
## 9️⃣ 과거 데이터 삭제(VACUUM)
- https://docs.databricks.com/aws/en/sql/language-manual/delta-vacuum
- Delta Lake는 데이터의 수정이나 삭제 등이 발생하더라도 과거의 parquet 파일들은 다 남아있게 됩니다.
- 과거 버전의 파일들을 더 이상 사용하지 않는데 계속 남겨놓는 것은 낭비이기 때문에 VACUUM 기능을 활용하여 특정 기간 이전의 데이터를 삭제하는 작업을 수행할 수 있습니다.
- `delta` 유형의 데이터의 경우 과거의 파일 데이터를 지울 때는 디렉토리에서 직접 삭제하면 안되고 `VACUUM` 명령을 통해 지워야 테이블의 정합성을 해치지 않고 이후에도 원활한 작업이 가능해집니다.
- `VACUUM` 작업 발생시 삭제된 날짜 이전으로는 Time Travel 하여 조회하는 것이 불가능해집니다.
- 파일의 기본 유지 기간은 168시간(7일)이며, spark config 수정을 통해 유지 기간을 조정할 수 있습니다.
### ▪ 1) 기본 유지 기간 설정 해제
- spark에 기본적으로 설정되어 있는 설정을 해제해 주어야 retention 기간을 커스텀하게 관리할 수 있습니다.
```python
# 설정 확인
spark.conf.get(&quot;spark.databricks.delta.retentionDurationCheck.enabled&quot;)
-&gt; &#39;true&#39;

# 유지 기간 설정 해제
spark.conf.set(&quot;spark.databricks.delta.retentionDurationCheck.enabled&quot;, &quot;false&quot;)</code></pre><h3 id="▪-2-vacuum-명령어-실행">▪ 2) VACUUM 명령어 실행</h3>
<ul>
<li>사용자가 지정한 기간 이전에 생성된 parquet 파일은 삭제하도록 <code>VACUUM</code> 명령을 수행합니다.</li>
<li><code>DRY RUN</code> 옵션은 실제로 작업은 되지 않도록 하는 설정입니다.<pre><code class="language-python"># 기본 VACUUM 명령 (168시간 이전의 파일 삭제)
spark.sql(&quot;VACUUM deltalake_db.trainer_delta&quot;).show(truncate=False)
</code></pre>
</li>
</ul>
<h1 id="현재-버전-이전의-파일들-삭제">현재 버전 이전의 파일들 삭제</h1>
<p>spark.sql(&quot;VACUUM deltalake_db.trainer_delta RETAIN 0 HOURS DRY RUN&quot;).show(truncate=False)</p>
<h1 id="2일-이전의-파일들-삭제">2일 이전의 파일들 삭제</h1>
<p>spark.sql(&quot;VACUUM deltalake_db.trainer_delta RETAIN 2 DAYS DRY RUN&quot;).show(truncate=False)</p>
<pre><code>
### ▪ 3) 테이블 생성시 유지 기간 설정
- `delta` 유형 테이블 생성시 기본 retention 기간을 설정합니다.
```python
query = f&quot;&quot;&quot;
CREATE TABLE IF NOT EXISTS deltalake_db.trainer_delta_2 (
  id INT,
  name STRING,
  age INT,
  hometown STRING,
  prefer_type STRING,
  badge_count INT,
  level STRING
)
USING delta
LOCATION &#39;/workspace/spark/deltalake/delta_local/trainer_delta_2/&#39;
TBLPROPERTIES (&#39;delta.deletedFileRetentionDuration&#39; = &#39;interval 2 days&#39;);
&quot;&quot;&quot;

spark.sql(query)</code></pre><hr>
<h2 id="🔟-parquet-to-delta-변환">🔟 Parquet to Delta 변환</h2>
<ul>
<li><a href="https://docs.databricks.com/aws/en/sql/language-manual/delta-convert-to-delta">https://docs.databricks.com/aws/en/sql/language-manual/delta-convert-to-delta</a></li>
<li><code>parquet</code> 형태로 저장되어 있던 데이터를 <code>delta</code> 유형의 테이블 데이터로 변환하는 기능입니다.</li>
</ul>
<h3 id="▪-1-일반-parquet-데이터-변환">▪ 1) 일반 parquet 데이터 변환</h3>
<p>👉 <code>fish_data.csv</code> 데이터를 parquet으로 저장합니다.</p>
<pre><code class="language-python"># csv 파일 읽어오기
fish = spark.read.option(&#39;header&#39;, &#39;true&#39;).csv(&#39;fish_data.csv&#39;)

# 로컬 디렉토리 저장 + Catalog 저장
fish.write \
    .mode(&#39;overwrite&#39;) \
    .format(&#39;parquet&#39;) \
    .option(&#39;path&#39;, &#39;/workspace/spark/deltalake/delta_local/fish_parquet/&#39;) \
    .saveAsTable(&#39;deltalake_db.fish_parquet&#39;)</code></pre>
<p>👉 <code>parquet</code>으로 저장되어 있던 데이터를 <code>delta</code>로 변환</p>
<pre><code class="language-python">query = &quot;&quot;&quot;
CONVERT TO DELTA
parquet.`/workspace/spark/deltalake/delta_local/fish_parquet/`
&quot;&quot;&quot;

spark.sql(query)</code></pre>
<h3 id="▪-2-파티션-된-parquet-데이터-변환">▪ 2) 파티션 된 parquet 데이터 변환</h3>
<p>👉 <code>Species</code> 컬럼으로 파티션 된 parquet 데이터 쓰기</p>
<pre><code class="language-python">fish_df.write.mode(&#39;overwrite&#39;)\
    .format(&#39;parquet&#39;) \
    .partitionBy(&#39;Species&#39;) \
    .option(&#39;path&#39;, &#39;/workspace/spark/deltalake/delta_local/fish_parquet_partitioned/&#39;) \
    .saveAsTable(&#39;deltalake_db.fish_parquet_partitioned&#39;)</code></pre>
<p>👉 파티션 된 parquet 데이터를 <code>delta</code>로 변환</p>
<pre><code class="language-python">query = &quot;&quot;&quot;
CONVERT TO DELTA 
parquet.`/workspace/spark/deltalake/delta_local/fish_parquet_partitioned/`
PARTITIONED BY (Species STRING)
&quot;&quot;&quot;
spark.sql(query)</code></pre>
<hr>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://docs.databricks.com/aws/en/delta/">Delta Lake Documentation</a></li>
<li><a href="https://docs.delta.io/latest/quick-start.html#set-up-apache-spark-with-delta-lake">Delta Lake Quickstart - Apache Spark</a></li>
<li><a href="https://www.youtube.com/watch?v=_98JHyHGXvA">𝑫𝒆𝒍𝒕𝒂 𝑳𝒂𝒌𝒆 𝒊𝒏 𝑨𝑾𝑺 - 𝑭𝒓𝒐𝒎 𝒁𝒆𝒓𝒐 𝒕𝒐 𝑯𝒆𝒓𝒐 𝒊𝒏 4 𝒉𝒐𝒖𝒓𝒔</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[📊 200+ 데이터 엔지니어 인터뷰에서 발견한 최상위 1%의 비결]]></title>
            <link>https://velog.io/@newnew_daddy/article02</link>
            <guid>https://velog.io/@newnew_daddy/article02</guid>
            <pubDate>Sun, 23 Mar 2025 14:23:25 GMT</pubDate>
            <description><![CDATA[<p>(해당 글은 아래 명시한 출처의 글을 한글로 각색 및 요약한 내용입니다.)
🔥 최상위 데이터 엔지니어들의 공통점</p>
<h3 id="1️⃣-데이터를-흐름으로-이해한다">1️⃣ 데이터를 흐름으로 이해한다</h3>
<p>이들은 데이터를 단순한 값이 아니라, 흐름(flow) 으로 봅니다.</p>
<p>분산 시스템(HDFS, S3), 배치 vs. 스트림 처리 차이를 직관적으로 이해함.</p>
<p>저장 포맷(Parquet, Avro) 장단점을 빠르게 판단할 수 있음.</p>
<p>👉 데이터를 &quot;어떻게 최적의 방식으로 전달할까?&quot;를 먼저 고민합니다.</p>
<hr>
<h3 id="2️⃣-단순한-코더가-아니라-문제-해결자다">2️⃣ 단순한 코더가 아니라, 문제 해결자다</h3>
<p>SQL만 잘하는 게 아닙니다. 복잡한 문제를 논리적으로 해결하는 능력이 뛰어납니다.</p>
<p>Python, Scala, Java를 능숙하게 다루며, 가독성 좋은 코드를 작성.</p>
<p>자료구조와 알고리즘을 이해하고, 성능 최적화 감각이 있음.</p>
<p>👉 이들은 &quot;어떻게 하면 더 효율적으로 문제를 해결할까?&quot;를 끊임없이 고민합니다.</p>
<hr>
<h3 id="3️⃣-성능-최적화를-당연하게-여긴다">3️⃣ 성능 최적화를 당연하게 여긴다</h3>
<p>이들은 파이프라인을 만들고 끝이 아니라, 최적화를 필수 과정으로 생각합니다.</p>
<p>Spark 디버깅 능력이 뛰어나고, 병목 현상을 빠르게 찾음.</p>
<p>파티셔닝, 버케팅을 활용해 대용량 데이터도 빠르게 처리.</p>
<p>👉 그냥 작동하는 코드가 아니라, &quot;최적의 성능을 내는 코드&quot;를 만듭니다.</p>
<hr>
<h3 id="4️⃣-클라우드를-제대로-활용한다">4️⃣ 클라우드를 제대로 활용한다</h3>
<p>단순히 클라우드를 사용하는 게 아니라, 비용과 성능을 고려한 최적의 조합을 찾습니다.</p>
<p>AWS, GCP, Azure 활용에 능숙하며, EMR, Databricks, Kubernetes의 차이를 명확히 이해.</p>
<p>Terraform 같은 IaC(Infrastructure as Code) 도구를 능숙하게 다룸.</p>
<p>👉 &quot;어떤 서비스를 사용할까?&quot;가 아니라, &quot;가장 효율적인 조합은 무엇일까?&quot;를 고민합니다.</p>
<hr>
<h3 id="5️⃣-데이터-품질을-철저히-관리한다">5️⃣ 데이터 품질을 철저히 관리한다</h3>
<p>많은 데이터보다 신뢰할 수 있는 데이터가 중요함을 압니다.</p>
<p>데이터 검증 및 모니터링(Great Expectations 등) 활용.</p>
<p>데이터가 언제, 어디서, 어떻게 생성되었는지 명확히 추적 가능.</p>
<p>👉 &quot;이 데이터가 정확한가?&quot;를 항상 먼저 고려합니다.</p>
<hr>
<h3 id="💡-최상위-데이터-엔지니어들의-마인드셋">💡 최상위 데이터 엔지니어들의 마인드셋</h3>
<p>✅ 주인의식 – 문제를 끝까지 해결하는 태도.
✅ 호기심 – 단순한 사용이 아니라, 원리를 깊이 이해.
✅ 실행력 – 이론이 아니라, 실제로 실험하고 개선.
✅ 비즈니스 감각 – 데이터를 통해 실제 가치를 창출하는 사고방식.</p>
<p>👉 이들은 &quot;좋은 엔지니어&quot;가 아니라, &quot;비즈니스에 기여하는 엔지니어&quot;입니다.</p>
<hr>
<h3 id="✨-최상위-1-데이터-엔지니어는-이렇게-다르다">✨ 최상위 1% 데이터 엔지니어는 이렇게 다르다</h3>
<p>✅ 데이터를 흐름으로 이해하고, 최적의 설계를 고민함.
✅ 성능 최적화가 몸에 배어 있음.
✅ 클라우드를 단순 사용이 아니라, 효율적으로 활용함.
✅ 데이터 품질을 철저히 관리하며, 신뢰할 수 있는 데이터를 다룸.
✅ 단순한 코더가 아니라, 문제 해결 능력을 갖춘 전문가.</p>
<p>💡 최고의 데이터 엔지니어들은 단순히 툴을 잘 쓰는 것이 아니라, 데이터로 가치를 만드는 사람들입니다.</p>
<hr>
<p>▶ 출처 : <a href="https://blog.det.life/i-interviewed-200-data-engineers-heres-what-separates-the-best-from-the-rest-3092524e5875">https://blog.det.life/i-interviewed-200-data-engineers-heres-what-separates-the-best-from-the-rest-3092524e5875</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🌊 Delta Lake 입문자를 위한 가이드 - 이론편]]></title>
            <link>https://velog.io/@newnew_daddy/data07</link>
            <guid>https://velog.io/@newnew_daddy/data07</guid>
            <pubDate>Thu, 20 Mar 2025 06:35:05 GMT</pubDate>
            <description><![CDATA[<ul>
<li>Delta Lake 이론 - <a href="https://velog.io/@newnew_daddy/data07">🌊 Delta Lake 입문자를 위한 가이드 - 이론편</a></li>
<li>Delta Lake 실전 Part 1 - <a href="https://velog.io/@newnew_daddy/data08">🌊 Delta Lake 입문자를 위한 가이드 - 실전편(Part 1. 로컬 환경)</a></li>
<li>Delta Lake 실전 Part 2 - <a href="https://velog.io/@newnew_daddy/data09">🌊 Delta Lake 입문자를 위한 가이드 - 실전편(Part 2. delta-spark 라이브러리 활용)</a></li>
</ul>
<hr>
<h2 id="0-delta-lake란-무엇인가">0. Delta Lake란 무엇인가?</h2>
<p>Delta Lake는 data lakehouse 아키텍처를 구축할 수 있는 오픈 소스 스토리지 레이어로, 기존의 데이터 레이크(Lake)에 트랜잭션 기능과 데이터 무결성 보장을 추가하여 데이터 웨어하우스 수준의 신뢰성과 성능을 제공하는 기술입니다.</p>
<p>주로 데이터 엔지니어링, 데이터 분석, 데이터 파이프라인 등 데이터를 다루기 위한 다양한 에서 활용되며, 데이터의 변경 이력을 관리하는 기능까지 제공합니다.</p>
<h2 id="1-delta-lake가-등장한-배경">1. Delta Lake가 등장한 배경</h2>
<h3 id="1-클라우드-객체-저장소의-부상">1) 클라우드 객체 저장소의 부상</h3>
<p>과거에는 HDFS 기반의 물리적인 서버 자원을 활용한 데이터 레이크가 널리 사용되었지만, 최근에는 Amazon S3, Google Cloud Storage와 같은 클라우드 오브젝트 스토리지가 대체하고 있습니다. 이들의 장점은 다음과 같습니다.</p>
<p>✅ 사실상 무제한의 확장성
✅ 사용한 만큼만 비용 지불
✅ 높은 내구성 및 안정성 보장</p>
<p>데이터 처리시 사용되는 Apache Spark, Presto, Trino, Pandas, DuckDB 등의 도구들 역시 클라우드 객체 저장소와의 연동을 지원해주고 있으며, 클라우드 객체 저장소를 기반으로 데이터를 읽고 저장하는 과정이 표준으로 자리잡고 있습니다.</p>
<h3 id="2-기존-데이터-레이크의-한계">2) 기존 데이터 레이크의 한계</h3>
<p>전통적인 데이터 레이크(Hadoop HDFS, AWS S3, Azure Data Lake 등)는 대량의 데이터를 저장하기에는 적합하지만, 파일 기반이기 때문에 데이터 웨어하우스 수준의 쿼리나 분석 기능을 제공하는데는 한계가 있었습니다. 주요한 문제점들은 아래와 같습니다.</p>
<p>1️⃣ 데이터 정합성 부족</p>
<ul>
<li>데이터가 여러 파일로 분산 저장되기 때문에, 중간에 작업이 실패하면 데이터가 불완전하게 저장될 가능성이 존재합니다.</li>
<li>예를 들어, 한 테이블에 100개의 파일이 저장되어 있고, 새로운 데이터를 추가하는 도중 작업이 중단되거나 실패하게 된다면 데이터의 일부만 기록되어 데이터 정합성이 깨질 수 있는 것이죠.</li>
</ul>
<p>2️⃣ ACID 트랜잭션 미지원</p>
<ul>
<li>파일 형태로 관리되는 데이터레이크는 데이터베이스처럼 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장하지 않습니다.</li>
<li>따라서 여러 사용자가 동시에 데이터를 수정하거나 추가할 경우, 예측할 수 없는 충돌이나 데이터 손실이 발생할 가능성이 높습니다.</li>
</ul>
<p>3️⃣ Schema Evolution(스키마 변경) 문제</p>
<ul>
<li>기존에 저장되어 있던 데이터의 스키마(컬럼, 데이터 타입 등)를 변경하려면 전체 데이터를 새롭게 덮어쓰는 등 큰 작업을 요하는 경우가 많습니다.</li>
</ul>
<p>4️⃣ 데이터 버전 관리 부재</p>
<ul>
<li>동일한 저장 경로에 데이터가 추가되거나 삭제될 경우, 기존 데이터와의 변경 사항을 쉽게 추적할 수 없습니다.</li>
<li>이전의 데이터로 되돌아가는 롤백 기능이 없어 작업중 데이터를 삭제하거나 덮어썼다면 다시 복구하기가 어렵습니다.</li>
</ul>
<p>이러한 문제들을 해결하기 위해 Databricks는 Delta Lake를 개발하였고, 이를 2017년에 고객에게 제공한 후 2019년 오픈소스로 공개했습니다.</p>
<h2 id="2-delta-lake의-핵심-설계-원리">2. Delta Lake의 핵심 설계 원리</h2>
<p>Delta Lake의 핵심 원리는 객체 저장소에 파일 기반으로 저장되어 있는 데이터들에 대하여 ACID 트랜잭션을 보장하는 것입니다. 
Delta Lake는 Apache Parquet을 기반으로 동작하며, Delta Log라는 추가적인 메타데이터 로그 파일을 사용하여 ACID 트랜잭션과 데이터 버전 관리를 수행합니다.</p>
<p>1️⃣ Raw Data → 원본 데이터 파일 (Parquet)
2️⃣ Delta Log → 모든 변경 사항이 기록되는 로그 파일
<img src="https://velog.velcdn.com/images/newnew_daddy/post/cca0ec79-5bfb-4725-a628-f65d8cd5c3a4/image.png" alt=""></p>
<p>✅ ACID 트랜잭션 보장</p>
<ul>
<li>Delta Lake는 Parquet 파일을 기반으로 데이터를 저장하면서도 데이터와 같이 관리되는 <code>delta_log</code> 메타데이터 로그를 통해 ACID 트랜잭션을 지원합니다.</li>
<li>따라서 아래와 같은 기능 구현이 가능해졌습니다.<ul>
<li>여러 데이터를 동시에 업데이트 가능.</li>
<li>이전 데이터로의 자유로운 롤백 가능.</li>
<li>데이터 일관성을 유지하면서도 높은 성능 제공.</li>
</ul>
</li>
</ul>
<p>이렇게 Delta Log를 활용하면 데이터가 언제, 어떻게 변경되었는지 추적할 수 있고, 트랜잭션 충돌을 방지할 수도 있습니다.</p>
<h2 id="3-delta-lake의-핵심-기능">3. Delta Lake의 핵심 기능</h2>
<p>1️⃣ ACID 트랜잭션 지원</p>
<ul>
<li>Delta Lake는 ACID 트랜잭션을 보장하여 데이터 정합성을 유지합니다. 즉, 여러 사용자가 동시에 데이터를 수정하거나 삽입해도 데이터가 일관되게 유지됩니다.</li>
</ul>
<p>2️⃣ 데이터 버전 관리 (Time Travel)</p>
<ul>
<li>Delta Lake는 모든 데이터 변경 사항을 버전 관리합니다. 따라서 파일 기반으로 저장되어 있지만 특정 시점의 데이터로 롤백이 가능합니다.</li>
<li>실수로 데이터를 삭제하거나 덮어 쓰더라도 과거 버전으로 복구가 가능한 것이죠. 예를 들어, &quot;어제의 데이터 상태로 돌아가고 싶다!&quot;라고 할 때, 별도의 백업 없이도 특정 버전을 선택하여 데이터 조회가 가능합니다.</li>
</ul>
<p>3️⃣ 스키마 관리 (Schema Management)</p>
<ul>
<li>Schema Enforcement(스키마 강제 적용)<ul>
<li>기존 데이터와 맞지 않는 스키마가 들어오는 경우 오류를 발생시켜 데이터 무결성을 유지합니다.</li>
</ul>
</li>
<li>Schema Evolution(스키마 변경 지원)<ul>
<li>기존 테이블의 컬럼을 추가/삭제하는 등의 스키마 변경 작업을 쉽게 적용할 수 있습니다.</li>
</ul>
</li>
</ul>
<p>4️⃣ 데이터 파일 최적화 (File Compaction)</p>
<ul>
<li>데이터 레이크에서는 작은 용량의 파일들이 디렉토리 내에 많이 생성될 경우 성능이 저하될 수 있습니다. Delta Lake는 자동으로 작은 파일들을 병합하여 데이터 저장을 최적화시킬 수 있습니다.</li>
<li>아래의 최적화 메소드를 활용하여 대규모 데이터를 다룰 때 성능 문제를 줄일 수 있습니다.<ul>
<li><code>OPTIMIZE</code> : 작은 파일을 합쳐서 성능을 개선</li>
<li><code>Z-Ordering</code> : 여러 개의 컬럼을 기준으로 데이터를 정렬하여 쿼리 성능 향상</li>
</ul>
</li>
</ul>
<p>5️⃣ 데이터 정리 및 삭제 (Data Vacuuming)</p>
<ul>
<li>저장된 데이터에 Retention Policy를 설정하면 일정 기간이 지난 불필요한 데이터를 자동으로 삭제할 수 있습니다.</li>
<li>Delta Lake에서 데이터 파일을 삭제하게되면 정합성 문제가 발생할 수 있으므로 특정 시점 이전의 데이터를 삭제해야 할 경우에는 반드시 <code>VACUUM</code> 기능을 활용하여 파일을 삭제합니다. </li>
</ul>
<p>6️⃣ 캐싱 (Caching)</p>
<ul>
<li>Delta Lake는 클러스터 내에서 데이터 및 메타데이터 캐싱을 제공하여 성능을 더욱 향상시킵니다.</li>
</ul>
<p>7️⃣ 감사 로그 (Audit Logging)</p>
<ul>
<li>모든 데이터 변경 내역을 기록하여 누가 언제 어떤 데이터를 수정했는지 추적 가능합니다.</li>
</ul>
<p>8️⃣ 스트리밍 데이터 처리</p>
<ul>
<li>배치 처리와 스트리밍 처리를 모두 지원합니다.</li>
<li>Delta Lake 테이블을 스트리밍 원본 및 싱크(Sink)로 사용할 수 있습니다.</li>
</ul>
<h2 id="4-기존의-데이터-아키텍처와의-비교">4. 기존의 데이터 아키텍처와의 비교</h2>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>기존 데이터 레이크</th>
<th>기존 데이터 웨어하우스</th>
<th>Delta Lake</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 저장 방식</td>
<td>오브젝트 스토리지 (Parquet, CSV 등)</td>
<td>행/컬럼 기반 스토리지</td>
<td>오브젝트 스토리지 (Parquet + Delta Log)</td>
</tr>
<tr>
<td>트랜잭션 지원</td>
<td>❌ 미지원</td>
<td>✅ ACID 보장</td>
<td>✅ ACID 보장</td>
</tr>
<tr>
<td>데이터 버전 관리</td>
<td>❌ 불가능</td>
<td>⚠ 일부만 가능</td>
<td>✅ Time Travel 가능</td>
</tr>
<tr>
<td>성능 최적화</td>
<td>❌ 없음</td>
<td>✅ 지원</td>
<td>✅ 지원</td>
</tr>
<tr>
<td>스키마 변경</td>
<td>⚠ 어렵거나 불가능</td>
<td>✅ 지원</td>
<td>✅ 지원</td>
</tr>
<tr>
<td>활용 사례</td>
<td>데이터 저장소</td>
<td>데이터 분석, OLAP</td>
<td>데이터 저장소, 빅데이터 분석</td>
</tr>
</tbody></table>
<h2 id="5-outro">5. OUTRO</h2>
<p>Delta Lake, Hudi, Iceberg와 같은 Open Table Format이 나온지는 몇 년이 되었지만 저는 최근에서야 본격적으로 학습을 시작했습니다.
이전 회사에서도 데이터를 Parquet 형태로 관리했지만, 주로 업데이트된 테이블을 덮어쓰거나 새로운 디렉토리를 생성하는 방식으로 버전 관리를 했었죠. 
그런 경험이 있어서인지, <code>파일 기반 저장</code> + <code>AICD 트랜잭션 지원</code> + <code>롤백</code> 이 모든 기능이 파일 기반으로 가능한 Open Table Format은 저에게는 혁신적인 개념으로 다가왔습니다. 가까운 미래까지는 이 기술이 데이터를 저장하고 관리하는데 있어 표준으로 자리매김 할 것이라는 생각이 들었습니다.
이번 글에서는 Delta Lake의 이론적 개념을 정리해 보았고, 다음 글에서는 직접 실습을 진행하며 실무에서 어떻게 활용할 수 있을지 살펴보겠습니다. </p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://www.qlik.com/us/data-lake/delta-lake">https://www.qlik.com/us/data-lake/delta-lake</a></li>
<li><a href="https://medium.com/@ansam.yousry/understand-delta-lake-a-comprehensive-guide-a0b4beaeafe3">https://medium.com/@ansam.yousry/understand-delta-lake-a-comprehensive-guide-a0b4beaeafe3</a></li>
<li><a href="https://blog.det.life/i-spent-5-hours-understanding-more-about-the-delta-lake-table-format-b8516c5091eb">https://blog.det.life/i-spent-5-hours-understanding-more-about-the-delta-lake-table-format-b8516c5091eb</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[❗데이터 엔지니어링의 현실, 화려함 뒤에 숨겨진 10가지 뼈 때리는 진실🤕]]></title>
            <link>https://velog.io/@newnew_daddy/article01</link>
            <guid>https://velog.io/@newnew_daddy/article01</guid>
            <pubDate>Wed, 12 Mar 2025 07:58:32 GMT</pubDate>
            <description><![CDATA[<p>(해당 글은 아래 명시한 출처의 글을 한글로 각색 및 요약한 내용입니다.)</p>
<h3 id="❗데이터-엔지니어링의-현실-화려함-뒤에-숨겨진-10가지-뼈-때리는-진실🤕">❗데이터 엔지니어링의 현실, 화려함 뒤에 숨겨진 10가지 뼈 때리는 진실🤕</h3>
<p>대용량 데이터, 최첨단 기술, 실시간 분석, 멋진 대시보드…... 데이터 엔지니어링에 대한 이야기는 늘 화려하죠. 하지만 그 뒤에 숨겨진 매일의 고군분투, 혼란, 그리고 눈에 띄지 않는 노력들은 수면 위로 잘 드러나지 않습니다. 
데이터 엔지니어링에 대한 솔직하고 현실적인 이야기들을 들려드릴게요.</p>
<hr>
<h3 id="1️⃣-예상보다-훨씬-더-엉망진창인-데이터-🗑️">1️⃣ 예상보다 훨씬 더 엉망진창인 데이터 🗑️</h3>
<p>▪ 아무리 최첨단 ETL 파이프라인을 구축해도, 결국 시간의 80%는 데이터를 정리하고, 변환하고, 검증하는 데 쓰게 될 거예요. 오타, 누락된 값, 잘못된 형식, 중복된 기록들이 매일매일 여러분을 괴롭힐 겁니다.
👉 진실 → 최고의 데이터 파이프라인도 엉망인 소스 데이터를 커버할 순 없어요. 쓰레기를 넣으면 쓰레기가 나올 뿐!</p>
<hr>
<h3 id="2️⃣-마법을-기대하는-사람들-🤖">2️⃣ 마법을 기대하는 사람들 🤖</h3>
<p>▪ 기술 지식이 없는 사람들은 데이터 엔지니어를 마법사라고 생각해요. 손가락만 튕기면 망가진 보고서를 고치고, 쿼리 속도를 높이고, 실시간 대시보드를 뚝딱 만들 수 있다고 믿죠.
👉 진실 → 기대치를 관리하는 것도 업무의 일부입니다. &quot;안돼요!&quot; 라고 말하지 못하면, 영원히 불 끄는 소방관 신세를 벗어날 수 없을 거예요.</p>
<hr>
<h3 id="3️⃣-확장은-구축보다-10배-더-어렵다-🤯">3️⃣ 확장은 구축보다 10배 더 어렵다 🤯</h3>
<p>▪ 하루에 1만 건의 레코드를 처리하는 멋진 데이터 파이프라인을 만드는 건 쉬운 일이에요. 하지만 그 규모가 수백만, 수십억 건으로 늘어나면 완전히 다른 이야기가 되죠. 지연 시간 급증, 예상치 못한 병목 현상, 폭발적인 스토리지 비용 증가가 매일 여러분을 괴롭힐 거예요. 💣
👉 진실 → 효율적인 파이프라인뿐만 아니라, 회복 탄력성이 뛰어난 파이프라인이 필요해요.</p>
<hr>
<h3 id="4️⃣-지루하지만-중요한-데이터-거버넌스-📜">4️⃣ 지루하지만 중요한 데이터 거버넌스 📜</h3>
<p>▪ 모두가 실시간 분석과 AI 기반 인사이트를 원하지만, 데이터 업데이트 하나 잘못되면 핵심 대시보드가 망가지고 관련 부서들에서 연락이 올 수도 있어요. 🚨
👉 진실 → 메타데이터 관리, 데이터 계보 추적, 접근 제어 등은 재미없지만, 여러분의 직장을 지켜줄 거예요.</p>
<hr>
<h3 id="5️⃣-배치-vs-스트리밍-경우에-따라-다르다-🤔">5️⃣ 배치 vs 스트리밍? &quot;경우에 따라 다르다&quot; 🤔</h3>
<p>▪ 기술 블로그는 스트리밍 파이프라인을 엄청나게 홍보하지만, 실제 비즈니스 활용 사례의 90%는 배치 처리로도 충분히 해결할 수 있어요. 하지만 사람들은 비용과 복잡성을 고려하지 않고 무작정 &quot;실시간&quot;을 외치죠.
👉 진실 → 대부분의 경우 배치 처리가 더 저렴하고, 간단하고, 안정적이에요. 스트리밍은 정말 필요할 때만 구축하세요!</p>
<hr>
<h3 id="6️⃣-반복적인-업무의-연속-🔁">6️⃣ 반복적인 업무의 연속 🔁</h3>
<p>▪ 항상 최첨단의 프로젝트들만 하는 건 아니에요. 때로는 똑같은 파이프라인을 유지 관리하고, 실패한 작업을 수정하고, 느린 쿼리를 열 번이나 다시 작성해야 할 수도 있죠.
👉 진실 → 최고의 데이터 엔지니어는 지루한 업무를 기꺼이 감수해요. 참신함보다는 안정성이 더 중요하니까요.</p>
<hr>
<h3 id="7️⃣-모든-잘못은-데이터-엔지니어-탓-🤷">7️⃣ 모든 잘못은 데이터 엔지니어 탓? 🤷‍</h3>
<p>▪ 대시보드에 오류가 있으면, 데이터 엔지니어가 가장 먼저 비난을 받아요. 소스 데이터가 잘못됐는지, 변환 설정이 잘못됐는지, 분석가가 결과를 잘못 해석했는지 아무도 묻지 않죠.
👉 진실 → 선제적으로 대처하세요. 모니터링 시스템을 구축하고, 알림을 설정하고, 작업 내용을 문서화하세요.</p>
<hr>
<h3 id="8️⃣-만능-도구는-없다-🛠️">8️⃣ 만능 도구는 없다! 🛠️</h3>
<p>▪ Databricks, Snowflake, Airflow, dbt 등 다양한 도구가 자동화와 간편함을 제공하지만, 결국 도구는 사용하는 사람만큼의 가치만 있을 뿐이에요. 🧰
👉 진실 → 새로운 도구를 쫓아다니는 대신, 기본 원리(SQL, 분산 시스템, 데이터 모델링 등)를 마스터하세요.</p>
<hr>
<h3 id="9️⃣-성공적인-데이터-엔지니어링은-눈에-띄지-않는다-👻">9️⃣ 성공적인 데이터 엔지니어링은 눈에 띄지 않는다 👻</h3>
<p>▪ 모든 것이 순조롭게 실행되면 아무도 알아채지 못해요. 문제가 발생했을 때만 관심을 받죠. 성공적인 데이터 엔지니어는 너무나 안정적으로 일을 처리해서, 사람들이 그 존재를 잊어버리게 만드는 사람이에요.
👉 진실 → 끊임없는 칭찬을 원한다면, 데이터 엔지니어링은 당신에게 맞지 않을 거예요.</p>
<hr>
<h3 id="🔟-ai가-당신을-대체하는-게-아니라-게으른-엔지니어가-대체될-것이다-🤖">🔟 AI가 당신을 대체하는 게 아니라, 게으른 엔지니어가 대체될 것이다. 🤖</h3>
<p>▪ AI가 ETL 작업을 자동화하고 SQL 쿼리를 생성하면서, 데이터 엔지니어링 일자리가 사라질 것이라고 믿는 사람들도 있어요. 😥
진실은... 게으른 엔지니어는 대체되겠지만, 훌륭한 엔지니어는 계속 성장해 나갈 거예요. 🌟
👉 진실 → AI는 위협이 아니라 도구예요. AI를 활용하는 방법을 배우세요.</p>
<p>그럼에도 불구하고 데이터 엔지니어링은 기술 분야에서 큰 영향력을 가진 직업 중 하나입니다. 현실적인 문제를 해결하고, 혼란을 다스리고, 안정적인 시스템을 구축하는 데 희열을 느낀다면, 분명 이 일을 사랑하게 될 거예요!</p>
<hr>
<p>▶원글 출처 : <a href="https://medium.com/@shenoy.shashwath/10-hard-truths-about-data-engineering-no-one-tells-you-a9e080ecfef1">https://medium.com/@shenoy.shashwath/10-hard-truths-about-data-engineering-no-one-tells-you-a9e080ecfef1</a> </p>
]]></description>
        </item>
    </channel>
</rss>