1. 배경: 같은 데이터를 반복 수집하는 파이프라인
KT Enterprise B2B 에이전트에서 기업 분석을 자동화하려면, 서로 다른 세 종류의 외부 데이터를 주기적으로 모아야 했습니다. 공시(DART), 조달 공고(나라장터), 뉴스입니다.
세 출처는 성격이 완전히 달랐습니다. 갱신 주기가 다르고, 제공 형식이 다르고, 같은 회사를 부르는 이름조차 달랐습니다. 이를 30분 주기로 수집해 하나의 분석 파이프라인에 태워야 했습니다.
2. 문제 정의
2.1 중복 적재의 누적
첫 번째 문제는 금방 나타났습니다. 30분마다 수집을 돌리니 같은 공고와 같은 기사가 반복해서 적재됐습니다.
원인은 여러 가지였습니다. 외부 API는 대체로 "최근 N건"을 주지 "지난번 이후 새로 생긴 것"을 주지 않습니다. 그러니 매번 겹치는 구간이 생깁니다. 게다가 같은 뉴스가 여러 매체에 재배포되면서 제목만 미세하게 다른 형태로 들어왔습니다.
중복이 쌓이면 저장 공간만 낭비되는 게 아닙니다. 분석 단계에서 같은 사건이 여러 번 있었던 것처럼 보입니다. LLM에게 "이 회사에 최근 무슨 일이 있었나"를 물었을 때 같은 사건을 세 번 언급하는 결과가 나왔습니다.
2.2 엔티티 표기의 불일치
두 번째 문제는 더 까다로웠습니다. 같은 회사가 출처마다 다르게 표기됐습니다.
정식 법인명, 약칭, 흔히 쓰는 별칭, 영문명이 뒤섞여 있었습니다. 사람은 같은 회사라고 바로 알지만 시스템은 서로 다른 네 개의 회사로 인식했습니다. 회사 단위로 데이터를 모아야 하는데 그 기준점이 흔들리는 상황이었습니다.
2.3 파싱 규칙 변경 시 과거 데이터 재처리 불가
세 번째는 시간이 지나서야 드러났습니다.
초기에는 수집한 데이터를 파싱해서 필요한 필드만 저장하고 있었습니다. 저장 공간을 아끼고 조회를 단순하게 하는, 정규화된 스키마의 기본형입니다. 스키마가 안정적인 데이터라면 이 선택이 맞습니다.
전제가 어긋난 지점은 파싱 규칙이 고정이라고 본 것이었습니다. 운영하다 보니 규칙을 고칠 일이 계속 생겼습니다. 새로운 형식이 들어오거나, 처음에 안 쓴다고 판단해 버린 필드가 나중에 필요해지는 식입니다.
그러자 규칙을 고쳐도 이미 적재된 과거 데이터에는 적용할 수 없다는 문제가 드러났습니다. 원문을 안 갖고 있으니 다시 파싱할 방법이 없었습니다. 외부 API에서 과거 데이터를 다시 받아오는 건 가능한 경우도 있고 아닌 경우도 있었습니다.
파싱을 되돌릴 수 없는 연산으로 만든 것이 원인입니다. 저장 시점에 규칙을 적용하면 그 시점의 판단이 데이터에 굳습니다.
3. 기술적 난점
세 문제는 별개로 보이지만 동일성 판정이라는 하나의 축을 공유합니다. 같은 데이터인지, 같은 회사인지, 같은 필드인지를 무엇을 기준으로 판단할 것인가의 문제입니다.
판정 기준을 출처가 부여한 값(고유 ID, 표기된 회사명)에 두면 출처가 바뀔 때마다 기준이 흔들립니다. 30분 주기로 도는 수집기에서는 그 흔들림이 매 주기 누적되므로, 시간이 지날수록 데이터가 정확해지는 것이 아니라 부정확해집니다.
여기에 외부 API라는 제약이 더해집니다. 잘못 적재한 데이터를 고치려고 다시 호출하면 일일 한도를 소모하므로, 재수집 없이 고칠 수 있는 구조를 처음부터 만들어야 했습니다.
4. 대안 검토 및 설계 결정
중복 제거: 동일성 판정 기준
1안. ID 기준 중복 제거. 출처가 주는 고유 ID로 판별합니다. 간단하지만, ID가 없는 출처가 있고 내용이 수정돼도 ID는 그대로라 변경을 감지하지 못합니다.
2안. 제목+날짜 조합. 직관적이지만 제목이 미세하게 다른 재배포 기사를 걸러내지 못하고, 반대로 제목이 같은 다른 공고를 잘못 합칠 위험이 있습니다.
3안. 페이로드 전체를 해싱. 실제 내용을 SHA-256으로 해싱해 지문을 만듭니다.
3안을 골랐습니다. 기준을 "출처가 뭐라고 부르는가"가 아니라 "실제 내용이 같은가" 로 잡은 것입니다. 이렇게 하니 내용이 한 글자라도 바뀌면 다른 해시가 나와 변경을 감지할 수 있고, 완전히 동일한 재수집은 확실히 걸러졌습니다.
이 해시를 유일성 제약으로 걸어두니 수집기를 몇 번을 돌려도 결과가 같아졌습니다. 멱등성(idempotency)이 확보된 것입니다. 운영 관점에서 실질적인 이점이 있었습니다. 수집이 중간에 실패했을 때 "어디까지 됐는지" 따지지 않고 다시 돌리면 됐습니다.
엔티티 정규화: 기준 표기의 통일
회사명 문제는 기준 이름을 하나 정하고 나머지를 거기에 매핑하는 방식으로 풀었습니다. 공시 시스템의 법인명을 기준으로 삼고, 약칭과 별칭을 별도 테이블로 관리해 수집 시점에 정규화했습니다.
여기에 Pydantic 스키마를 붙여 들어오는 데이터의 형태를 입구에서 검증했습니다. 출처마다 형식이 다르니, 파이프라인 안쪽으로 들어오기 전에 하나의 정해진 모양으로 맞춰버린 것입니다. 이렇게 하니 뒤쪽 코드가 "이 필드가 있을 수도 없을 수도 있습니다"를 신경 쓰지 않아도 됐습니다.
원문·가공 데이터의 분리 저장
세 번째 문제는 저장 전략을 바꿔 해결했습니다. 받은 원문(Raw)을 그대로 보관하고, 가공한 결과를 따로 저장했습니다.
외부 API → [원문 저장] ─────────────┐
(손대지 않음) │ 규칙 변경 시
↓ │ 여기서 다시 시작
[파싱·정규화] → [가공 데이터 저장]저장 공간은 더 쓰지만, 얻는 게 훨씬 컸습니다. 파싱 규칙을 바꾸면 원문에서 다시 처리하면 됩니다. 외부 API를 다시 호출할 필요도, 과거 데이터를 포기할 필요도 없습니다.
이 결정은 이후 여러 차례 유효하게 작용했습니다. 처음에 안 쓴다고 판단했던 필드가 필요해졌을 때, 원문이 있었기 때문에 몇 시간 만에 재처리로 해결했습니다.
출처별로 독립적인 생명주기를 줬습니다
세 출처를 하나의 수집 작업으로 묶지 않고 각각 독립적인 데몬과 스케줄로 분리했습니다.
이유는 두 가지입니다. 첫째, 갱신 주기가 다릅니다. 뉴스는 자주 바뀌지만 공시는 그렇지 않습니다. 둘째, 하나가 죽어도 나머지가 계속 돌아야 합니다. 묶어뒀다면 한 출처의 API 장애가 전체 수집을 멈췄을 것입니다.
5. 구현 요약
| 문제 | 적용한 방식 | 근거 |
|---|---|---|
| 중복 적재 | SHA-256 페이로드 해싱 + 유일성 제약 | 동일성 기준을 출처 ID가 아닌 내용으로 잡아야 재배포·수정을 구분 |
| 표기 불일치 | 공시 법인명 기준 + 별칭 테이블 | 집계 단위를 회사로 고정해야 초안 근거가 한 기업으로 모임 |
| 형식 불일치 | Pydantic 스키마 검증 | 파이프라인 입구에서 형태를 통일해 뒤쪽 분기를 제거 |
| 재처리 불가 | 원문·가공 분리 저장 | 파싱 규칙 변경 시 외부 재호출 없이 재생성 |
| 장애 전파 | 출처별 독립 데몬 (30분 주기) | 한 출처의 API 장애가 전체 수집을 멈추지 않게 격리 |
6. 실험 결과
6.1 중복 제거 실효
뉴스 수집 경로에서 한 기업(삼성전자, 2026-08-21) 기준으로 단계별 잔존 건수를 측정했습니다.
| 단계 | 건수 |
|---|---|
| 검색어 조합 8회 호출로 수집 | 210건 |
| 내용 해시 기준 중복 제거 | -47건 |
| 무관 기사 제외 | -52건 |
| 기간 초과 제외 | -0건 |
| 최종 적재 | 44건 (21%) |
수집한 210건 중 47건(22%)이 내용이 동일한 재수집이었습니다. 해시 기준 제거가 없으면 이 47건이 매 주기마다 누적됩니다.
6.2 멱등성
해시를 유일성 제약으로 걸어 수집기를 반복 실행해도 적재 결과가 동일함을 확인했습니다. 수집이 중간에 실패한 경우 진행 지점을 추적하지 않고 전체를 재실행하는 것으로 복구가 완료됩니다.
6.3 재처리
원문을 분리 보관한 덕분에, 초기에 사용하지 않기로 판단했던 필드가 나중에 필요해진 시점에 외부 API 재호출 없이 저장된 원문에서 재생성했습니다.
다만 이 결정에는 대가가 있었습니다. 조달 공고를 6개월치까지 확대하자 원문 테이블이 898MB로 불어나 전체 DB의 85%를 차지했고, 결국 실제 참조하는 값만 남기고 원본을 제거하는 작업을 별도로 수행했습니다. 원문 보관은 기간이 짧을 때 유효하며, 누적 구조에서는 보관 상한을 함께 정해야 합니다.
7. 결론 및 시사점
"같습니다"의 기준을 정하는 게 중복 제거의 전부였습니다. 기술적으로 해싱은 한 줄입니다. 어려운 건 무엇을 해싱할지 정하는 일이었습니다. ID로 볼 것인가, 제목으로 볼 것인가, 내용 전체로 볼 것인가. 이 선택이 파이프라인의 성격을 결정했습니다.
원문을 버리지 않는 것이 가장 값싼 보험이었습니다. 저장 비용은 예측 가능하고 저렴합니다. 반면 "그때 그 데이터를 다시 처리할 수 있었다면"이라는 상황의 비용은 예측할 수 없습니다. 되돌릴 수 있는 상태를 유지하는 것이 데이터 파이프라인에서 가장 중요한 설계 원칙이라고 생각하게 됐습니다.
멱등성은 코드를 단순하게 만듭니다. 여러 번 실행해도 결과가 같다는 보장이 있으면, 실패 처리가 "다시 돌린다"로 끝납니다. 어디까지 처리됐는지 추적하고 이어서 시작하는 복잡한 로직이 필요 없어집니다. 안전장치를 넣었는데 코드가 오히려 간단해지는, 흔치 않은 경우였습니다.
