내 파이프라인이 내 옛날 블로그를 차단했습니다 (feat. 자기 조상 잡아먹기)

요즘 개인 프로젝트로 수입 고양이 사료 영양 카탈로그를 만들고 있습니다. 집에 고양이가 있으니 사료 성분표를 자주 들여다보게 되는데, 브랜드마다 표기 방식이 제각각이라 한자리에 모아서 비교하고 싶었거든요. Next.js에 Supabase, 그리고 빈칸 채우는 건 Anthropic API를 붙였습니다. "AI한테 시키면 알아서 채워주겠지" 하는 그 흔한 낙관으로 시작했죠.
그러다 어느 날 제가 짠 코드 한 줄을 보고 잠깐 멈췄습니다.
const BLOCKED_DOMAINS = ["catminzzi.tistory.com"];
catminzzi.tistory.com.
이거, 제가 예전에 열심히 쓰던 고양이 사료 블로그입니다.
대부분 2020년쯤 사료 리뷰를 부지런히 올리던, 그 시절의 저 말이에요.
참고로 지금도 멀쩡히 살아 있습니다. 삭제된 것도 아니고, 그냥 제 손으로 만든 파이프라인이 "저긴 쓰지 마"라고 막고 있을 뿐이죠.
그러니까 상황을 정리하면 이렇습니다. 제가 만든 데이터 강화 파이프라인이, 제가 옛날에 쓴 블로그를 출처로 쓰지 말라고 명시적으로 차단하고 있었던 겁니다. 자업자득도 이런 자업자득이 없죠. (내가 나를 블랙리스트에 올리는 기분, 겪어보셨나요.)
문제의 그 스크립트
한때 저장소에는 scripts/research-enrich.mjs라는 파일이 있었습니다.
사료 데이터 테이블을 훑다가 영양 수치가 비어 있는 행을 만나면, Anthropic API한테 "이 사료 조단백 몇 퍼센트야?" 하고 물어서 웹에서 찾아 채우는 자동 리서치 스크립트였어요.
채울 때 어디서 가져왔는지 출처 태그도 같이 붙이도록 했고요.
말로만 들으면 꽤 그럴싸합니다. 빈칸이 수백 개인데 사람이 일일이 제조사 사이트 뒤지는 것보다 AI가 밤새 채워주면 좋잖아요. 문제는, AI한테 "웹에서 찾아와"라고 시키면 이 녀석이 아무 데서나 찾아올 수 있다는 겁니다. 그리고 그 "아무 데" 목록에는, 2020년의 제 블로그도 들어갑니다.
그래서 프롬프트에 이런 지침을 박아뒀습니다.
Prefer PRIMARY/CURRENT sources: the manufacturer's official site and current
Korean importer/retailer product pages. Do NOT rely on hobbyist blogs or
aggregated community posts — they may be stale (pre-2021) and are not authoritative.
풀어 쓰면 "1차 소스, 그러니까 제조사 공식 사이트랑 지금 파는 국내 수입사·판매처 상품 페이지를 우선해라. 취미 블로그나 커뮤니티 짜깁기 글은 믿지 마라 — 2021년 이전 정보라 낡았을 수 있고 권위도 없다"는 얘깁니다.
그리고 그 "낡은 취미 블로그"의 대표 사례로, 제 손으로 제 블로그 도메인을 차단 목록에 적어 넣은 거죠. (프롬프트에서 "hobbyist blogs"라고 쓰면서 속으로 좀 찔리긴 했습니다. 그게 저잖아요.)
왜 하필 내 블로그를 막았나 — 순환 재확인의 함정
여기서 "아니 그냥 놔두면 안 되나? 본인 블로그면 오히려 믿을 만한 거 아냐?" 싶으실 수 있습니다. 저도 처음엔 그렇게 생각했어요. 근데 아니더라고요. 오히려 제 블로그라서 더 위험했습니다.
핵심은 출처(provenance) 라는 개념입니다. 데이터가 "어디서 왔는가"를 추적하는 건데, 여기엔 한 가지 절대 어기면 안 되는 원칙이 있어요.
자동 강화 파이프라인은 자기 조상 데이터를 다시 인용하면 안 된다.
무슨 말이냐면 이렇습니다. 제 2020년 블로그에 적힌 사료 영양 수치는, 그 시절의 제가 어딘가에서 조사해서 옮겨 적은 값입니다. 즉 그 자체가 2차 가공물이에요. 원본이 아니라 사본이죠.
그런데 지금의 파이프라인이 그 블로그를 다시 긁어오면 어떻게 될까요? 겉보기엔 "2026년에 갓 수집한 최신 데이터"처럼 보입니다. 타임스탬프도 오늘 날짜로 찍히고요. 하지만 실제 내용물은 2020년의 낡은 조사 결과를 그대로 세탁한 것에 불과합니다. 제조사가 그새 배합을 바꿨어도, 수입사가 단종시켰어도, 파이프라인은 그걸 모른 채 옛날 숫자를 "새 데이터"라는 라벨을 붙여 다시 집어넣는 겁니다.
이게 순환 재확인(circular re-verification)입니다. 데이터를 독립적으로 1차 소스에서 다시 길어 오는 게 아니라, 예전에 길어둔 물을 다시 퍼서 "새 물"이라고 부르는 거예요. 그러니 하필 제 블로그였기 때문에 더 조심해야 했습니다. 남의 블로그는 그래도 독립된 출처지만, 제 블로그는 파이프라인 입장에서 자기 자신의 과거 출력이거든요. 자기가 뱉은 걸 자기가 다시 먹는, 데이터판 우로보로스랄까요.
provenance invariant — 3개만 남기고 692개를 비워둔 이유
사실 이 프로젝트에는 처음부터 못 박아둔 규칙이 하나 있습니다. 제가 provenance invariant라고 부르는 건데, 문장 하나로 요약하면 이렇습니다.
영양 수치는 필드별 출처 태그 없이는 대량 수입 금지.
각 영양 수치(조단백, 조지방, 수분...)마다 "이 값은 정확히 어디서 왔다"는 태그가 붙어 있어야 한다는 겁니다.
DB에는 nutrient_sources라는 JSONB 컬럼을 따로 둬서, 필드 하나하나에 출처를 매달도록 했어요.
출처 태그 없는 숫자는 아무리 그럴듯해도 통째로 들어올 수 없습니다.
이 규칙 때문에 웃픈 상황이 벌어졌습니다.
저한테는 2023년에 정리해둔 구글 시트가 하나 있어요. 수입사료정리 CSV, 대략 290행짜리입니다. 꽤 공들여 만든 자료라 이걸 그냥 부어 넣으면 될 줄 알았죠. 그런데 이 시트에는 필드별 출처 태그가 없습니다. 그냥 값만 쭉 적혀 있어요. 그래서 invariant에 걸립니다. 직접 인제스트 소스로는 못 씁니다. 결국 이 시트는 스키마를 정의하고 브랜드 메타데이터를 잡는 용도로만 쓰고, 영양 수치는 한 톨도 안 가져왔습니다.
그 결과가 지금 DB 상태예요. 출처 태그까지 제대로 갖춘, 그러니까 자신 있게 "이건 검증됐다" 말할 수 있는 큐레이션된 행은 딱 3개입니다. 나머지 약 692개는 브랜드와 제품명만 있고 영양칸은 텅 빈 스켈레톤 행이고요.
3 대 692. 누가 보면 프로젝트 진도가 안 나간 걸로 보이겠지만, 저는 이 비율이 오히려 마음에 듭니다. 빈칸으로 남겨둔 692개는 "아직 제대로 된 출처를 못 찾은 것들"이지, "대충 채워 넣고 검증된 척하는 것들"이 아니거든요. 빈칸은 정직합니다. 틀린 값이 채워진 칸보다 백배 낫죠. (요리로 치면, 재료 없이 그릇만 깔아둔 겁니다. 상한 재료로 접시 채우는 것보다 낫잖아요.)
그래서 결국 — 자율 강화 기능을 지웠습니다
여기까지 오면 결말이 좀 예상되실 겁니다.
research-enrich.mjs는 지금 저장소에 없습니다.
commit 6a155fb, 커밋 메시지 chore(collection): remove autonomous web enrichment.
315줄짜리 파일이 통째로 삭제됐어요.
차단 목록도, 프롬프트 지침도, 전부 그 커밋과 함께 사라졌습니다.
그래서 위에서 인용한 코드들은 전부 삭제되기 전 커밋(6a155fb^)의 흔적입니다. 지금 HEAD를 아무리 뒤져도 안 나와요.
자율 웹 강화라는 접근 자체를 접은 겁니다. 차단 목록을 정교하게 다듬어서 살릴 수도 있었을 텐데 왜 그냥 지웠냐면 — 솔직히 말씀드리면, provenance를 제대로 지키려니까 자동화가 주는 이점이 자꾸 줄어들더라고요. "이 출처는 되고 저 출처는 안 되고"를 엄격하게 걸수록, AI가 자유롭게 웹을 뒤져서 빈칸을 채우는 그 편리함이 사라집니다. 남는 건 결국 사람이 1차 소스를 하나씩 확인하는 작업이고요. 그럴 거면 애초에 자율 강화 스크립트를 두는 의미가 옅어진 거죠.
(왜 지웠는지 더 거창한 이유를 붙이고 싶은 마음도 있지만, 커밋 메시지가 말해주는 건 딱 여기까지라 여기서 멈추겠습니다. 지어내면 그것도 출처 없는 데이터니까요.)
마무리 — 출처를 지킨다는 건 편함을 포기하는 일
이 소동을 겪고 나서 남은 생각은 하나입니다.
데이터의 출처를 지킨다는 건, 생각보다 낭만적이지 않습니다. 멋진 검증 파이프라인을 자랑하는 일이 아니라, 편한 자동화를 포기하는 일에 가깝더라고요. AI한테 "알아서 채워"라고 시키는 그 시원함을 내려놓고, 빈칸 692개를 빈칸인 채로 견디는 인내심에 더 가깝습니다.
그리고 그 포기의 상징이 하필 제 옛날 블로그였다는 게, 지나고 보니 좀 절묘합니다. 2020년의 제가 정성껏 쓴 글이, 지금의 제가 만든 파이프라인한테는 "믿지 마라"고 차단당했으니까요. 근데 그게 맞습니다. 그때의 저를 못 믿어서가 아니라, 그때의 데이터가 지금도 유효하다고 자동으로 가정하면 안 되기 때문이에요. 사람은 옛날 자기 글을 애틋하게 보지만, 파이프라인은 그러면 안 됩니다.
혹시 여러분도 어떤 데이터 파이프라인을 만들고 계신다면, 한번 물어보세요. "이 소스가 혹시 내 파이프라인이 예전에 뱉어낸 출력의 후손은 아닌가?" 그게 자기 자신이더라도요. 아니, 자기 자신일수록 더요.
저는 이제 692개의 빈칸을 1차 소스로 하나씩 채우러 갑니다. 느리겠죠. 근데 이번엔 적어도, 채워진 숫자마다 "어디서 왔는지"는 정확히 말할 수 있을 겁니다. 그거면 됐습니다.