← 기술 블로그

19개 테이블, 원본은 절대 건드리지 않는다 — 데이터 모델 설계 원칙

Phase: 6 — 아키텍처 시리즈: 공공데이터 레시피 대응 스토리: 운영 블로그 6화

이 글의 배경이 되는 이야기는 6화: 제품 100만 개를 담을 그릇에서 읽을 수 있습니다.

이 글을 읽으면 알 수 있는 것


문제: 12개 소스의 데이터를 한 곳에 모으다

이전 글에서 12개 데이터 소스를 하나의 패턴으로 수집하는 구조를 다뤘습니다. 수집한 데이터를 저장할 테이블 구조를 설계해야 했습니다.

처음에는 단순하게 생각했습니다. “제품 테이블 하나, 원재료 테이블 하나면 되지 않을까?”

안 됐습니다. 제품에는 원재료가 있고, 원재료에는 표준 사전이 있고, 표준 사전에는 사용기준이 있고, 사용기준에는 식품유형별 제한이 있습니다. 데이터 간의 관계가 복잡했습니다.


18개 테이블, 9개 그룹

최종적으로 18개 테이블이 9개 그룹으로 나뉘어 있습니다.

그룹테이블 수역할
ETL 운영2배치 실행 이력, 원본 JSON 보관
핵심 도메인4업체, 제품, 업체-제품 관계, 원재료 표시문구
표준 사전3표준 원재료 사전, 원재료 일반정보, 품목유형 트리
매핑1원문 → 표준 사전 매칭 결과
공전/규격1공전 4종 통합 기준규격
첨가물 사용기준2첨가물 마스터 + 식품유형별 제한
기피성분1기피성분 태깅 규칙
공공데이터포털 보강3HACCP 포장지, 원재료 속성, 영양성분
규제 모니터링1식약처 고시 RSS 알림

숫자가 많아 보이지만, 핵심은 세 가지 설계 원칙에서 나옵니다.


원칙 1: 원본은 절대 건드리지 않는다

API에서 받은 원본 데이터를 저장하는 곳과, 그 데이터를 가공한 결과를 저장하는 곳이 분리되어 있습니다.

원본 테이블가공 결과 테이블
원재료 표시문구 (Statement)표준 사전 매칭 결과 (Map)
첨가물 고시 원문사용기준 분류 결과
API 원본 JSON (RawIngest)각 도메인 테이블

원재료 표시문구 테이블에는 제조사가 API에 등록한 원재료 텍스트가 그대로 들어 있습니다. “밀가루(밀:미국산, 호주산), 설탕, 팜유” — 이 문자열을 수정하지 않습니다.

첨가물 고시 원문도 마찬가지입니다. hwpx에서 파싱한 사용기준 원문을 그대로 저장하고, 분류 결과(GENERAL_ALLOWED, RESTRICTED 등)는 별도 필드에 넣습니다.

원본을 보존하는 이유는 이전 글(장치 3: 원본 보존)에서 이야기했습니다. 파싱 로직이나 분류 규칙을 나중에 바꿔야 할 때, 원본이 있으면 API를 다시 호출하지 않아도 재처리할 수 있습니다.


원칙 2: Statement와 Map을 분리한다

이 설계의 핵심입니다.

제품 → 원재료 표시문구(Statement) → 매칭 결과(Map) → 표준 사전

Statement(원재료 표시문구)는 “이 제품에 무엇이 들어있다고 적혀 있는가”입니다. API에서 받은 그대로의 데이터.

Map(매칭 결과)은 “그 원재료명이 표준 사전의 어떤 항목과 연결되는가”입니다. 파싱과 매칭 엔진이 만들어낸 결과.

왜 분리해야 하는가?

상황Statement만 있을 때Statement + Map 분리 시
매칭 로직 변경원본도 영향 받음Map만 다시 생성
매칭 검수원본과 결과가 뒤섞임결과만 검수 가능
매칭 품질 통계산출 어려움Map의 신뢰도 필드로 즉시 산출
원본 역추적불가능Map → Statement → 원본 JSON

분리했기 때문에, 매칭 엔진을 개선해서 다시 돌릴 때 원본 데이터를 건드리지 않고 Map 테이블만 재생성할 수 있습니다.


원칙 3: 모든 연결에 근거를 남긴다

데이터를 연결할 때, “어떤 방법으로, 얼마나 확실하게” 연결했는지를 함께 기록합니다.

매칭 결과 테이블의 핵심 필드:

필드역할예시
매칭 방법어떻게 연결했는가정확일치, 이명일치, 부분포함
신뢰도얼마나 확실한가0.7~1.0
검수 상태사람이 확인했는가대기/승인/거부

이 정보가 있으면:

새우깡 사건 이후, 이 필드들의 중요성을 깨달았습니다. 매칭 결과만 저장하고 과정을 남기지 않으면, 잘못된 매칭을 발견해도 원인을 추적할 수 없습니다.


관계 구조 요약

제품(Product)
├── 원재료 표시문구(Statement) ← API 원본
│   └── 매칭 결과(Map) → 표준 사전(Canonical)
│                            ├── 일반정보
│                            ├── 사용기준(Usage) → 식품유형별 제한
│                            ├── 기피성분 규칙(Risk)
│                            └── 번역(Translation)
├── HACCP 포장지 정보 ← 공공데이터포털
├── 영양성분 ← 공공데이터포털
└── 업체(Company) ← 업체-제품 관계 테이블

중심에 제품표준 사전이 있고, 나머지 테이블은 이 둘에 연결됩니다.


결과

항목수치
전체 테이블18개
그룹9개
원본 보존 테이블3개 (RawIngest, Statement 원문, 고시 원문)
FK 관계15개

한계

테이블 수가 많아 진입 장벽이 있습니다. 새로운 기능을 추가하거나 쿼리를 작성할 때, 어떤 테이블에서 어떤 데이터를 가져와야 하는지 파악하는 데 시간이 걸립니다.

원본 보존의 비용. API 원본 JSON을 90일간 보관하면 저장 공간이 늘어납니다. 현재는 해시 기반 중복 방지로 같은 응답을 두 번 저장하지 않지만, 장기적으로 보관 정책을 조정해야 할 수 있습니다.

한 줄 교훈

원본과 가공 결과를 같은 테이블에 넣으면 편하지만, 나중에 “가공 로직만 다시 돌리고 싶을 때” 원본을 복구할 수 없습니다. 분리의 비용은 미래의 유연성입니다.


다음 글: Claude와 Gemini 크로스 리뷰 워크플로우