alt text

๐ŸŽฏ ๊ฑฐ๋ž˜ ๋ฐ์ดํ„ฐ ๋ฐฐ์น˜ ํŒŒ์ดํ”„๋ผ์ธ

์–ด๋А๋‚  K-pop ๋ฐ๋ชฌ ํ—Œํ„ฐ๋“ค์ด ํ•€ํ…Œํฌ ์Šคํƒ€ํŠธ์—…์„ ๋งŒ๋“ค์—ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์ž.
์ด์ œ ๊ทธ๋“ค์€ ๋งค์ผ ์ˆ˜๋ฐฑ๋งŒ ๊ฑด์˜ ์‹ ์šฉ์นด๋“œ ๊ฑฐ๋ž˜๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์ดํ•ดํ•ด์•ผ ํ•œ๋‹ค.

๊ฑฐ๋ž˜ ๋ฐ์ดํ„ฐ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ๋‹ค์–‘ํ•˜๊ฒŒ ํ™œ์šฉ๋œ๋‹ค:

  • ์‚ฌ๊ธฐ ํƒ์ง€ โ†’ ์˜์‹ฌ์Šค๋Ÿฌ์šด ๊ฑฐ๋ž˜๋ฅผ ๋น ๋ฅด๊ฒŒ ํƒ์ง€
  • ๊ณ ๊ฐ ์ธ์‚ฌ์ดํŠธ โ†’ RFM ์„ธ๊ทธ๋จผํŠธ ๋งŒ๋“ค๊ณ  ๊ณ ๊ฐ ์ƒ์• ๊ฐ€์น˜(LTV) ๊ณ„์‚ฐ
  • ๊ทœ์ œ ๋ณด๊ณ  โ†’ ๋‹น๊ตญ์— ์ •ํ™•ํ•˜๊ณ  ๊ฐ์‚ฌ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ ์ œ์ถœ
  • ๋งˆ์ผ€ํŒ… & ์ œํ’ˆ ๋ถ„์„ โ†’ ์ ์ ˆํ•œ ๊ณ ๊ฐ์—๊ฒŒ ์ ์ ˆํ•œ ์‹œ์ ์— ์˜คํผ ์ œ๊ณต

์‹ ๋ขฐํ•  ๋งŒํ•œ ํŒŒ์ดํ”„๋ผ์ธ์ด ์—†๋‹ค๋ฉด ๋ฐ์ดํ„ฐ๋Š” ๋Šฆ๊ฒŒ ๋„์ฐฉํ•˜๊ณ , ์ค‘๋ณต๋˜๋ฉฐ, ์—‰๋ง์ง„์ฐฝ์ด ๋œ๋‹ค.
โ†’ ์ด๋Š” ๊ณง ๋งค์ถœ ์†์‹ค, ๊ทœ์ œ ๋ฆฌ์Šคํฌ, ๊ณ ๊ฐ ๋ถˆ๋งŒ์œผ๋กœ ์ด์–ด์ง„๋‹ค.

๐Ÿ› ๏ธ ๋ชจ๋“ˆ์‹ ์ ‘๊ทผ๋ฒ•

๋‚˜๋Š” ํ”„๋กœ๋•์…˜ ์ˆ˜์ค€ ETL ํŒŒ์ดํ”„๋ผ์ธ์„ ๋งŒ๋“ค๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ ์›ํ•˜๋Š”๋Œ€๋กœ ๋‹ค ๊ตฌํ˜„ํ•˜๋ ค๋‹ˆ ์—„๋‘๊ฐ€ ๋‚˜์ง€ ์•Š๋Š” ๊ฑฐ๋Œ€ํ•œ ํ”„๋กœ์ ํŠธ๊ฐ€ ๋˜์–ด๊ฐ€๊ณ  ์žˆ์—ˆ๋‹ค.

So I broke it into modular milestones (M0 โ†’ M4):

  1. M0 โ€” Core: ๋กœ์ปฌ์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ โ†’ ์ •์ œ โ†’ ํŒŒํ‹ฐ์…”๋‹
  2. M1 โ€” Orchestration: Airflow DAG์œผ๋กœ ๋งค์ผ ์Šค์ผ€์ค„ + ๋ฐฑํ•„(backfill)
  3. M2 โ€” Modeling: dbt๋กœ staging / fact / mart ํ…Œ์ด๋ธ” ๊ตฌ์„ฑ (DuckDB ๋กœ์ปฌ)
  4. M3 โ€” Cloud: AWS๋กœ ์ „ํ™˜ (S3 + Redshift)
  5. M4 โ€” Governance: ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ, ๋ฆฌ๋‹ˆ์ง€, ๋Œ€์‹œ๋ณด๋“œ

์ด๋ ‡๊ฒŒ ์ชผ๊ฐœ๋ฉด ํ”„๋กœ์ ํŠธ ์œ ์ง€๋ณด์ˆ˜๋„ ์‰ฌ์›Œ์ง€๊ณ , ์ง„์ฒ™ ์ƒํ™ฉ์„ ๋‹จ๊ณ„๋ณ„๋กœ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋‹ค.

๐Ÿšฆ M0 โ€” ์ฝ”์–ด ๋ฐ์ดํ„ฐ ํŒŒ์ดํ”„๋ผ์ธ (๋กœ์ปฌ)

Faker๋กœ 9์›” 1~11์ผ ๋™์•ˆ ํ•˜๋ฃจ ์•ฝ 5,000๊ฑด์˜ ๊ฐ€์งœ ๊ฑฐ๋ž˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ–ˆ๋‹ค.
๊ฐ ํ–‰์€ ์‹ค์ œ ์‹ ์šฉ์นด๋“œ ๊ฒฐ์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋ฐฉํ–ˆ๋‹ค:
Sample raw transaction data

transaction_id | customer_id | card_id | merchant_id | txn_ts | amount | currency | mcc | status | is_refund | channel 
  • mcc: ๊ฐ€๋งน์  ์—…์ข… ์ฝ”๋“œ (์นด๋“œ์‚ฌ์—์„œ ๋ถ€์—ฌํ•˜๋Š” 4์ž๋ฆฌ ์ˆซ์ž)
  • channel: online, POS, mobile

์‹ค์ œ์ƒํ™ฉ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๊ธฐ ์œ„ํ•ด ์ค‘๋ณต ๋ฐ์ดํ„ฐ์™€ ์ง€์—ฐ ๋„์ฐฉ ๋ฐ์ดํ„ฐ๋„ ์ผ๋ถ€ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  PySpark ์žก(Docker์—์„œ ์‹คํ–‰)์„ ์ž‘์„ฑํ•˜์—ฌ:

  • transaction_id ๊ธฐ์ค€์œผ๋กœ ์ค‘๋ณต ์ œ๊ฑฐ (๊ฐ€์žฅ ์ตœ๊ทผ ๊ธฐ๋ก๋งŒ ์œ ์ง€)
  • ๋Šฆ๊ฒŒ ๋„์ฐฉํ•œ ๋ฐ์ดํ„ฐ(upsert) ๋ฐ˜์˜ + ์ด๋ฒคํŠธ ๋‚ ์งœ๋ณ„ ํŒŒํ‹ฐ์…”๋‹ ๋‹ค์‹œ ์ˆ˜ํ–‰
  • ํŒŒํ‹ฐ์…”๋‹๋œ Parquet ํŒŒ์ผ๋กœ ์ €์žฅ: silver/transactions/year=YYYY/month=MM/day=DD

โœ… ๊ฒฐ๊ณผ: ์ค‘๋ณต๋ฅ  <0.001% ์ˆ˜์ค€์œผ๋กœ ๊นจ๋—ํ•˜๊ณ  ํŒŒํ‹ฐ์…”๋‹๋œ Parquet ๋ฐ์ดํ„ฐ ์™„์„ฑ โ€” ๋ฐ”๋กœ ๋ถ„์„์— ํˆฌ์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค.

Partitioned data

โฐ M1 โ€” Airflow๋กœ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜

๋ฐ์ดํ„ฐ๋Š” ๊นจ๋—ํ•˜์ง€๋งŒ ํ˜„์žฌ๋กœ์จ๋Š” ๋งค์ผ ์ง์ ‘ ์ˆ˜์ง‘์„ ๋Œ๋ ค์•ผํ•œ๋‹ค. ์—ฌ๊ธฐ์„œ Airflow (Docker)๊ฐ€ ๋“ฑ์žฅํ•œ๋‹ค.

๊ฐ„๋‹จํ•œ DAG์„ ๋งŒ๋“ค๊ณ  dbt ๋‹จ๊ณ„๋„ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

seed_for_day โ†’ spark_clean_for_day โ†’ dbt_run

Airflow Dag

ํ•ต์‹ฌ ํฌ์ธํŠธ:

  • ๋งค์ผ ์Šค์ผ€์ค„: ์˜ค์ „ 6์‹œ ์‹คํ–‰, 8์‹œ ์ „ ์™„๋ฃŒ ๋ณด์žฅ
  • ๋ฐฑํ•„: ๊ณผ๊ฑฐ ๋‚ ์งœ ์žฌ์ฒ˜๋ฆฌ ํ•œ ๋ฒˆ์— ๊ฐ€๋Šฅ
  • ์žฌ์‹œ๋„: ์ผ์‹œ์  ์˜ค๋ฅ˜ ์‹œ ์ž๋™ ์žฌ์‹œ๋„

โœ… ๊ฒฐ๊ณผ: SLA๋ฅผ ๋งŒ์กฑํ•˜๋Š” (์ œ๋•Œ ๋„์ฐฉํ•˜๋Š”) ํŒŒ์ดํ”„๋ผ์ธ ์™„์„ฑ. ์ž๋™ ์žฌ์‹œ๋„์™€ ๋ฐฑํ•„๋„ ์ง€์›ํ•œ๋‹ค.

๐Ÿ“Š M2 โ€” dbt๋กœ ๋ชจ๋ธ๋ง (DuckDB ๋กœ์ปฌ)

์ด์ œ ๋ชจ์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋น„์ฆˆ๋‹ˆ์Šค ํŒ€์ด ์“ธ ์ˆ˜ ์žˆ๋„๋ก ๋ถ„์„ ์นœํ™”์  ํ…Œ์ด๋ธ”์ด ํ•„์š”ํ•˜๋‹ค. ์—ฌ๊ธฐ์„œ dbt๊ฐ€ ๋“ฑ์žฅํ•œ๋‹ค.

DuckDB๋ฅผ ๋กœ์ปฌ๋กœ ์—ฐ๊ฒฐํ•ด dbt ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. dbt lineage graph

  • Staging: stg_transactions (Parquet์—์„œ ๋กœ๋“œ)

  • Fact: fact_transactions (์ •์ œ + ์Šน์ธ ๊ฑฐ๋ž˜๋งŒ)

  • Mart:

    • mart_sales_daily: ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ผ๋ณ„ ๋งค์ถœ, ๊ฑฐ๋ž˜ ์ˆ˜
    • mart_rfm_customer: RFM ์ ์ˆ˜ ๊ณ„์‚ฐ

๋˜ํ•œ dbt ํ…Œ์ŠคํŠธ (unique key, not null, ๊ฐ’ ๊ฒ€์ฆ)๋ฅผ ์ถ”๊ฐ€ํ•ด ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ์„ ์ž๋™์œผ๋กœ ๋ณด์žฅํ•˜๊ฒŒ ํ–ˆ๋‹ค. dbt test results

์˜ˆ์‹œ mart ์ฟผ๋ฆฌ (mart_sales_daily):

WITH base AS (
  SELECT
    CAST(strftime(txn_ts, '%Y-%m-%d') AS DATE) AS day,
    mcc,
    channel,
    SUM(CASE WHEN is_refund THEN -amount ELSE amount END) AS gross_sales,
    COUNT(*) AS txn_count
  FROM {{ ref('fact_transactions') }}
  GROUP BY 1,2,3
)
SELECT * FROM base

mart_sales_daily ์ƒ˜ํ”Œ ์ถœ๋ ฅ (grain: ์ผ ร— MCC ร— ์ฑ„๋„) โ†’ ๋ฆฌํฌํŒ… ์œ ์—ฐ์„ฑ์„ ๊ณ ๋ คํ•œ ์„ค๊ณ„๋‹ค.

Sample query result

โœ… ๊ฒฐ๊ณผ: ์ผ๋ณ„ ๋งค์ถœ ํŠธ๋ Œ๋“œ์™€ RFM ์ฝ”ํ˜ธํŠธ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ์นœํ™”์  ํ…Œ์ด๋ธ” ์™„์„ฑ. ๋ชจ๋‘ Docker์—์„œ ์žฌํ˜„ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ ํ…Œ์ŠคํŠธ๊นŒ์ง€ ์ž๋™ํ™”๋œ๋‹ค.


๐Ÿ ์—ฌ๊ธฐ๊นŒ์ง€์˜ ์ •๋ฆฌ

M0โ€“M2 ๋‹จ๊ณ„๊ฐ€ ๋๋‚˜๋ฉด์„œ ์ด์ œ ๋‚˜๋Š”:

  • โœ… ๊นจ๋—ํ•˜๊ณ  ์ค‘๋ณต ์—†๋Š” ๊ฑฐ๋ž˜ ๋ฐ์ดํ„ฐ ํ™•๋ณด
  • โœ… Airflow๋กœ ๋งค์ผ ์ž๋™ ์ˆ˜์ง‘ + ๋ฐฑํ•„
  • โœ… dbt ํ…Œ์ŠคํŠธ๊ฐ€ ์ ์šฉ๋œ ๋น„์ฆˆ๋‹ˆ์Šค์šฉ mart ์ƒ์„ฑ

์ด ๋‹จ๊ณ„๋งŒ์œผ๋กœ๋„ ๋ฏธ๋‹ˆ ํ”„๋กœ๋•์…˜ ํŒŒ์ดํ”„๋ผ์ธ ๋А๋‚Œ์ด ๋‚œ๋‹ค. ์žฌํ˜„ ๊ฐ€๋Šฅํ•˜๊ณ , ์ž๋™ํ™”๋˜์–ด ์žˆ๊ณ , ๋ถ„์„ ์นœํ™”์ ์ด๋‹ค.

ํ•˜์ง€๋งŒ ์•„์ง ๋์ด ์•„๋‹ˆ๋‹ค. ๋‹ค์Œ ๊ธ€ (M3โ€“M4)์—์„œ๋Š” DuckDB ๋Œ€์‹  AWS S3 + Redshift๋ฅผ ์‚ฌ์šฉํ•ด ํด๋ผ์šฐ๋“œ๋กœ ์˜ฎ๊ธฐ๊ณ , ๊ฑฐ๋ฒ„๋„Œ์Šค, ๋ฆฌ์ง€๋‹ˆ, ๋Œ€์‹œ๋ณด๋“œ๊นŒ์ง€ ์ถ”๊ฐ€ํ•ด ๋” ๋ฆฌ์–ผํ•œ ํ•€ํ…Œํฌ ๋ฐ์ดํ„ฐ ํ”Œ๋žซํผ์œผ๋กœ ๋ฐœ์ „์‹œํ‚ฌ ์˜ˆ์ •์ด๋‹ค.

๐ŸŽถ ํด๋ผ์šฐ๋“œ๋กœ ๋ฟ…

alt text