CHAPTER 04

๐Ÿ”— Data Lineage Tracking

๋ฐ์ดํ„ฐ ๊ณ„๋ณด ์ถ”์ ๊ณผ ์˜ํ–ฅ๋„ ๋ถ„์„

4.1 ๋ฐ์ดํ„ฐ ๊ณ„๋ณด๋ž€?

Data Lineage(๋ฐ์ดํ„ฐ ๊ณ„๋ณด)๋Š” ๋ฐ์ดํ„ฐ์˜ ์ถœ์ฒ˜๋ถ€ํ„ฐ ์ตœ์ข… ๋ชฉ์ ์ง€๊นŒ์ง€์˜ ์ „์ฒด ์—ฌ์ •์„ ์ถ”์ ํ•˜๊ณ  ์‹œ๊ฐํ™”ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋””์„œ ์™”๊ณ , ์–ด๋–ป๊ฒŒ ๋ณ€ํ™˜๋˜์—ˆ์œผ๋ฉฐ, ์–ด๋””๋กœ ํ˜๋Ÿฌ๊ฐ€๋Š”์ง€๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

๐Ÿ’ก ์™œ ๋ฐ์ดํ„ฐ ๊ณ„๋ณด๊ฐ€ ์ค‘์š”ํ•œ๊ฐ€?
  • ์‹ ๋ขฐ์„ฑ: "์ด ๋Œ€์‹œ๋ณด๋“œ์˜ ์ˆซ์ž๋Š” ์ •ํ™•ํ•œ๊ฐ€?" ์งˆ๋ฌธ์— ๋‹ตํ•  ์ˆ˜ ์žˆ์Œ
  • ์˜ํ–ฅ๋„ ๋ถ„์„: ํ…Œ์ด๋ธ” ๋ณ€๊ฒฝ ์‹œ ์˜ํ–ฅ๋ฐ›๋Š” ๋ชจ๋“  ๋‹ค์šด์ŠคํŠธ๋ฆผ ํŒŒ์•…
  • ๊ทœ์ œ ์ค€์ˆ˜: GDPR, CCPA ๋“ฑ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์ถ”์  ์š”๊ตฌ์‚ฌํ•ญ ์ถฉ์กฑ
  • ๋””๋ฒ„๊น…: ๋ฐ์ดํ„ฐ ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ์›์ธ ์ถ”์ 
  • ์ตœ์ ํ™”: ๋ถˆํ•„์š”ํ•œ ETL ํŒŒ์ดํ”„๋ผ์ธ ์‹๋ณ„

4.2 ๊ณ„๋ณด์˜ 3๊ฐ€์ง€ ๋ ˆ๋ฒจ

1) ๋ฐ์ดํ„ฐ์…‹ ๋ ˆ๋ฒจ ๊ณ„๋ณด (Coarse-grained)

ํ…Œ์ด๋ธ”/ํŒŒ์ผ ๋‹จ์œ„์˜ ์˜์กด์„ฑ์„ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.

raw_customers โ†’ stg_customers โ†’ dim_customers โ†’ customer_report

2) ์ปฌ๋Ÿผ ๋ ˆ๋ฒจ ๊ณ„๋ณด (Fine-grained)

๊ฐœ๋ณ„ ์ปฌ๋Ÿผ์˜ ๋ณ€ํ™˜๊ณผ ํ๋ฆ„์„ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.

orders.total_amount โ†’ customer_metrics.total_revenue
orders.discount โ†’ customer_metrics.total_revenue (SUM aggregation)

3) ํ•„๋“œ ๋ ˆ๋ฒจ ๊ณ„๋ณด (Ultra-fine-grained)

๊ฐœ๋ณ„ ๊ฐ’์˜ ๋ณ€ํ™˜ ๋กœ์ง๊นŒ์ง€ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.

customer.email โ†’ LOWER(customer.email) โ†’ HASH(email) โ†’ anonymized_email

4.3 ๊ณ„๋ณด ๊ทธ๋ž˜ํ”„ ๊ตฌ์กฐ

๋ฐ์ดํ„ฐ ๊ณ„๋ณด๋Š” ๋ฐฉํ–ฅ์„ฑ ๋น„์ˆœํ™˜ ๊ทธ๋ž˜ํ”„(DAG, Directed Acyclic Graph)๋กœ ํ‘œํ˜„๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋ž˜ํ”„ ๊ตฌ์„ฑ ์š”์†Œ

๊ณ„๋ณด ๊ทธ๋ž˜ํ”„ JSON ํ‘œํ˜„:
{
  "nodes": [
    {"id": "src_orders", "type": "TABLE", "platform": "PostgreSQL"},
    {"id": "stg_orders", "type": "TABLE", "platform": "Snowflake"},
    {"id": "fct_orders", "type": "TABLE", "platform": "Snowflake"}
  ],
  "edges": [
    {"from": "src_orders", "to": "stg_orders", "process": "etl_ingest"},
    {"from": "stg_orders", "to": "fct_orders", "process": "dbt_transform"}
  ],
  "processes": [
    {"id": "etl_ingest", "type": "SPARK_JOB", "schedule": "0 2 * * *"},
    {"id": "dbt_transform", "type": "DBT_MODEL", "schedule": "0 3 * * *"}
  ]
}

4.4 ๊ณ„๋ณด ์ž๋™ ์ถ”์  ๋ฐฉ๋ฒ•

1) SQL ํŒŒ์‹ฑ

SQL ์ฟผ๋ฆฌ๋ฅผ ๋ถ„์„ํ•˜์—ฌ ํ…Œ์ด๋ธ” ๊ฐ„ ์˜์กด์„ฑ์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.

-- SQL ์ฟผ๋ฆฌ
CREATE TABLE customer_summary AS
SELECT
    c.customer_id,
    c.email,
    COUNT(o.order_id) as order_count,
    SUM(o.total_amount) as total_spent
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.email;

-- ์ถ”์ถœ๋œ ๊ณ„๋ณด
Upstream: customers, orders
Downstream: customer_summary
Columns: customers.customer_id, customers.email,
         orders.order_id, orders.total_amount

2) ์‹คํ–‰ ๋กœ๊ทธ ๋ถ„์„

Spark, Airflow ๋“ฑ์˜ ์‹คํ–‰ ๋กœ๊ทธ์—์„œ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.

3) API ํ›„ํ‚น

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋“œ๋ผ์ด๋ฒ„, Spark API์— ํ›„ํฌ๋ฅผ ์„ค์น˜ํ•˜์—ฌ ์‹ค์‹œ๊ฐ„ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.

4) ๋ฉ”ํƒ€์Šคํ† ์–ด ํ†ตํ•ฉ

Hive Metastore, AWS Glue ๋“ฑ์—์„œ ๊ณ„๋ณด ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

4.5 ์ฃผ์š” ํ”Œ๋žซํผ๋ณ„ ๊ณ„๋ณด ์ถ”์ 

ํ”Œ๋žซํผ ๊ณ„๋ณด ์ถ”์  ๋ฐฉ๋ฒ• ๋ ˆ๋ฒจ
Apache Spark Listener API, Query Plans Dataset, Column
dbt manifest.json ํŒŒ์ผ Dataset, Column
Airflow Lineage Backend Dataset
Snowflake ACCESS_HISTORY view Dataset, Column
BigQuery Audit Logs, INFORMATION_SCHEMA Dataset

dbt ๊ณ„๋ณด ์ถ”์ถœ ์˜ˆ์‹œ

import json

# manifest.json ํŒŒ์ผ ์ฝ๊ธฐ
with open('target/manifest.json') as f:
    manifest = json.load(f)

# ๋ชจ๋ธ ๊ณ„๋ณด ์ถ”์ถœ
for node_id, node in manifest['nodes'].items():
    if node['resource_type'] == 'model':
        print(f"Model: {node['name']}")
        print(f"  Depends on: {node['depends_on']['nodes']}")
        print(f"  Columns: {list(node['columns'].keys())}")

4.6 ์˜ํ–ฅ๋„ ๋ถ„์„ (Impact Analysis)

ํŠน์ • ๋ฐ์ดํ„ฐ์…‹์„ ๋ณ€๊ฒฝํ•  ๋•Œ, ์˜ํ–ฅ์„ ๋ฐ›๋Š” ๋ชจ๋“  ๋‹ค์šด์ŠคํŠธ๋ฆผ์„ ์ž๋™์œผ๋กœ ํŒŒ์•…ํ•ฉ๋‹ˆ๋‹ค.

์‹œ๋‚˜๋ฆฌ์˜ค: customers ํ…Œ์ด๋ธ”์˜ email ์ปฌ๋Ÿผ ์‚ญ์ œ
์˜ํ–ฅ๋ฐ›๋Š” ๋‹ค์šด์ŠคํŠธ๋ฆผ:
1. stg_customers.email (์ง์ ‘ ์˜์กด)
2. dim_customers.email_domain (ํŒŒ์ƒ ์ปฌ๋Ÿผ)
3. customer_segments (์„ธ๊ทธ๋จผํŠธ ๊ณ„์‚ฐ์— ์‚ฌ์šฉ)
4. marketing_campaigns (ํƒ€๊ฒŸํŒ…์— ์‚ฌ์šฉ)
5. customer_analytics_dashboard (Tableau ๋Œ€์‹œ๋ณด๋“œ)

์ด ์˜ํ–ฅ: 5๊ฐœ ๋ฐ์ดํ„ฐ์…‹, 12๊ฐœ ๋ฆฌํฌํŠธ, 3๊ฐœ ๋Œ€์‹œ๋ณด๋“œ

์˜ํ–ฅ๋„ ๋ถ„์„ ์•Œ๊ณ ๋ฆฌ์ฆ˜

def analyze_impact(dataset_id, change_type):
    """
    ์ฃผ์–ด์ง„ ๋ฐ์ดํ„ฐ์…‹ ๋ณ€๊ฒฝ์˜ ์˜ํ–ฅ๋„๋ฅผ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค.
    """
    visited = set()
    impact = []

    def dfs_downstream(node_id, depth=0):
        if node_id in visited:
            return
        visited.add(node_id)

        # ํ˜„์žฌ ๋…ธ๋“œ์˜ ๋‹ค์šด์ŠคํŠธ๋ฆผ ์ฐพ๊ธฐ
        downstream = get_downstream_nodes(node_id)

        for ds_node in downstream:
            impact.append({
                'node': ds_node,
                'depth': depth + 1,
                'impact_type': calculate_impact_type(change_type, ds_node)
            })
            dfs_downstream(ds_node['id'], depth + 1)

    dfs_downstream(dataset_id)
    return impact

4.7 ๊ณ„๋ณด ์‹œ๊ฐํ™”

๊ณ„๋ณด ๊ทธ๋ž˜ํ”„๋ฅผ ์‚ฌ์šฉ์ž๊ฐ€ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ์‹œ๊ฐํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

์‹œ๊ฐํ™” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

๐Ÿ’ก ์‹œ๊ฐํ™” ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค
  • ๊ณ„์ธต์  ๋ ˆ์ด์•„์›ƒ ์‚ฌ์šฉ (์™ผ์ชฝ โ†’ ์˜ค๋ฅธ์ชฝ ๋˜๋Š” ์œ„ โ†’ ์•„๋ž˜)
  • ํ”Œ๋žซํผ๋ณ„๋กœ ์ƒ‰์ƒ ์ฝ”๋”ฉ
  • ์ค‘์š”ํ•œ ๋…ธ๋“œ ๊ฐ•์กฐ ํ‘œ์‹œ
  • ํ™•๋Œ€/์ถ•์†Œ, ํ•„ํ„ฐ๋ง ๊ธฐ๋Šฅ ์ œ๊ณต
  • ๋…ธ๋“œ ํด๋ฆญ ์‹œ ์ƒ์„ธ ์ •๋ณด ํŒ์—…

4.8 ์ปฌ๋Ÿผ ๋ ˆ๋ฒจ ๊ณ„๋ณด

ํ…Œ์ด๋ธ” ๋ ˆ๋ฒจ๋ณด๋‹ค ๋” ์„ธ๋ฐ€ํ•œ ์ปฌ๋Ÿผ ๋‹จ์œ„ ๊ณ„๋ณด๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.

์ปฌ๋Ÿผ ๋ณ€ํ™˜ ์ถ”์ :
-- ์›๋ณธ SQL
SELECT
    LOWER(TRIM(email)) as clean_email,
    CASE
        WHEN lifetime_value > 10000 THEN 'VIP'
        WHEN lifetime_value > 1000 THEN 'Regular'
        ELSE 'New'
    END as customer_segment
FROM customers;

-- ์ถ”์ถœ๋œ ์ปฌ๋Ÿผ ๊ณ„๋ณด
clean_email:
  - source: customers.email
  - transformations: [TRIM, LOWER]

customer_segment:
  - source: customers.lifetime_value
  - transformations: [CASE WHEN segmentation]
  - logic: "VIP: >10000, Regular: >1000, New: else"

4.9 ๊ณ„๋ณด ์ €์žฅ์†Œ

๊ทธ๋ž˜ํ”„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

๊ณ„๋ณด๋Š” ๋ณธ์งˆ์ ์œผ๋กœ ๊ทธ๋ž˜ํ”„ ๊ตฌ์กฐ์ด๋ฏ€๋กœ ๊ทธ๋ž˜ํ”„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

Neo4j ์˜ˆ์‹œ

// ๋…ธ๋“œ ์ƒ์„ฑ
CREATE (c:Dataset {
    name: 'customers',
    platform: 'PostgreSQL',
    type: 'TABLE'
})

CREATE (s:Dataset {
    name: 'customer_summary',
    platform: 'Snowflake',
    type: 'TABLE'
})

// ๊ด€๊ณ„ ์ƒ์„ฑ
CREATE (c)-[:FEEDS_INTO {
    process: 'etl_pipeline',
    transformation: 'aggregation'
}]->(s)

// ๋‹ค์šด์ŠคํŠธ๋ฆผ ์กฐํšŒ
MATCH (d:Dataset {name: 'customers'})-[:FEEDS_INTO*]->(downstream)
RETURN downstream.name, downstream.platform;

4.10 ์‹ค์ „ ํ™œ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค

์‹œ๋‚˜๋ฆฌ์˜ค 1: ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ ์ด์Šˆ ์ถ”์ 

๋Œ€์‹œ๋ณด๋“œ์—์„œ ์ด์ƒํ•œ ์ˆ˜์น˜๊ฐ€ ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ณ„๋ณด๋ฅผ ์—ญ์ถ”์ ํ•˜์—ฌ ๋ฌธ์ œ์˜ ๊ทผ์›์„ ์ฐพ์Šต๋‹ˆ๋‹ค.

๋Œ€์‹œ๋ณด๋“œ โ†’ BI ๋ ˆ์ด์–ด โ†’ DW ํ…Œ์ด๋ธ” โ†’ ETL โ†’ ์†Œ์Šค DB
์—ญ์ถ”์  ๊ฒฐ๊ณผ: ์†Œ์Šค DB์—์„œ NULL ๊ฐ’์ด ์ฆ๊ฐ€ํ•จ โ†’ ETL์ด ์ž˜๋ชป ์ฒ˜๋ฆฌ
์‹œ๋‚˜๋ฆฌ์˜ค 2: GDPR ๋ฐ์ดํ„ฐ ์‚ญ์ œ ์š”์ฒญ

๊ณ ๊ฐ์ด ๊ฐœ์ธ ์ •๋ณด ์‚ญ์ œ๋ฅผ ์š”์ฒญํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ณ„๋ณด๋ฅผ ํ†ตํ•ด ๋ชจ๋“  ๋ณต์‚ฌ๋ณธ ์œ„์น˜๋ฅผ ํŒŒ์•…ํ•ฉ๋‹ˆ๋‹ค.

๊ณ ๊ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋œ ์œ„์น˜:
1. prod_db.customers (์›๋ณธ)
2. analytics_db.dim_customers (๋ณต์‚ฌ๋ณธ)
3. marketing_db.customer_segments (ํŒŒ์ƒ)
4. logs.user_activities (๋กœ๊ทธ)
5. s3://backup/customers/ (๋ฐฑ์—…)

โ†’ ๋ชจ๋“  ์œ„์น˜์—์„œ ํ•ด๋‹น ๊ณ ๊ฐ ๋ฐ์ดํ„ฐ ์‚ญ์ œ ํ•„์š”

4.11 ๋‹ค์Œ ์žฅ ์˜ˆ๊ณ 

๋‹ค์Œ ์žฅ์—์„œ๋Š” Business Glossary๋ฅผ ๋‹ค๋ฃน๋‹ˆ๋‹ค. ์กฐ์ง์˜ ๊ณตํ†ต ์–ธ์–ด๋ฅผ ์ •์˜ํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.