이 글의 배경이 되는 이야기는 10화: 이 성분이 저 성분이랑 같은 건가요에서 읽을 수 있습니다.
이 글을 읽으면 알 수 있는 것
- 표준 사전의 이명(alias)이 매칭을 엉뚱한 곳으로 끌고 가는 현상
- 2-Pass 구조로 데이터 하이재킹을 원천 차단하는 방법
- 사전 자체의 품질이 매칭 정확도를 좌우하는 이유
문제: 이명이 다른 원재료의 이름을 빼앗는다
식약처 I2520 API는 약 4만 3천 개의 원재료 표준 사전을 제공합니다. 각 항목에는 표준명(name_ko)과 이명(name_alias)이 있습니다. 이명은 “다른 이름”이라는 뜻입니다. 표준명이 “설탕”이면 이명에 “정백당, 백설탕”이 등록되어 있는 식입니다.
매칭 엔진은 이 이명을 활용합니다. 제조사가 “정백당”이라고 적어도 사전에서 “설탕”을 찾을 수 있으니까요.
그런데 이명 데이터를 자세히 살펴보니, 예상하지 못한 문제가 있었습니다.
어떤 원재료의 이명이, 다른 원재료의 표준명과 같은 경우가 있었습니다.
예를 들어 이런 상황입니다.
항목 A: 표준명 = "포도당"
항목 B: 표준명 = "물엿", 이명 = "포도당, 옥수수시럽"
“포도당”은 그 자체로 독립된 원재료입니다. 그런데 “물엿”의 이명 목록에도 “포도당”이 들어 있습니다.
이 상태에서 매칭 사전을 구축하면 어떻게 될까요?
하이재킹이 일어나는 과정
매칭 사전은 “이름 → 사전 항목”의 매핑입니다. 이 매핑을 구축하는 순서에 따라 결과가 달라집니다.
만약 표준명을 먼저 등록하고 이명을 나중에 등록하는데, 이명이 이미 등록된 표준명을 덮어쓴다면:
- “포도당” → 항목 A (표준명으로 등록)
- “물엿”의 이명 “포도당”이 등록되면서 → “포도당” → 항목 B로 덮어쓰기
제조사가 “포도당”이라고 적었을 때, 사전의 “포도당”이 아니라 “물엿”과 연결됩니다.
데이터가 데이터를 하이재킹한 것입니다.
이 문제가 까다로운 이유는, 눈에 잘 띄지 않기 때문입니다. 매칭은 성공합니다. 에러도 나지 않습니다. “포도당”이라는 이름이 사전의 어떤 항목과 연결되었으니까요. 단지 그 항목이 “포도당”이 아니라 “물엿”일 뿐입니다.
갈림길: 이명 충돌을 어떻게 처리할 것인가
| 방법 | 동작 | 리스크 |
|---|---|---|
| 등록 순서로 결정 (먼저 온 것이 승리) | 단순하지만 순서에 의존 | 데이터 적재 순서가 바뀌면 결과가 달라짐 |
| 이명을 전부 등록하되 충돌 시 경고만 | 유연하지만 위험 | 하이재킹이 감지되지만 방지되지는 않음 |
| 충돌하는 이명을 사전에서 아예 제거 | 강력하지만 이명 손실 | 일부 유효한 이명이 삭제될 수 있음 |
세 번째를 선택했습니다.
이유는 명확했습니다. 표준명은 사전의 권위 있는 이름입니다. 어떤 이명도 표준명의 자리를 빼앗으면 안 됩니다. 충돌하는 이명을 제거하면 일부 유효한 연결이 사라질 수 있지만, 잘못된 연결이 생기는 것보다는 낫습니다.
이전 글에서 이야기한 원칙과 같습니다. 틀릴 바에야 모르겠다고 말하는 것이 맞다는 것.
해법: 2-Pass 구조
이명 정제는 두 단계로 이루어집니다.
Pass 1: 전체 표준명 집합을 구축합니다. 4만 3천 개의 원재료 표준명을 모두 모아서 집합(set)으로 만듭니다.
Pass 2: 이명이 있는 모든 항목을 순회하면서, 각 이명 토큰이 표준명 집합에 존재하는지 확인합니다. 존재하면 — 즉, 다른 원재료의 표준명과 같으면 — 해당 이명을 제거합니다.
Pass 1: 표준명 집합 = {"포도당", "물엿", "설탕", ...}
Pass 2: 물엿의 이명 "포도당, 옥수수시럽" 검사
- "포도당" → 표준명 집합에 있음 → 제거
- "옥수수시럽" → 표준명 집합에 없음 → 유지
결과: 물엿의 이명 = "옥수수시럽"
자기 자신의 표준명과 같은 이명도 제거합니다. 표준명이 “설탕”인데 이명에도 “설탕”이 있으면, 이 이명은 아무 역할도 하지 않으므로 삭제합니다.
왜 2-Pass인가
이 작업이 반드시 두 단계여야 하는 이유가 있습니다.
만약 데이터를 한 건씩 적재하면서 동시에 이명 정제를 하면, 아직 적재되지 않은 항목의 표준명과 충돌하는 이명을 놓칠 수 있습니다.
예를 들어, “물엿”을 먼저 적재할 때 “포도당”이라는 표준명이 아직 사전에 없을 수 있습니다. 그 시점에서는 “포도당”이 유효한 이명처럼 보입니다. “포도당” 항목이 나중에 적재되면 그때서야 충돌이 발생하지만, 이미 이명은 등록된 상태입니다.
2-Pass는 이 타이밍 문제를 해결합니다. 모든 데이터를 먼저 넣고(upsert), 그 다음에 전체를 한 번에 정제합니다. 적재 순서에 의존하지 않으므로, 데이터가 어떤 순서로 들어오든 결과가 동일합니다.
정제가 이루어지는 시점
2-Pass 정제는 표준 사전을 적재하는 과정의 후처리(post-import)로 실행됩니다.
- API에서 4만 3천 건의 원재료를 받아서 DB에 저장 (upsert)
- 모든 upsert가 완료된 후, 후처리가 자동 실행
- Pass 1: 전체 표준명 세트 구축
- Pass 2: 이명 순회 → 충돌 토큰 제거 → DB 반영
이 구조 덕분에 표준 사전이 업데이트될 때마다 이명 정제도 자동으로 이루어집니다. 새로운 원재료가 추가되어 새로운 충돌이 생기면, 다음 적재 시 자동으로 정제됩니다.
결과
| 항목 | 수치 |
|---|---|
| 표준명 세트 크기 | 약 4만 3천 개 |
| 정제 대상 항목 수 | 이명이 있는 전체 항목 |
| 제거된 충돌 토큰 수 | 적재 시마다 자동 집계 |
한계
유효한 이명이 삭제될 수 있습니다. 어떤 원재료의 이명이 다른 원재료의 표준명과 글자가 같지만, 실제로는 다른 맥락에서 사용되는 경우가 있을 수 있습니다. 이런 경우 이명이 불필요하게 삭제되어, 해당 이름으로 매칭이 안 될 수 있습니다.
하지만 이것은 의도된 트레이드오프입니다. 하이재킹으로 인한 잘못된 매칭보다, 매칭이 안 되는 것이 덜 위험합니다. 매칭이 안 된 것은 미매칭 목록에서 발견할 수 있지만, 잘못된 매칭은 발견하기가 훨씬 어렵습니다.
한 줄 교훈
매칭 엔진의 정확도는 매칭 로직만으로 결정되지 않습니다. 사전 데이터 자체가 오염되어 있으면, 아무리 좋은 로직이라도 틀린 결과를 냅니다.