이 글의 배경이 되는 이야기는 6화: 제품 100만 개를 담을 그릇에서 읽을 수 있습니다.
이 글을 읽으면 알 수 있는 것
- 18개 테이블이 어떤 기준으로 나뉘어 있는지
- “원본 보존”과 “가공 결과 분리”가 왜 중요한지
- Statement(원문)과 Map(매칭 결과)을 분리한 이유
문제: 12개 소스의 데이터를 한 곳에 모으다
이전 글에서 12개 데이터 소스를 하나의 패턴으로 수집하는 구조를 다뤘습니다. 수집한 데이터를 저장할 테이블 구조를 설계해야 했습니다.
처음에는 단순하게 생각했습니다. “제품 테이블 하나, 원재료 테이블 하나면 되지 않을까?”
안 됐습니다. 제품에는 원재료가 있고, 원재료에는 표준 사전이 있고, 표준 사전에는 사용기준이 있고, 사용기준에는 식품유형별 제한이 있습니다. 데이터 간의 관계가 복잡했습니다.
18개 테이블, 9개 그룹
최종적으로 18개 테이블이 9개 그룹으로 나뉘어 있습니다.
| 그룹 | 테이블 수 | 역할 |
|---|---|---|
| ETL 운영 | 2 | 배치 실행 이력, 원본 JSON 보관 |
| 핵심 도메인 | 4 | 업체, 제품, 업체-제품 관계, 원재료 표시문구 |
| 표준 사전 | 3 | 표준 원재료 사전, 원재료 일반정보, 품목유형 트리 |
| 매핑 | 1 | 원문 → 표준 사전 매칭 결과 |
| 공전/규격 | 1 | 공전 4종 통합 기준규격 |
| 첨가물 사용기준 | 2 | 첨가물 마스터 + 식품유형별 제한 |
| 기피성분 | 1 | 기피성분 태깅 규칙 |
| 공공데이터포털 보강 | 3 | HACCP 포장지, 원재료 속성, 영양성분 |
| 규제 모니터링 | 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 |
| 검수 상태 | 사람이 확인했는가 | 대기/승인/거부 |
이 정보가 있으면:
- “신뢰도 0.7 미만인 매칭만 보여줘” → 검수 대상 추출
- “부분포함 매칭 중 오매칭이 있는가” → 품질 분석
- “이 매칭은 누가 승인했는가” → 이력 추적
새우깡 사건 이후, 이 필드들의 중요성을 깨달았습니다. 매칭 결과만 저장하고 과정을 남기지 않으면, 잘못된 매칭을 발견해도 원인을 추적할 수 없습니다.
관계 구조 요약
제품(Product)
├── 원재료 표시문구(Statement) ← API 원본
│ └── 매칭 결과(Map) → 표준 사전(Canonical)
│ ├── 일반정보
│ ├── 사용기준(Usage) → 식품유형별 제한
│ ├── 기피성분 규칙(Risk)
│ └── 번역(Translation)
├── HACCP 포장지 정보 ← 공공데이터포털
├── 영양성분 ← 공공데이터포털
└── 업체(Company) ← 업체-제품 관계 테이블
중심에 제품과 표준 사전이 있고, 나머지 테이블은 이 둘에 연결됩니다.
결과
| 항목 | 수치 |
|---|---|
| 전체 테이블 | 18개 |
| 그룹 | 9개 |
| 원본 보존 테이블 | 3개 (RawIngest, Statement 원문, 고시 원문) |
| FK 관계 | 15개 |
한계
테이블 수가 많아 진입 장벽이 있습니다. 새로운 기능을 추가하거나 쿼리를 작성할 때, 어떤 테이블에서 어떤 데이터를 가져와야 하는지 파악하는 데 시간이 걸립니다.
원본 보존의 비용. API 원본 JSON을 90일간 보관하면 저장 공간이 늘어납니다. 현재는 해시 기반 중복 방지로 같은 응답을 두 번 저장하지 않지만, 장기적으로 보관 정책을 조정해야 할 수 있습니다.
한 줄 교훈
원본과 가공 결과를 같은 테이블에 넣으면 편하지만, 나중에 “가공 로직만 다시 돌리고 싶을 때” 원본을 복구할 수 없습니다. 분리의 비용은 미래의 유연성입니다.