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)๋ก ํํ๋ฉ๋๋ค.
๊ทธ๋ํ ๊ตฌ์ฑ ์์
- ๋ ธ๋ (Node): ๋ฐ์ดํฐ์ (ํ ์ด๋ธ, ๋ทฐ, ํ์ผ)
- ์ฃ์ง (Edge): ๋ฐ์ดํฐ ํ๋ฆ (๋ณํ, ๋ณต์ฌ, ์ด๋)
- ํ๋ก์ธ์ค (Process): ETL ์์ , SQL ์ฟผ๋ฆฌ, ์คํฌ๋ฆฝํธ
{
"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)
ํน์ ๋ฐ์ดํฐ์ ์ ๋ณ๊ฒฝํ ๋, ์ํฅ์ ๋ฐ๋ ๋ชจ๋ ๋ค์ด์คํธ๋ฆผ์ ์๋์ผ๋ก ํ์ ํฉ๋๋ค.
์ํฅ๋ฐ๋ ๋ค์ด์คํธ๋ฆผ:
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 ๊ณ๋ณด ์๊ฐํ
๊ณ๋ณด ๊ทธ๋ํ๋ฅผ ์ฌ์ฉ์๊ฐ ์ดํดํ๊ธฐ ์ฝ๊ฒ ์๊ฐํํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
์๊ฐํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- D3.js: ์ปค์คํฐ๋ง์ด์ฆ ๊ฐ๋ฅํ ์น ๊ธฐ๋ฐ ๊ทธ๋ํ
- Cytoscape.js: ๋๊ท๋ชจ ๊ทธ๋ํ ์๊ฐํ
- Mermaid: ํ ์คํธ ๊ธฐ๋ฐ ๋ค์ด์ด๊ทธ๋จ
- Graphviz: DOT ์ธ์ด ๊ธฐ๋ฐ ๋ ์ด์์
- ๊ณ์ธต์ ๋ ์ด์์ ์ฌ์ฉ (์ผ์ชฝ โ ์ค๋ฅธ์ชฝ ๋๋ ์ โ ์๋)
- ํ๋ซํผ๋ณ๋ก ์์ ์ฝ๋ฉ
- ์ค์ํ ๋ ธ๋ ๊ฐ์กฐ ํ์
- ํ๋/์ถ์, ํํฐ๋ง ๊ธฐ๋ฅ ์ ๊ณต
- ๋ ธ๋ ํด๋ฆญ ์ ์์ธ ์ ๋ณด ํ์
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 ์ค์ ํ์ฉ ์๋๋ฆฌ์ค
๋์๋ณด๋์์ ์ด์ํ ์์น๊ฐ ๋ฐ๊ฒฌ๋์์ต๋๋ค. ๊ณ๋ณด๋ฅผ ์ญ์ถ์ ํ์ฌ ๋ฌธ์ ์ ๊ทผ์์ ์ฐพ์ต๋๋ค.
๋์๋ณด๋ โ BI ๋ ์ด์ด โ DW ํ
์ด๋ธ โ ETL โ ์์ค DB
์ญ์ถ์ ๊ฒฐ๊ณผ: ์์ค DB์์ NULL ๊ฐ์ด ์ฆ๊ฐํจ โ ETL์ด ์๋ชป ์ฒ๋ฆฌ
๊ณ ๊ฐ์ด ๊ฐ์ธ ์ ๋ณด ์ญ์ ๋ฅผ ์์ฒญํ์ต๋๋ค. ๊ณ๋ณด๋ฅผ ํตํด ๋ชจ๋ ๋ณต์ฌ๋ณธ ์์น๋ฅผ ํ์ ํฉ๋๋ค.
๊ณ ๊ฐ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋ ์์น:
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๋ฅผ ๋ค๋ฃน๋๋ค. ์กฐ์ง์ ๊ณตํต ์ธ์ด๋ฅผ ์ ์ํ๊ณ ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ๊ฒ ๋ฉ๋๋ค.