여러 중개 사이트에서 같은 아이템의 시세를 긁어 모으다 보면 늘 같은 벽에 부딪힌다. A 사이트는 price_krw, B 사이트는 가격, C 사이트는 unit_price — 뜻은 같은데 이름도 단위도 다 다르다. 이걸 하나의 테이블에 부어 비교하려면 결국 정규화(normalization), 즉 서로 다른 표기를 공통 규칙으로 맞추는 작업이 필요하다. 오늘은 그 통합 ETL을 어떻게 설계했는지 일기처럼 남긴다.

공개·더미 데이터 기준의 설계 예시이며, 특정 서비스의 실데이터나 투자·거래 권유가 아니다.

전체 그림은 어떻게 생겼나?

먼저 파이프라인 전체를 한 장으로 봐야 머릿속이 정리된다. 원천은 여러 개, 도착지는 하나다.

flowchart LR
  subgraph SRC["원천 (제각각)"]
    A["① 사이트 A<br/>price_krw"]
    B["② 사이트 B<br/>가격(만원)"]
    C["③ 사이트 C<br/>unit_price(USD)"]
  end
  subgraph ETL["④ 정규화 ETL"]
    M["매핑 + 단위환산<br/>+ 출처 태깅"]
  end
  subgraph DST["⑤ 통합 스키마"]
    U["prices_unified<br/>(공통 모델)"]
  end
  A --> M
  B --> M
  C --> M
  M --> U
  classDef s fill:#e7f5ff,stroke:#1c7ed6,color:#10548f;
  classDef e fill:#fff3bf,stroke:#f08c00,color:#8a5300;
  classDef d fill:#d3f9d8,stroke:#2f9e44,color:#1b5e2c;
  class A,B,C s;
  class M e;
  class U d;

핵심은 가운데 ETL 단계에서 세 가지를 동시에 처리한다는 점이다. 컬럼 매핑, 단위 환산, 그리고 출처 태깅. 이 셋을 분리해서 생각하면 코드가 훨씬 깔끔해진다.

컬럼은 어떻게 하나로 맞추나?

가장 먼저 하는 일은 각 사이트의 컬럼을 공통 필드로 대응시키는 매핑 표를 만드는 것이다. 코드가 아니라 설정(config)으로 관리해야 사이트가 늘어도 코드를 안 건드린다.

공통 필드사이트 A사이트 B사이트 C
item_idcode상품코드sku
priceprice_krw가격unit_price
currency(고정 KRW)(고정 KRW)(고정 USD)
unit_scale110000 (만원)1
observed_atcrawled수집시각ts

unit_scale이 핵심이다. B 사이트는 “3.5”가 3만 5천 원을 뜻하므로 배율 10000을 곱해야 한다. 이런 규칙을 데이터가 아니라 매핑 표에 박아두면, 나중에 “왜 이 값이 이렇게 나왔지?”를 표만 보고 추적할 수 있다.

단위와 통화는 어디서 통일하나?

값을 부을 때 세 단계를 순서대로 거친다. 순서를 지키는 게 중요하다 — 환율을 먼저 곱하고 배율을 나중에 곱하면 결과가 틀어진다.

flowchart TD
  R["① 원본 값 (raw)"] --> S["② unit_scale 곱하기<br/>(만원→원 등)"]
  S --> F["③ 환율 적용<br/>(USD→KRW)"]
  F --> N["④ 공통 단위: 원(정수)"]
  classDef a fill:#e7f5ff,stroke:#1c7ed6,color:#10548f;
  classDef b fill:#f3d9fa,stroke:#ae3ec9,color:#6a1a7a;
  class R,S a;
  class F,N b;

여기서 실무 함정이 하나 있다. 환율을 코드 상수로 박으면 과거 데이터가 오늘 환율로 재계산돼 버린다. 그래서 환율은 수집 시점 기준으로 별도 테이블에서 조인해 온다. 시세 데이터는 “그때 그 값”이어야 의미가 있기 때문이다.

출처 컬럼은 왜 반드시 남겨야 하나?

통합했다고 원천 정보를 버리면 안 된다. 나중에 “이 이상치, 어느 사이트에서 온 거야?”를 못 답하면 데이터를 신뢰할 수 없다. 그래서 통합 테이블에는 값뿐 아니라 출처(provenance)를 같이 적재한다.

-- 합성/더미 예시. 통합 시세 테이블
CREATE TABLE prices_unified (
    item_id      TEXT      NOT NULL,   -- 공통 아이템 키
    price_krw    BIGINT    NOT NULL,   -- 원 단위 정수로 통일
    observed_at  TIMESTAMP NOT NULL,   -- 수집 시각
    source_site  TEXT      NOT NULL,   -- 출처: 'A' / 'B' / 'C'
    source_raw   TEXT,                 -- 원본 값 문자열 그대로 보관
    fx_rate      NUMERIC,              -- 적용 환율 (KRW=1)
    ingested_at  TIMESTAMP DEFAULT now()
);

source_sitesource_raw가 없는 통합 테이블은 감사(audit)가 불가능하다. source_raw에 원본 문자열을 그대로 남겨두면, 파싱 버그를 나중에 발견해도 재처리가 가능하다. 실제로 B 사이트가 “3.5만” 같은 문자열을 던진 적이 있는데, 이 컬럼 덕분에 뒤늦게 배율 파싱을 고칠 수 있었다.

이제 통합 뷰에서 사이트별 최저가를 뽑는 건 간단하다.

-- 아이템별 최신 최저가 + 어느 사이트인지
SELECT DISTINCT ON (item_id)
       item_id,
       price_krw,
       source_site,
       observed_at
FROM   prices_unified
WHERE  observed_at >= now() - INTERVAL '1 day'
ORDER  BY item_id, price_krw ASC, observed_at DESC;

정리하면 무엇이 남나?

원칙
매핑은 코드가 아닌 설정으로사이트 추가 시 코드 무수정
단위 환산은 정해진 순서로배율↔환율 순서 뒤바뀜 방지
환율은 수집 시점 조인과거 시세 왜곡 방지
출처 컬럼 필수 보관이상치 추적·재처리 가능
공통 단위는 원(정수)부동소수 오차 회피

결국 통합의 품질은 “값을 얼마나 잘 합쳤나”가 아니라 “합친 값이 어디서 왔는지 되짚을 수 있나”에서 갈린다. 출처 컬럼 하나가 통합 테이블의 신뢰도를 결정한다 — 이게 여러 번 데어보고 얻은 결론이다.