Skip to content
게으른 엔지니어의 기술 블로그
Go back

고객사 뉴스 리포트 만들다가 9편 — 빨라진 진짜 이유는 따로 있었다

지난 글에서 캐싱과 크롤러를 손본 뒤, 몇 가지를 더 확인하고 정리하면서 이 시리즈를 마무리한다.

기계적으로 매핑만 하는 섹션은 지웠다

리포트에는 원래 네 번째 섹션으로 키워드 몇 개를 미리 정해둔 매핑 테이블에 대입해서 관련 솔루션 이름을 끼워 넣는 부분이 있었다. 지금까지 잘 동작하고는 있었는데, 막상 들여다보니 여긴 LLM 판단이 하나도 들어가지 않는 단순 문자열 대입이었다 — 앞의 세 섹션은 전부 LLM이 기사를 읽고 판단한 결과인데, 이 섹션만 미리 짜둔 규칙표를 그대로 찍어내고 있었다. 동작이 잘못된 게 아니라, 애초에 있을 이유가 약한 기능이었다. 이 섹션과 관련 코드를 통째로 지웠다.

진짜 똑같은 입력을 두 번 넣어봤다

작업하던 중에 질문 하나가 나왔다 — 같은 기사가 다른 키워드로도 검색됐다면 LLM이 뽑아내는 내용이 달라지는지. 프롬프트를 직접 열어서 확인해보니, 매칭된 키워드 자체는 프롬프트에 아예 들어가지 않고 있었다(제목·본문·후보 회사 목록만 들어간다). 그래서 같은 키워드 질문은 애초에 성립하지 않는 질문이었는데, 더 정확한 질문이 하나 남았다 — 완전히 똑같은 입력을 두 번 넣으면 결과가 똑같이 나오는지.

캐시를 우회하고 같은 함수를 실제로 두 번 호출해서 결과를 비교했다. 관련 회사 목록이나 담당조직·인물 이름 같은 고유명사는 두 번 다 한 글자도 다르지 않게 똑같이 나왔다. 반면 요약 문장이나 팩트 서술 같은 산문 부분은 의미는 같은데 표현이 매번 조금씩 달랐다 — “16만9700㎡“와 “16만 9,700㎡“처럼 숫자 표기 방식까지 달라지는 정도였다. 이 변동성 자체는 문제가 안 된다는 것도 같이 확인했다 — 캐시가 있는 이상, 이 요동은 한 기사가 처음 처리되는 그 순간 한 번만 발생하고 그 뒤로는 고정된다.

빨라진 진짜 이유는 따로 있었다

지금까지 고친 걸 반영해서 다시 돌려보니, 예전에 29분 51초 걸렸던 LLM 요약 단계가 이번엔 비슷한 건수(18,743건)에 2분 13초 만에 끝났다 — 열두 배 넘게 빨라진 셈이다. 처음엔 동시 처리 개수를 10개에서 30개로 올린 게 이유라고 생각하기 쉬웠는데, 두 실행의 시간을 처리 건수와 동시 처리 개수로 나눠서 실제 호출당 걸린 시간을 역산해보니 얘기가 달랐다.

29분 51초가 나온 실행은, 검색 범위를 1000건으로 늘린 뒤 이 구조로 처음 돌린 실행이었다 — 대부분이 한 번도 캐시에 없던 진짜 새 기사라서 실제 API 호출이 필요했고, 실측 호출당 시간(약 0.95초)도 이때 나온 값이었다. 이번 실행은 바로 한 시간 전에 같은 범위로 끝난 실행이 채워놓은 캐시를 그대로 물려받은 상태라, 건당 처리 시간이 0.0071초 수준으로 나왔다 — 어떤 실제 API 응답도 이렇게 빠를 수는 없으니, 사실상 거의 전부가 캐시에서 바로 읽어온 것이었다. 동시 처리 개수를 올린 효과는 분명히 있고 앞으로 새 기사가 많을 때는 실제로 체감될 텐데, 이번 열두 배는 그 효과보다는 하루 전에 만든 캐싱 구조가 두 번째로 같은 범위를 돌면서 처음 제 몫을 한 결과였다. 그럴듯한 설명을 그대로 믿는 대신 계산을 한 번 더 해보고 나서야 알게 된 차이였다.

그러다 진짜 크래시가 났다

캐시 재사용이 파이프라인 전체를 얼마나 짧게 만드는지 보려고 한 번 더 돌렸는데, 이번엔 도중에 진짜로 죽었다. 날짜 형식이 잘못됐다는 에러였다. 원인을 추적해보니, 대표 기사로 표시할 때 발행일을 문자열로 저장하는 함수가 날짜(date)와 시각까지 포함한 날짜시각(datetime)을 구분하지 않고 있었다 — 둘은 문자열로 바꿨을 때 모양이 다른데(2026-07-082026-07-08T11:38:00+09:00), 이전 실행에서 저장해둔 대표 기사를 불러오는 코드는 앞의 짧은 형식만 받을 수 있게 짜여 있었다.

이 버그가 지금까지 안 걸린 이유도 같이 확인했다. 이 함수를 만들 때 짠 테스트는 날짜(date)만 넣어서 확인했지, 실제 운영 코드가 넘기는 시각 포함 값으로는 한 번도 확인한 적이 없었다 — 테스트한 경로와 실제로 쓰이는 경로가 달랐던 것이다. 게다가 이 버그가 든 코드는 하루 전 실행에서 이미 한 번 지나갔었는데, 그때는 불러올 이전 실행 결과가 하나도 없어서 문제의 코드가 아예 실행되지 않고 넘어갔다 — 조건이 갖춰지기 전까지는 있는 줄도 몰랐던 버그였다. 시각 포함 값이 들어오면 날짜만 뽑아 쓰도록 고치고, 이미 잘못된 형식으로 저장돼 있던 기존 행 23,749건도 다시 파싱해서 제자리로 돌려놨다. 이번엔 테스트에도 실제로 쓰이는 시각 포함 값을 그대로 넣어서, 같은 틈이 다시 생기지 않게 했다.

9분 37초

버그를 고치고 다시 돌렸다. 검색 5분 18초(중간에 네이버 API 요청 제한에 한 번 걸렸지만 전체엔 영향 없었다), 크롤링 1분 35초, 중복 제거 2분 23초(대상 52,722건 중 51,657건이 캐시에서 바로 스킵되고 1,065건만 실제로 비교됐다), LLM 요약은 19,104건에 20초 — 이번에도 거의 다 캐시 히트였다. 전체 9분 37초. 몇 시간을 넘겨도 안 끝나던 것에서 시작한 이 시리즈가, 반복 실행에서는 10분 안쪽으로 끝나는 데까지 왔다.

문서도 실제와 맞춰놨다

마지막으로 문서 두 개를 손봤다. README는 방금 지운 섹션 얘기가 그대로 남아 있어서 항목 수를 하나 줄이는 한 줄만 고쳤다. 스펙 문서는 훨씬 오래 방치돼 있었다 — 어제 있었던 표기 방식 변경도, 오늘 새로 생긴 캐싱·크롤러 구조도 전혀 반영이 안 돼 있었다. 실제로 방금 생성한 리포트 파일을 옆에 펴놓고 한 줄씩 대조해가며 다시 썼다. 그 과정에서 예전 문서에 있던 “요약 인덱스가 3개”라는 서술이 실제로는 2개뿐이라는 것도 새로 발견해서 바로잡았다. 캐싱과 날짜 버킷, 크롤러 대체 경로에 대한 설명도 이번에 처음 문서에 들어갔다 — 지금까지는 어디에도 적혀 있지 않던 내용이었다.

배운 점

이 시리즈를 관통한 태도 하나를 꼽자면, 그럴듯해 보이는 설명을 그대로 쓰지 않고 한 번 더 계산하거나 실제로 실험해본 것이었다. 동시 처리 개수를 올려서 빨라졌다는 설명은 틀리지 않았지만 이번 열두 배의 진짜 이유는 아니었고, 캐시가 잘 동작할 거라는 가정도 실제로 크래시가 나고서야 어디가 비어 있었는지 드러났다. 5시간 넘게 멈춰 있던 걸 9분 37초로 줄이는 과정 자체보다, 그 과정에서 계속 “진짜 그런가”를 다시 확인한 것이 이 시리즈에서 남는 부분이다.


Share this post:

Previous Part
고객사 뉴스 리포트 만들다가 8편 — 매번 처음부터 다시 비교하는 게 아까웠다