기술

크롤러 상태 필터가 모르는 값 하나에 6개월치 매출이 조용히 빠졌습니다

2026.09.268분 읽기

POS 본부에서 받은 반기 매출 원장을 우리 숫자와 나란히 놓았습니다. 매장 대부분은 맞았습니다. 그런데 두 브랜드를 함께 운영하는 한 매장에서, 브랜드 하나의 매출이 6개월 내내 비어 있었습니다. 합치면 한 브랜드의 반년치 매출 전부였습니다. 그 사이 크롤러는 에러를 한 번도 내지 않았고, 일별 속보 화면은 매일 정상으로 갱신됐습니다.

더 이상한 것은 같은 매장의 다른 지표는 멀쩡했다는 점이었습니다. 이 프랜차이즈 ERP의 채널 매출과 매출 추이에는 그 브랜드 숫자가 있는데, 일별 속보와 상품별 매출에는 없었습니다. 같은 매장인데 화면마다 숫자가 달랐습니다.


코드도 계산도 아니었습니다

처음엔 집계 쿼리를 의심했습니다. 이런 매장은 단말이 브랜드별로 나뉘어 있어, 조인 조건 하나가 어긋나면 한쪽이 통째로 빠질 수 있습니다. 쿼리는 문제가 없었습니다. 다음 후보는 원본 자체가 안 들어왔을 가능성이었습니다.

그래서 영수증 합계를 먼저 봤습니다. 영수증 수집 경로로 들어온 그 매장의 6개월 합계는 POS 본부 원장과 원 단위까지 일치했습니다. 원본은 100% 있었습니다. 문제는 수집 누락이 아니라, 원본에서 속보를 만드는 경로 어딘가에 있었습니다.

그 경로를 따라가니 크롤러의 상태 필터가 나왔습니다. 일별 속보와 상품별 매출 크롤러는 단말 상태가 '개점'인 것만 수집하도록 기본값이 잡혀 있었습니다. 빠진 브랜드의 단말들은 상태가 '개점'이 아니었습니다.


모르는 상태값은 에러 없이 사라집니다

원인은 세 단계로 이어져 있었습니다. 어느 하나도 그 자체로는 버그가 아니었습니다.

원천 POS의 단말 상태값에 우리 매핑표에 없는 'H'(휴점을 뜻하는 라벨)가 있었습니다. 동기화는 그 값을 해석하지 않고 raw 그대로 저장했습니다.

속보·상품별 매출 크롤러의 상태 필터는 기본값이 '개점'이라, 'H' 단말은 수집 대상 목록에서 빠졌습니다.

그런데 그 'H' 단말들은 실제로 영업 중이었습니다. 두 브랜드를 함께 운영하는 매장에서 한 브랜드 쪽 단말이 그 상태로 등록돼 있었고, 매출은 매일 찍히고 있었습니다.

원천 POS의 모르는 상태값이 매핑표·크롤러 상태 필터를 거치며 수집 대상에서 조용히 빠지는 3단계 흐름

필터는 명단을 든 경비원과 같습니다. 명단에 있는 사람은 들여보내고 없는 사람은 돌려보내는데, 돌려보낸 기록은 남기지 않습니다. 명단에 없는 사람이 손님이 아니라 직원이었다면, 그 사실을 아는 사람은 문 밖에서 돌아간 당사자뿐입니다. 시스템 안에서는 아무 일도 일어나지 않은 것으로 보입니다.

이것이 조용한 누락의 본질입니다. 필터는 아는 값만 통과시킵니다. 모르는 값은 예외를 던지지 않고 그냥 통과하지 못합니다. 원천이 상태 어휘를 하나 늘리는 순간, 그 상태에 속한 대상은 우리 쪽에서 존재 자체가 지워집니다. 로그에도 없고 알람에도 없습니다.

상태값과 실제가 어긋나는 일은 반대 방향으로도 생깁니다. 시스템엔 '개점'인데 매출이 두 달 가까이 0인 매장을, 들어온 데이터가 아니라 들어오지 않은 매출을 신호로 잡아낸 이야기는 아래 글에 있습니다.


같은 사고인데 지표마다 피해가 달랐습니다

수습 범위를 정하려면 어느 지표가 오염됐는지부터 알아야 했습니다. 다행히 지표마다 원천이 달랐습니다. 영수증 수집 경로에는 상태 필터가 없습니다. 단말 상태와 무관하게 영수증이 찍히면 가져옵니다. 그래서 영수증 기반 지표는 무사했습니다.

지표

원천 경로

상태 필터

영향

채널 매출 · 매출 추이

영수증 수집

없음

정상 (원 단위 일치)

일별 속보

속보 크롤러

'개점'만

6개월 누락

상품별 매출

상품별 매출 크롤러

'개점'만

6개월 누락

같은 원천 POS에서 영수증 경로와 속보 크롤러 경로로 갈라져 한쪽은 정상, 한쪽은 6개월 누락이 된 지표별 원천 계보 비교

이 분리가 없었으면 '전부 의심'이 됐을 겁니다. 계보를 알고 있었기 때문에 재크롤 대상이 속보와 상품별 매출 두 경로로 좁혀졌습니다. 영수증 기반 화면은 손대지 않아도 된다는 것을 바로 말할 수 있었습니다. '같은 매장인데 지표마다 숫자가 다르다'는 미스터리도 여기서 풀렸습니다.

경로가 다른 숫자끼리 대조하면 한쪽의 구멍이 드러납니다. 세 경로로 모은 매출 합계가 늘 1.37% 어긋났던 원인을 원천 API를 직접 호출해 실증한 기록도 같은 원리였습니다.


수집은 전체로, 해석은 뒤에서

원래 설계는 '개점 매장만 크롤'이었습니다. 폐점·휴점 단말까지 매일 긁으면 볼륨이 늘고 원천에도 부담이 갑니다. 효율을 생각한 의도였습니다. 문제는 그 설계가 '상태 어휘는 닫혀 있다'는 가정 위에 서 있었다는 점입니다. 원천은 우리에게 묻지 않고 상태를 늘립니다.

원래 설계

바꾼 설계

수집 필터 기본값

'개점'만

전체 (상태 조건 없음)

상태 해석 위치

수집 단계

소비 단계

모르는 상태가 오면

조용히 탈락

일단 수집됨

판단이 틀렸을 때

되돌릴 데이터가 없음

해석 규칙만 고치면 됨

고친 것은 세 가지입니다. 크롤러의 상태 필터 기본값을 None, 즉 전체로 바꿨습니다. 수동 트리거 경로에서 상태를 주입하던 코드를 걷어냈습니다. 그리고 과거 6개월을 전체 상태로 다시 크롤해 속보를 백필했습니다. 빠져 있던 반년치는 이 재크롤로 다시 채워 넣었습니다.

수집 단계에서 상태를 거르던 BEFORE와 전체 수집 후 소비 단계에서 해석하는 AFTER의 비교

'이 상태는 빼자'는 판단은 여전히 필요합니다. 다만 그 판단을 수집 단계에서 하면, 판단이 틀렸을 때 되돌릴 데이터 자체가 없습니다. 소비 단계에서 하면 해석 규칙만 고치고 다시 집계하면 됩니다. 이번 사고에서 영수증 경로가 무사했던 이유가 정확히 이것입니다.


다음에 같은 구멍이 생기면 어떻게 아나

기본값을 전체로 바꾼 것만으로는 반쪽입니다. 매핑표에 없는 값이 raw로 들어오는 것 자체가 새 상태의 신호입니다. 지금까지는 그 신호를 저장만 하고 보지 않았습니다. 이미 본 값은 중복을 제거하고 처음 보는 값만 메신저로 알리는 정도면, 원천이 상태를 늘린 그날 알 수 있습니다.

감수하는 것도 있습니다. 전체 수집은 볼륨과 비용이 늘어납니다. 원천이 폐기한 상태의 단말이나 테스트 단말까지 들어오니 쓰레기도 섞입니다. 그래서 '수집은 전체, 소비는 해석'이 짝으로 가야 합니다. 거르는 일은 소비 단계에서 합니다. 미지 상태 알림도 원천이 상태를 자주 늘리는 시스템에서는 노이즈가 되므로, 처음 보는 값을 한 번만 알리는 수준에서 멈추는 편이 맞습니다.


점검할 것

수집기의 상태·유형 필터에 기본값이 있는가. 있다면 그 값 목록이 원천 쪽에서 늘어날 수 있다는 전제로 잡혀 있는가

원천에서 매핑표에 없는 값이 들어오면 알 수 있는가, 아니면 저장만 되는가

같은 대상의 지표들이 각각 어느 원천 경로에서 오는지 계보가 정리돼 있는가

외부 원장과 주기적으로 대조하는가. 내부 지표끼리는 같은 구멍을 공유할 수 있다


마무리

이 사고에서 코드는 한 줄도 틀리지 않았습니다. 필터는 설계대로 '개점'만 통과시켰고, 동기화는 설계대로 모르는 값을 그대로 저장했습니다. 깨진 것은 설계가 기대고 있던 가정, '원천의 상태 목록은 우리가 아는 것이 전부다'뿐입니다.

외부 원장과의 대조가 이 누락을 잡은 유일한 수단이었습니다. 내부 지표끼리 맞춰보는 것으로는 안 잡힙니다. 같은 필터를 거친 숫자끼리는 같은 구멍을 공유합니다. 수집 필터의 기본값은 전체로 두고, 무엇을 뺄지는 수집이 끝난 뒤에 정하는 편이 되돌릴 여지를 남깁니다.

#크롤링#상태필터#조용한누락#데이터정합성#수집설계#원장대조#ERP#POS매출