시스템 디자인SYSTEM DESIGN
학습 시작하기 →
NEWS & DATA IDENTITY

같은 뉴스는 줄이고, 다른 소식은 놓치지 않으려면?

제목이 비슷하다고 지웠더니 후속 소식까지 사라질 수 있습니다. 문서와 사건을 나눠 정리했습니다.

같은 뉴스 타일을 분류하고 서로 다른 출처와 후속 소식을 별도 보관하는 문서 정리 모형
내용의 흐름을 표현한 개념 이미지

뉴스 카드를 여섯 장 채웠는데 네 장이 같은 발표를 다루고 있습니다. 주소는 다르고 제목도 조금씩 다릅니다. 그러면 제목이 비슷한 것을 지우면 될까요? 그랬다가 정정 기사나 후속 패치 소식까지 사라질 수 있습니다. 뉴스 중복 처리는 많이 지우는 작업보다, 무엇을 같은 것으로 볼지 정하는 작업에 가깝습니다.

앞선 뉴스 신선도 글에서는 지금 화면이 어느 시점의 데이터를 보여주는지 살펴봤습니다. 이번에는 그 목록에 무엇이 한 번씩 올라와야 하는지를 봅니다. 새로 수집됐다는 사실과 새로운 기사라는 판정, 새로운 사건이라는 판정은 각각 다릅니다. 저는 이 셋을 한 번에 처리하지 않겠습니다.

1. 같은 주소, 같은 내용, 같은 사건은 다릅니다

첫 번째는 같은 문서를 다시 만난 경우입니다. RSS에서 읽은 링크와 메일에서 읽은 링크에 추적용 쿼리가 다르게 붙어 있을 수 있습니다. 이런 것은 수집 경로만 다를 뿐 같은 기사일 가능성이 높습니다. 다만 어떤 쿼리를 제거해도 되는지는 해당 출처의 규칙을 확인해야 합니다.

두 번째는 내용이 같은 다른 문서입니다. 공식 발표문이 여러 매체에 실릴 수 있습니다. 본문이 같더라도 출처 URL까지 없애 버리면 어디에서 수집했는지 되짚기 어렵습니다. 화면에서는 대표 기사 하나를 보여주더라도, 원문 기록과 출처 연결은 남겨둘 이유가 있습니다.

세 번째는 같은 사건을 다르게 설명한 문서입니다. 공식 발표, 성능 분석, 운영상의 우려를 짚는 후속 글은 같은 제품을 다루지만 같은 정보는 아닙니다. 기사 단위로 저장하고 사건 단위로 묶는 것은 가능해도, 내용이 닮았다는 이유로 원문을 삭제하는 것은 다른 결정입니다.

비교하는 것판단에 쓰는 단서처리 방향
같은 문서의 재수집채널·검토된 정규 URL기존 문서에 관측 기록 추가
같은 본문의 다른 출처정규화한 본문 해시같은 내용 후보로 연결·출처 보존
같은 사건의 여러 보도대상·버전·시각·내용 비교별도 문서를 사건 묶음으로 연결

이렇게 나누면 중복 제거율 하나로 모든 결과를 설명하지 않게 됩니다. 수집은 아홉 번 했어도 문서는 일곱 개일 수 있고, 화면의 사건 카드는 그보다 적을 수 있습니다. 어느 단계의 숫자인지 빠뜨리면 정상적인 묶음 처리도 데이터 손실처럼 보입니다.

2. 쿼리를 전부 지웠더니 서로 다른 기사가 하나가 됐습니다

주소를 간단하게 만들고 싶어 물음표 뒤를 전부 잘라내면 위험합니다. /report?id=2025/report?id=2026은 서로 다른 보고서를 가리킬 수 있습니다. page, version, lang 같은 값도 문서의 일부일 수 있습니다. 반대로 추적용으로 확인된 utm_source는 제거해도 문서 식별에 영향이 없을 수 있습니다.

첨부 예제에서는 테스트 출처 하나에만 제거 가능한 쿼리 이름을 지정했습니다. 모르는 호스트에서는 추적용처럼 보여도 그대로 둡니다. 경로의 대소문자, 끝 슬래시, 의미 있는 쿼리 순서도 임의로 같게 만들지 않았습니다. 과하게 합치는 것보다, 확인되지 않은 중복 후보를 남기는 쪽을 택한 겁니다.

확인한 출처:
  /release?utm_source=rss → /release
  /report?id=2025        → 그대로 유지
  /report?id=2026        → 별도 문서로 유지

모르는 출처:
  /app#story-1           → fragment 유지
  /News 와 /news         → 임의로 합치지 않음

fragment도 모든 사이트에서 장식은 아닙니다. 일반 문서의 본문 위치 표시일 수도 있지만, 클라이언트 라우팅에서는 다른 화면을 가리킬 수 있습니다. URL 문자열을 다듬는 일과 실제 자원이 같은지 판정하는 일은 구분해야 합니다. RFC 3986의 정규화·비교 절은 그 차이를 이해하는 기준이 됩니다.

페이지에 canonical 링크가 있다고 외부 사이트의 주소를 무조건 믿고 합치는 것도 피하겠습니다. 잘못 지정되었거나 사이트 전체를 같은 주소로 보내는 경우가 있을 수 있습니다. Google의 canonical 안내는 검색에서 대표 URL을 전달하는 방법을 다룹니다. 수집기의 사건 분류 정답이나 뉴스 출처 삭제 명령은 아닙니다.

3. 아홉 번 수집하고, 일곱 문서와 여덟 버전을 남겼습니다

테스트 입력에는 같은 발표 URL의 재수집, 같은 URL의 본문 정정, 별도 매체의 분석, 발표문 재게재, 후속 버전 소식, 연도만 다른 보고서 두 개를 넣었습니다. 마지막으로 같은 URL을 다른 채널에서 수집한 경우도 추가했습니다. 실제 기사를 가져오지는 않았고, 이 경계를 확인하도록 직접 만든 아홉 개 입력입니다.

문서 키는 채널과 정규 URL의 조합으로 정했습니다. 같은 키에 본문 해시까지 같으면 재관측으로 셉니다. URL은 같지만 본문 해시가 달라지면 새 문서를 늘리는 대신 기존 문서에 새 버전을 붙입니다. 다른 출처의 URL은 본문이 같아도 별도 문서로 유지하고, 같은 내용 후보라는 연결만 만듭니다.

2026-09-09 실행 · Node.js v24.13.1 · 직접 작성한 입력 9건
항목결과남긴 경계
수집 관측9건재수집과 정정도 관측에 포함
고유 문서7개같은 채널·URL만 하나로 관리
본문 버전8개동일 URL의 정정 전후 보존
같은 본문·URL 재관측1건문서·버전 수를 늘리지 않음
같은 본문의 별도 출처한 묶음 · 문서 2개 유지대표 후보로 연결하되 삭제하지 않음
후속 버전·연도 쿼리·다른 채널각각 보존겉보기 유사성으로 합치지 않음

본문 비교에서는 Unicode NFC와 공백 정리 정도만 적용했습니다. 띄어쓰기가 달라도 같은 문장인 경우를 다루지만, 숫자와 문장 자체는 지우지 않습니다. 이 정도의 해시는 문장 순서가 바뀌거나 다시 쓴 기사까지 찾아내지 못합니다. 그래서 같은 사건 판정과 구분해 두었습니다.

뉴스 동일성·버전 보존 실험 받기 · Node.js ↗외부 뉴스 수집 없이 작성된 입력으로 실행합니다. 출처별 정규화 경계도 검사합니다.
node news-deduplication.mjs
# observations: 9
# uniqueArticles: 7
# retainedVersions: 8
# followUpPreserved: true
# assertions: PASS

이 결과를 ‘중복 판정 정확도’라고 부르지는 않겠습니다. 아홉 개의 의도된 입력에서 저장과 보존 규칙이 맞게 동작한 결과입니다. 실제 뉴스의 표현 차이, 추출 실패, 정정 시각의 역전까지 검증한 것은 아닙니다. 운영 데이터에 적용하려면 출처별 표본으로 잘못 합친 건과 놓친 중복을 따로 확인해야 합니다.

4. 제목이 비슷해도, 버전 하나가 사건을 바꿉니다

‘캐시 엔진 2.0 공개’와 ‘캐시 엔진 2.0.1 공개’는 글자 대부분이 같습니다. 그렇다고 같은 소식은 아닙니다. 뒤의 글이 앞선 버전의 장애를 고친 패치라면 오히려 놓치면 안 되는 업데이트입니다. 숫자나 부정 표현을 제거해서 제목을 단순하게 만들수록 이런 차이가 사라집니다.

제목 유사도나 임베딩은 비교할 후보를 줄이는 데 쓸 수 있습니다. 다만 점수가 높다는 이유만으로 즉시 하나로 합치기보다, 제품과 버전, 발표 주체, 시간 범위, 정정·철회 여부를 함께 봐야 합니다. 동일 회사의 월간 보고서처럼 반복 제목이 많은 출처는 별도 규칙이 필요할 수 있습니다.

저는 잘못 합친 것의 비용을 먼저 보겠습니다. 비슷한 카드 두 장이 잠깐 남는 것은 눈에 띄지만, 중요한 후속 기사가 기존 묶음 안에 조용히 사라지는 것은 알아채기 어렵습니다. 자동 병합 기준을 느슨하게 만들어 카드 수를 맞추기보다, 후보로 남겨 확인할 수 있는 쪽이 낫습니다.

첨부 예제는 자동 의미 분류를 구현하지 않습니다. 문서와 버전을 안전하게 남기는 단계까지만 실행하고, 사건 묶음은 별도의 판단으로 설명합니다. 짧은 예제에 분류 정확성까지 암묵적으로 떠넘기지 않으려는 선택입니다.

5. 대표 기사만 보여줘도, 나머지 근거는 남겨둡니다

대표 기사는 가장 먼저 수집된 주소로 고정할 필요가 없습니다. 공식 발표가 뒤늦게 들어올 수도 있고, 기존 보도에 정정이 생길 수도 있습니다. 원문 접근성, 발표 주체, 정정 여부, 내용의 충실도 같은 기준을 두고 선택하되, 왜 대표가 바뀌었는지 기록할 수 있어야 합니다.

구조는 문서, 문서 버전, 사건 묶음, 보고서 항목으로 나눠 생각하면 편합니다. 사건 묶음에는 여러 문서를 연결하고 대표 문서 식별자를 둡니다. 일일 보고서에는 그날 선택한 대표와 요약 버전을 남깁니다. 최신 대표가 바뀌었다고 과거 날짜의 보고서까지 아무 표시 없이 달라지게 만들지는 않는 겁니다.

정정 내용이 중요한 경우에는 지난 보고서에 정정 표시와 새 버전으로 가는 링크를 제공할 수 있습니다. 반대로 단순 중복 재수집 때마다 게시 날짜를 오늘로 바꾸면 오래된 기사가 최신처럼 올라옵니다. 원문 게시 시각, 수집 시각, 본문 변경 시각, 보고서 생성 시각을 분리해야 신선도 판단도 유지됩니다.

중복을 줄이겠다고 원문을 통째로 공개 보관할 필요는 없습니다. 출처 식별자, URL, 허용된 메타데이터, 내용 비교용 해시와 정정 관계만으로도 추적에 도움이 됩니다. 본문 수집·보관·재배포 범위는 출처의 조건과 권리를 확인한 뒤 정해야 합니다. 이 사이트의 예제는 직접 작성한 짧은 문장만 사용했습니다.

6. 메모리에서 중복을 걸렀어도 DB에서 한 번 더 막습니다

프로세스 안에서 Set으로 확인하면 이미 본 URL을 빠르게 걸러낼 수 있습니다. 그러나 수집기가 두 개면 각자의 Set에는 아직 없을 수 있습니다. 둘이 동시에 ‘처음 본 주소’라고 판단해 저장하는 문제는 DB의 유일성 제약과 원자적 쓰기로 막아야 합니다.

문서 단위는 채널과 정규 URL, 버전 단위는 문서 식별자와 본문 해시처럼 목적에 맞는 고유 키를 둡니다. PostgreSQL에서는 유일성 제약과 INSERT의 ON CONFLICT를 조합할 수 있습니다. 다만 DO NOTHING만 쓰면 기존 URL의 정정 본문을 놓칠 수 있으므로, 문서 중복 처리와 새 버전 저장을 같은 뜻으로 취급하지 않아야 합니다.

늦게 수집된 이전 본문이 최신 버전을 덮어쓰지 않게 하는 기준도 필요합니다. 예제는 주어진 순서가 관측 순서라고 가정합니다. 실제 파이프라인에서는 출처의 수정 시각을 신뢰할 수 있는지, 추출기 버전이 바뀌어 해시만 달라진 것인지 확인하고 최신 대표를 갱신해야 합니다.

  • 알려진 추적 쿼리 차이는 합쳐지고, 문서 식별 쿼리는 보존되는지 검사합니다.
  • 같은 URL의 정정이 새 버전으로 남는지 확인합니다.
  • 같은 본문의 다른 출처가 삭제되지 않는지 봅니다.
  • 패치 버전, 연도, 철회·정정 표현이 다른 기사를 표본으로 검사합니다.
  • 서로 다른 채널을 의도치 않게 합치지 않는지 확인합니다.
  • 관측 수, 고유 문서 수, 사건 묶음 수, 최종 카드 수를 별도로 집계합니다.

카드를 줄였다는 것보다, 중요한 소식이 남았는지를 봅니다

이번 입력에서는 아홉 번 관측한 결과를 일곱 문서로 정리했습니다. 그렇다고 나머지를 전부 버린 것은 아닙니다. 같은 주소의 재수집은 관측으로, 정정은 새 버전으로 남겼고, 다른 매체의 같은 본문도 출처를 보존했습니다. 후속 패치와 다른 연도 보고서는 별도 문서로 유지했습니다.

중복 처리 뒤 목록이 깔끔해졌다면 다음 질문도 해보면 좋겠습니다. “무엇을 합쳤고, 합치지 말아야 할 것은 남아 있는가?” 원문을 안전하게 남기는 규칙부터 정한 뒤 사건 묶음과 대표 선택을 올리는 편이, 제목 유사도 숫자 하나를 조정하는 것보다 확인하기 쉽습니다.