๐ฏ ๊ฑฐ๋ ๋ฐ์ดํฐ ๋ฐฐ์น ํ์ดํ๋ผ์ธ
์ด๋๋ K-pop ๋ฐ๋ชฌ ํํฐ๋ค์ด ํํ
ํฌ ์คํํธ์
์ ๋ง๋ค์๋ค๊ณ ๊ฐ์ ํ์.
์ด์ ๊ทธ๋ค์ ๋งค์ผ ์๋ฐฑ๋ง ๊ฑด์ ์ ์ฉ์นด๋ ๊ฑฐ๋๋ฅผ ์ฒ๋ฆฌํ๊ณ ์ดํดํด์ผ ํ๋ค.
๊ฑฐ๋ ๋ฐ์ดํฐ๋ ์๋์ ๊ฐ์ด ๋ค์ํ๊ฒ ํ์ฉ๋๋ค:
- ์ฌ๊ธฐ ํ์ง โ ์์ฌ์ค๋ฌ์ด ๊ฑฐ๋๋ฅผ ๋น ๋ฅด๊ฒ ํ์ง
- ๊ณ ๊ฐ ์ธ์ฌ์ดํธ โ RFM ์ธ๊ทธ๋จผํธ ๋ง๋ค๊ณ ๊ณ ๊ฐ ์์ ๊ฐ์น(LTV) ๊ณ์ฐ
- ๊ท์ ๋ณด๊ณ โ ๋น๊ตญ์ ์ ํํ๊ณ ๊ฐ์ฌ ๊ฐ๋ฅํ ๋ฐ์ดํฐ ์ ์ถ
- ๋ง์ผํ & ์ ํ ๋ถ์ โ ์ ์ ํ ๊ณ ๊ฐ์๊ฒ ์ ์ ํ ์์ ์ ์คํผ ์ ๊ณต
์ ๋ขฐํ ๋งํ ํ์ดํ๋ผ์ธ์ด ์๋ค๋ฉด ๋ฐ์ดํฐ๋ ๋ฆ๊ฒ ๋์ฐฉํ๊ณ , ์ค๋ณต๋๋ฉฐ, ์๋ง์ง์ฐฝ์ด ๋๋ค.
โ ์ด๋ ๊ณง ๋งค์ถ ์์ค, ๊ท์ ๋ฆฌ์คํฌ, ๊ณ ๊ฐ ๋ถ๋ง์ผ๋ก ์ด์ด์ง๋ค.
๐ ๏ธ ๋ชจ๋์ ์ ๊ทผ๋ฒ
๋๋ ํ๋ก๋์ ์์ค ETL ํ์ดํ๋ผ์ธ์ ๋ง๋ค๊ณ ์ถ์๋๋ฐ ์ํ๋๋๋ก ๋ค ๊ตฌํํ๋ ค๋ ์๋๊ฐ ๋์ง ์๋ ๊ฑฐ๋ํ ํ๋ก์ ํธ๊ฐ ๋์ด๊ฐ๊ณ ์์๋ค.
So I broke it into modular milestones (M0 โ M4):
- M0 โ Core: ๋ก์ปฌ์์ ๋ฐ์ดํฐ ์์ง โ ์ ์ โ ํํฐ์ ๋
- M1 โ Orchestration: Airflow DAG์ผ๋ก ๋งค์ผ ์ค์ผ์ค + ๋ฐฑํ(backfill)
- M2 โ Modeling: dbt๋ก staging / fact / mart ํ ์ด๋ธ ๊ตฌ์ฑ (DuckDB ๋ก์ปฌ)
- M3 โ Cloud: AWS๋ก ์ ํ (S3 + Redshift)
- M4 โ Governance: ๋ฐ์ดํฐ ํ์ง, ๋ฆฌ๋์ง, ๋์๋ณด๋
์ด๋ ๊ฒ ์ชผ๊ฐ๋ฉด ํ๋ก์ ํธ ์ ์ง๋ณด์๋ ์ฌ์์ง๊ณ , ์ง์ฒ ์ํฉ์ ๋จ๊ณ๋ณ๋ก ๋ณด์ฌ์ค ์ ์๋ค.
๐ฆ M0 โ ์ฝ์ด ๋ฐ์ดํฐ ํ์ดํ๋ผ์ธ (๋ก์ปฌ)
Faker๋ก 9์ 1~11์ผ ๋์ ํ๋ฃจ ์ฝ 5,000๊ฑด์ ๊ฐ์ง ๊ฑฐ๋ ๋ฐ์ดํฐ๋ฅผ ์์ฑํ๋ค.
๊ฐ ํ์ ์ค์ ์ ์ฉ์นด๋ ๊ฒฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ฐฉํ๋ค:
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 ๋ฐ์ดํฐ ์์ฑ โ ๋ฐ๋ก ๋ถ์์ ํฌ์ ํ ์ ์๋ค.
โฐ M1 โ Airflow๋ก ์ค์ผ์คํธ๋ ์ด์
๋ฐ์ดํฐ๋ ๊นจ๋ํ์ง๋ง ํ์ฌ๋ก์จ๋ ๋งค์ผ ์ง์ ์์ง์ ๋๋ ค์ผํ๋ค. ์ฌ๊ธฐ์ Airflow (Docker)๊ฐ ๋ฑ์ฅํ๋ค.
๊ฐ๋จํ DAG์ ๋ง๋ค๊ณ dbt ๋จ๊ณ๋ ์ถ๊ฐํ๋ค.
seed_for_day โ spark_clean_for_day โ dbt_run
ํต์ฌ ํฌ์ธํธ:
- ๋งค์ผ ์ค์ผ์ค: ์ค์ 6์ ์คํ, 8์ ์ ์๋ฃ ๋ณด์ฅ
- ๋ฐฑํ: ๊ณผ๊ฑฐ ๋ ์ง ์ฌ์ฒ๋ฆฌ ํ ๋ฒ์ ๊ฐ๋ฅ
- ์ฌ์๋: ์ผ์์ ์ค๋ฅ ์ ์๋ ์ฌ์๋
โ ๊ฒฐ๊ณผ: SLA๋ฅผ ๋ง์กฑํ๋ (์ ๋ ๋์ฐฉํ๋) ํ์ดํ๋ผ์ธ ์์ฑ. ์๋ ์ฌ์๋์ ๋ฐฑํ๋ ์ง์ํ๋ค.
๐ M2 โ dbt๋ก ๋ชจ๋ธ๋ง (DuckDB ๋ก์ปฌ)
์ด์ ๋ชจ์ธ ๋ฐ์ดํฐ๋ฅผ ๋น์ฆ๋์ค ํ์ด ์ธ ์ ์๋๋ก ๋ถ์ ์นํ์ ํ ์ด๋ธ์ด ํ์ํ๋ค. ์ฌ๊ธฐ์ dbt๊ฐ ๋ฑ์ฅํ๋ค.
DuckDB๋ฅผ ๋ก์ปฌ๋ก ์ฐ๊ฒฐํด dbt ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์๋ค.
-
Staging:
stg_transactions
(Parquet์์ ๋ก๋) -
Fact:
fact_transactions
(์ ์ + ์น์ธ ๊ฑฐ๋๋ง) -
Mart:
mart_sales_daily
: ์นดํ ๊ณ ๋ฆฌ๋ณ ์ผ๋ณ ๋งค์ถ, ๊ฑฐ๋ ์mart_rfm_customer
: RFM ์ ์ ๊ณ์ฐ
๋ํ dbt ํ
์คํธ (unique key, not null, ๊ฐ ๊ฒ์ฆ)๋ฅผ ์ถ๊ฐํด ๋ฐ์ดํฐ ํ์ง์ ์๋์ผ๋ก ๋ณด์ฅํ๊ฒ ํ๋ค.
์์ 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 ร ์ฑ๋) โ ๋ฆฌํฌํ
์ ์ฐ์ฑ์ ๊ณ ๋ คํ ์ค๊ณ๋ค.
โ ๊ฒฐ๊ณผ: ์ผ๋ณ ๋งค์ถ ํธ๋ ๋์ RFM ์ฝํธํธ๋ฅผ ํ์ธํ ์ ์๋ ๋น์ฆ๋์ค ์นํ์ ํ ์ด๋ธ ์์ฑ. ๋ชจ๋ Docker์์ ์ฌํ ๊ฐ๋ฅํ๋ฉฐ, ๋ฐ์ดํฐ ํ์ง ํ ์คํธ๊น์ง ์๋ํ๋๋ค.
๐ ์ฌ๊ธฐ๊น์ง์ ์ ๋ฆฌ
M0โM2 ๋จ๊ณ๊ฐ ๋๋๋ฉด์ ์ด์ ๋๋:
- โ ๊นจ๋ํ๊ณ ์ค๋ณต ์๋ ๊ฑฐ๋ ๋ฐ์ดํฐ ํ๋ณด
- โ Airflow๋ก ๋งค์ผ ์๋ ์์ง + ๋ฐฑํ
- โ dbt ํ ์คํธ๊ฐ ์ ์ฉ๋ ๋น์ฆ๋์ค์ฉ mart ์์ฑ
์ด ๋จ๊ณ๋ง์ผ๋ก๋ ๋ฏธ๋ ํ๋ก๋์ ํ์ดํ๋ผ์ธ ๋๋์ด ๋๋ค. ์ฌํ ๊ฐ๋ฅํ๊ณ , ์๋ํ๋์ด ์๊ณ , ๋ถ์ ์นํ์ ์ด๋ค.
ํ์ง๋ง ์์ง ๋์ด ์๋๋ค. ๋ค์ ๊ธ (M3โM4)์์๋ DuckDB ๋์ AWS S3 + Redshift๋ฅผ ์ฌ์ฉํด ํด๋ผ์ฐ๋๋ก ์ฎ๊ธฐ๊ณ , ๊ฑฐ๋ฒ๋์ค, ๋ฆฌ์ง๋, ๋์๋ณด๋๊น์ง ์ถ๊ฐํด ๋ ๋ฆฌ์ผํ ํํ ํฌ ๋ฐ์ดํฐ ํ๋ซํผ์ผ๋ก ๋ฐ์ ์ํฌ ์์ ์ด๋ค.
๐ถ ํด๋ผ์ฐ๋๋ก ๋ฟ