토렌트 릴리즈명과 미디어 서버 라이브러리를 대조해서 “이미 보유 중인지"를 표시하는 기능을 만들다 보면, 처음엔 다들 같은 선택을 한다. 정규화한 제목과 연도가 완전히 똑같을 때만 매칭한다는 규칙이다. 구현도 쉽고 오탐도 없다. 문제는 이 규칙이 실제 데이터 앞에서 오래 버티지 못한다는 점이다. 개인 iptorrents 크롤러에 Plex 라이브러리 대조 기능을 붙였을 때도 마찬가지였다. 정확 일치 매칭은 며칠 만에 깨졌고, 그 자리를 메우려고 규칙을 하나씩 완화할 때마다 새로운 종류의 버그가 튀어나왔다. 이 글은 그 과정에서 실제로 발견된 네 가지 실패 사례와, 규칙을 완화하면서도 오탐을 늘리지 않기 위해 썼던 검증 절차를 정리한다.
왜 정확 일치만으로는 부족한가
미디어 서버(여기서는 Plex)에 등록된 공식 제목과, 토렌트 사이트에 올라오는 릴리즈명은 같은 작품을 가리켜도 문자열로는 다른 경우가 흔하다. 배급사가 프랜차이즈 접두어를 붙이거나 떼는 방식이 릴리즈마다 다르고, 구두점 표기가 갈리며, 다국어 메타데이터가 섞인 라이브러리에는 원어 제목과 현지화 제목이 서로 다른 필드에 들어 있기도 하다. (정규화된 제목, 연도) 키가 문자 단위로 완전히 같을 때만 매칭하는 exact match 로직은 이런 표기 차이를 전부 “다른 작품"으로 취급한다. 그래서 실제로는 보유 중인 작품이 “미보유"로 잘못 표시되는 거짓 음성(false negative)이 쌓인다.
이 문제를 고치는 가장 직관적인 방법은 매칭 규칙을 느슨하게 만드는 것이다. 그런데 규칙을 느슨하게 만들수록 이번엔 반대 방향의 실패, 즉 서로 다른 두 작품을 같은 작품으로 착각하는 거짓 양성(false positive)이 생길 위험이 커진다. 아래 네 가지 사례는 이 트레이드오프의 각 지점에서 실제로 무엇이 깨졌는지를 보여준다.
사례 1 — 프랜차이즈 접두어 누락: exact match의 근본적 한계
Plex에 등록된 공식 제목은 Star Wars: The Mandalorian and Grogu(2026)였지만, 실제로 올라온 토렌트 릴리즈 6개 중 5개는 배급사 접두어 없이 The Mandalorian and Grogu로만 표기돼 있었다. 정확 일치 로직은 우연히 “Star Wars"까지 포함해서 올라온 릴리즈 1개만 보유로 인식하고, 나머지 5개는 전부 미보유로 잘못 표시했다.
해법은 정확 일치가 실패했을 때, 같은 연도 안에서 “한쪽 제목이 다른 쪽 제목에 포함되는지"를 보는 부분 일치 fallback을 추가하는 것이었다. 그런데 이 fallback을 붙이자마자, 그 구현 방식 자체가 다음 두 가지 새로운 버그를 드러냈다.
사례 2 — 문자 단위 부분 문자열의 오탐: 단어 경계를 무시하면 생기는 일
fallback을 파이썬의 in 연산자로 그대로 구현하면 어떻게 될까.
| |
이 함수는 라이브러리 전체를 대상으로 검증하는 과정에서 실제로 오탐을 냈다. “Mute”(2018)라는 제목이 “The Commuter”(2018)와 매칭된 것이다. 원인은 단순하다. “mute"라는 문자열이 “commuter"라는 단어 한가운데에 우연히 끼어 있었을 뿐, 두 작품은 아무 관계가 없다. 문자 단위 부분 문자열 검사는 이런 “단어 내부의 우연한 겹침"과 “실제로 한쪽이 다른 쪽의 하위 구(phrase)인 경우"를 구별하지 못한다.
수정은 문자 단위 비교를 단어 단위 비교로 바꾸는 것이다. 양쪽 제목을 공백으로 토큰화한 뒤, 짧은 쪽의 단어 시퀀스가 긴 쪽 안에 “연속된 단어 묶음"으로 그대로 나타나는지만 확인한다.
| |
이렇게 하면 “com/mute/r"처럼 단어 경계를 넘나드는 우연한 겹침은 걸러지고, “Star Wars The Mandalorian and Grogu” 안에 “The Mandalorian and Grogu"가 연속된 단어열로 포함되는 정상 케이스는 그대로 잡힌다. 다만 이 방식도 완벽하지는 않다. 정말로 짧고 흔한 제목(예: “Up”)이 우연히 다른 제목의 부분 문자열로 나타날 위험은 여전히 남는다. 이 프로젝트는 최소 4자 미만인 제목에는 아예 fallback을 적용하지 않는 _MIN_PARTIAL_MATCH_LEN 하한을 둬서 이 잔여 위험을 절충했다.
사례 3 — 정규화가 비-ASCII 문자를 지워버리는 문제
두 번째 버그를 고치기 전, 제목을 비교 가능한 형태로 만드는 정규화 함수에 이미 문제가 있었다. [^a-z0-9] 정규식으로 ASCII 영숫자만 남기고 나머지를 전부 제거하고 있었던 것이다.
| |
이 라이브러리에는 한글 더빙 제목으로 등록된 항목이 21개 있었다. 위 함수를 거치면 한글 제목은 통째로 빈 문자열이나 숫자 몇 개만 남는 식으로 깨진다. 부분 일치 fallback을 붙이기 전까지는 이 깨진 빈 문자열이 그냥 아무 데도 매칭되지 않는 값이라 문제가 드러나지 않았다. 그런데 fallback을 붙이자마자 상황이 급변했다. 빈 문자열은 모든 문자열의 부분 문자열이기 때문에, 같은 연도 안에서 서로 무관한 작품 수백 쌍이 빈 문자열을 매개로 오매칭 후보에 오를 뻔한 것이 검증 스크립트로 확인됐다.
근본 수정은 정규화 정규식을 ASCII 전용 문자 클래스에서 유니코드 인식 \W로 바꾸는 것이다.
| |
파이썬 3의 re 모듈에서 \w/\W는 문자열이 str(유니코드)일 때 기본적으로 유니코드 인식 모드로 동작한다. 즉 re.ASCII 플래그를 명시적으로 주지 않는 한, 한글을 포함한 비-ASCII 문자도 “단어 문자"로 취급되어 정규화 과정에서 보존된다. 반대로 말하면 [^a-z0-9]처럼 직접 문자 범위를 하드코딩한 정규식은 이 기본 동작의 혜택을 받지 못하고, 작성자가 애초에 ASCII만 염두에 두고 짠 그대로 비-ASCII 문자를 지워버린다.
사례 4 — 다국어 필드 자체가 색인되지 않음
앞의 세 가지를 모두 고친 뒤에도 “Dead Poets Society”(1989)가 여전히 미보유로 표시되는 사례가 나왔다. 이번엔 원인이 완전히 달랐다. 이 Plex 라이브러리에는 한글 메타데이터로 등록된 별도 섹션이 있었는데, 이 섹션의 항목은 title 필드 자체가 한글(죽은 시인의 사회)이고 영어 원제는 originalTitle 필드에만 들어 있었다. 색인을 구축하는 함수가 title만 읽고 originalTitle은 아예 보지 않았기 때문에, 영어 릴리즈명으로는 애초에 매칭 후보가 될 수조차 없었다. 단어 단위 fallback으로도 잡을 수 없는 문제였다. 한글과 영어 사이에는 애초에 겹치는 단어가 없기 때문이다.
수정은 originalTitle이 존재하고 title과 다르면 그것도 별도 키로 함께 색인하는 것이었다.
| |
이 변경만으로 색인 크기가 1,082개에서 1,172개로 늘었고, 전체 누적 데이터 453건 중 24건이 추가로 “보유"로 올바르게 정정됐다. 네 가지 사례를 정리하면 아래와 같다.
| 사례 | 실패 원인 | 실패 방향 | 수정 |
|---|---|---|---|
| 1. 프랜차이즈 접두어 | 정확 일치만 비교 | 거짓 음성 | 연도로 후보를 좁힌 부분 일치 fallback 추가 |
| 2. Mute/Commuter | 문자 단위 부분 문자열 | 거짓 양성 | 토큰화 후 연속 단어열 포함 여부로 비교 |
| 3. 한글 제목 소실 | ASCII 전용 정규화 | 거짓 음성 → fallback 결합 시 거짓 양성 위험 | 유니코드 인식 \W 정규화 |
| 4. 원어 제목 미색인 | 단일 필드만 색인 | 거짓 음성 | originalTitle도 별도 키로 색인 |
규칙을 완화할 때마다 전수 충돌 검사를 먼저 돌린다
이 네 사례를 관통하는 패턴이 하나 있다. 매칭 규칙을 느슨하게 만드는 시도(정확 일치 → 부분 일치 → 단어 단위 부분 일치 → 다국어 필드 포함)는 그때마다 재현율(recall, 놓치는 진짜 매칭을 줄이는 것)을 개선했지만, 동시에 정밀도(precision, 잘못된 매칭을 줄이는 것)를 해칠 새로운 경로를 열었다. 이 프로젝트에서 실제로 효과가 있었던 방어책은, 규칙을 바꿀 때마다 “이 규칙을 라이브러리 전체에 적용하면 서로 다른 두 작품이 잘못 하나로 묶이는 경우가 있는가"를 자동 스크립트로 전수 검사하는 것이었다.
| |
단어 경계 수정 이후 자체 라이브러리(1,082개) 내부에서 이 검사를 돌려 충돌 0건을 확인했고, 실제 누적 데이터(453개) 중 새로 매칭된 21건은 전부 사람이 눈으로 확인하는 절차를 거쳤다. 이 검사는 라이브러리 규모가 약 1,000–1,200개 항목이었기 때문에 몇 초 안에 끝나는, 사실상 무료에 가까운 안전장치로 쓸 수 있었다. 다만 이 전제 자체가 트레이드오프다. 라이브러리가 수십만 건 규모라면 $O(n^2)$ 전수 비교는 더 이상 무시할 수 있는 비용이 아니게 되고, 새로 매칭된 항목을 사람이 눈으로 확인하는 절차도 확장성이 떨어진다.
자주 하는 실수
- 부분 문자열 검사를 “매칭 완화"로 착각한다.
in연산자로 짧은 문자열이 긴 문자열에 포함되는지만 보는 방식은 단어 경계를 전혀 고려하지 않는다. 두 제목이 우연히 겹치는 부분 문자열을 공유하기만 해도 매칭돼 버리므로, 실제로는 “완화"가 아니라 “무작위 오탐 통로 추가"에 가깝다. - 정규화 함수를 ASCII 기준으로 짜고 잊어버린다.
[a-z0-9]같은 문자 범위 하드코딩은 작성 시점에 다루는 데이터가 전부 영문이면 문제없이 통과한다. 다국어 데이터가 나중에 들어오는 순간 조용히 깨지는데, 매칭 대상이 없어지는 게 아니라 빈 문자열끼리 우연히 겹치는 형태로 나타나므로 원인 추적이 까다롭다. - 매칭 규칙을 완화하면서 검증 없이 배포한다. 재현율을 올리는 변경은 항상 정밀도를 해칠 잠재적 경로를 함께 연다. 규칙을 바꿀 때마다 전체 데이터셋에 대한 자체 충돌 검사를 습관적으로 돌리지 않으면, 오탐이 실제 사용자에게 노출되고 나서야 발견된다.
- 단일 필드만 색인하고 다국어 메타데이터의 존재를 가정하지 않는다. 미디어 서버·CMS·전자상거래 카탈로그처럼 다국어 메타데이터를 다루는 시스템에서는 “대표 제목” 필드 하나만 색인하면, 원어 제목이 별도 필드에만 있는 항목은 애초에 매칭 후보가 될 수 없다는 사실을 놓치기 쉽다.
요약
퍼지 문자열 매칭을 프로덕션에 들이는 과정은 “정확 일치 하나로는 부족하다"는 사실을 확인하는 데서 끝나지 않는다. 규칙을 완화하는 각 단계(부분 일치, 정규화 범위 확장, 다국어 필드 포함)마다 그 나름의 실패 모드가 따라오고, 그 실패 모드는 대개 원래 규칙이 놓치던 진짜 매칭과는 정반대 방향(거짓 양성)에서 나타난다. 이 프로젝트가 택한 순서 — 연도로 후보를 좁히고, 문자 단위가 아닌 단어 단위로 포함 관계를 보고, 정규화는 언어 중립적으로 하고, 다국어 필드를 함께 색인하고, 규칙을 바꿀 때마다 전수 충돌 검사를 돌리는 것 — 은 어느 하나만 있으면 위 네 사례 중 하나가 재발하는 방식으로 서로를 보완한다.
참고 자료
- Python 공식 문서,
re모듈 —\w/\W가 유니코드 문자열에서 기본적으로 유니코드 인식(비-ASCII 문자를 단어 문자로 취급)으로 동작하는 근거: https://docs.python.org/3/library/re.html
![Featured image of post [Python] 퍼지 문자열 매칭이 깨지는 세 가지 방식: 접두어·단어경계·유니코드](/post/2026-09-17-fuzzy-title-matching-pitfalls/wordcloud_hu_797401d2095764a4.webp)
![[Tool] Hugo 대표 이미지 자동 생성기(hero infographic) 사용법](/post/2025-12-23-hero-infographic-generator-usage/image01_hu_eac523164cf1bd8f.webp)
![[Python] 정규 표현식(Regex) 기초부터 고급·실전 활용까지](/post/2024-08-26-python-regex/wordcloud_hu_9534245dda8cf75c.webp)
![[Rust] Comprehensive Rust 무료 강의 정리 및 코스 구조](/post/2022-12-30-comprehensive-rust/wordcloud_hu_d1420ff38434cdb6.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)
![[Automation] 토렌트 다운로드 완료 후 자동 파일 정리 스크립트](/post/2025-05-22-automatic-file-organization-script-after-torrent-download-completion/ui-classic-bd3481be0133059729c5a937070f8b69_hu_6ef6df81c45eb446.webp)