56일째 매출 0인데 시스템엔 영업중인 매장 찾아내기
외식 프랜차이즈 ERP를 개발하고 있었습니다. 매장은 시스템 안에서 '개점' 또는 '폐점' 상태를 가집니다. 개점이면 영업 중인 매장이고, 매일 POS와 배달 채널에서 매출이 들어옵니다.
어느 날 정산 데이터를 들여다보다 이상한 점을 발견했습니다. 시스템상 '개점' 상태인데 매출이 한 푼도 들어오지 않는 매장이 있었습니다. 한두 개가 아니었습니다. 마지막 매출 이후 두 달 가까이 0인 매장도 있었습니다.
폐점한 매장이 시스템에는 영업 중으로 남아 있었다
원인은 운영 흐름에 있었습니다. 매장이 실제로 문을 닫아도, 본사 운영팀이 시스템에서 '폐점'으로 바꾸는 일을 잊는 경우가 있었습니다. 휴업에 들어간 매장을 시스템에 반영하지 않은 경우도 있었습니다.
문제는 이 불일치가 조용히 누적된다는 점입니다. 시스템상 status는 '개점'이라고 말하지만, 실제 운영은 멈춰 있습니다. 스키마가 가진 진실과 현장의 진실이 서로 어긋난 채로 굳어집니다. 그동안 이 매장들은 활성 매장 수에 계속 포함되고, 정기 점검에서도 매번 같은 식으로 빠져나갔습니다.
사람이 정기적으로 매장 목록을 훑어 거른다는 방식은 한계가 분명했습니다. 매장이 330개입니다. 사람이 눈으로 보는 점검은 같은 누락을 같은 방식으로 반복합니다. 지난달에 못 본 매장은 이번 달에도 못 봅니다.

들어온 데이터가 아니라 들어오지 않은 데이터를 봤다
발상을 뒤집었습니다. 보통은 들어온 매출 데이터를 분석합니다. 이번에는 들어오지 않은 매출을 신호로 삼기로 했습니다. 매출이 끊긴 매장은 운영에서 무언가 누락됐다는 신호입니다. 데이터가 들어오기를 기다리는 대신, 데이터의 '부재'를 매일 감지하는 쪽으로 방향을 잡았습니다.
감지 기준은 단순하게 잡았습니다. 어제(D-1)를 기준으로 최근 30일 윈도우를 봅니다. 이 안에서 매출이 0보다 큰 날, 즉 영업일수가 30일 미만인 '개점' 매장을 전부 추립니다. 그리고 휴무 일수가 많은 순으로 내림차순 정렬합니다. 매출이 가장 오래 끊긴 매장이 맨 위로 올라옵니다.
여기서 한 가지를 더 정했습니다. 신호를 '발견했다/아니다'라는 boolean으로 두지 않고, 휴무 일수에 따라 등급으로 나눴습니다. 며칠 쉰 매장과 한 달 넘게 죽어 있는 매장은 의미가 다르기 때문입니다.
등급 | 휴무 일수 | 의미 |
|---|---|---|
종료 의심 | 30일 이상 | 폐점 미처리 의심 |
빨강 | 10~29일 | 장기 미운영, 확인 필요 |
주황 | 5~9일 | 단기 이상, 관찰 |
회색 | 1~4일 | 정상 범위 휴무 |
등급을 나누니 운영팀이 위에서부터 순서대로 처리하면 됐습니다. 진빨강은 폐점 처리를 검토하고, 회색은 정상 휴무로 넘깁니다. 같은 '매출 0'이라도 대응이 달라집니다.

신호 채널을 POS로 한정하고 범위를 좁혔다
신호로 쓸 채널을 POS 매출 하나로 한정했습니다. 매장에는 POS와 배달 두 채널이 있습니다. 배달은 매출이 0이라도 매장이 살아 있을 수 있습니다. 배달만 안 하고 매장 영업은 정상인 경우가 있기 때문입니다. POS 매출이 끊긴 것이 매장이 멈췄다는 신호에 더 가깝습니다.
대상도 명확히 잘랐습니다. status가 '개점인 매장만 봅니다. 이미 폐점 처리된 매장이나 POS 매핑이 없는 매장은 자동으로 빠집니다. 우리가 찾는 건 '시스템은 살아 있다고 하는데 실제로는 죽은' 매장이니까, 시스템상 살아 있는 매장만 후보가 되면 됩니다.
각 매장에는 판단에 필요한 정보를 함께 붙였습니다. 마지막 매출일과 그로부터 며칠이 지났는지, 최근 60일 영업일수, 최근 14일 매출입니다. 매장을 클릭하면 상세로 들어갑니다. 목록만 보고도 운영팀이 바로 판단할 수 있게 했습니다.
21개 매장이 즉시 걸렸고 그중엔 190일짜리도 있었다
이 기준으로 한 번 돌리자 21개 매장이 즉시 걸려 나왔습니다. 한 매장은 마지막 매출 이후 56일째 0이었습니다. 다른 매장은 190일째 0이었습니다. 반년 넘게 시스템상 영업 중이던 매장이었습니다.
걸린 매장을 하나씩 확인해 보니 유형이 갈렸습니다. 일부는 실제로 폐점했는데 처리만 안 된 경우였습니다. 다른 하나는 매장에 POS가 두 개 등록돼 있는데 한쪽만 운영하고 다른 한쪽은 쓰지 않는 경우였습니다. 시스템에는 POS가 2개지만 실제 영업은 1개로 통합된 것으로 추정됐습니다. 시스템상 구조와 실제 운영이 어긋난 또 다른 모습입니다.
이 발상 자체는 처음이 아니었습니다. 전에 지식그래프를 운영할 때도, 스키마가 조용히 어긋나는 상태를 사람이 알아채기 전에 모니터링으로 먼저 잡은 적이 있습니다. '조용히 어긋나는 상태'를 신호로 바꿔 매일 감지한다는 점에서 같은 접근입니다.
거대 테이블을 통째로 join하지 않고 윈도우를 먼저 좁혔다
성능은 짚고 넘어갈 부분이 있었습니다. 일별 매출 테이블이 80만 행이고 매장은 300개입니다. 처음엔 매장과 매출을 통째로 join한 뒤 최근 30일 조건을 걸었습니다. 4568ms가 나왔습니다. 매일 도는 화면치고는 느렸습니다.
순서를 바꿨습니다. 최근 60일 매출만 먼저 sub-query로 추려서 작은 집합을 만든 뒤, 그 결과를 매장과 join했습니다. 240ms로 떨어졌습니다. 약 19배입니다. 관심 있는 윈도우를 먼저 좁히고 나서 join하면, 거대 테이블 전체를 들고 join할 때보다 다룰 행이 훨씬 줄어듭니다. 이 정도 속도면 캐싱은 따로 필요 없었습니다.
없는 데이터를 신호로 바꾸려면 챙겨야 할 것들
들어온 데이터만 보지 말고 '들어와야 하는데 안 들어온' 데이터를 신호로 잡는다
신호를 발견/미발견 boolean이 아니라 정도(휴무 일수)에 따른 등급으로 나눈다
신호 채널을 한정한다. POS는 매장 생존 신호에 가깝고 배달은 아니다
대상을 좁힌다. status='개점' & use_yn='Y'만 후보로 두고 나머지는 자동 제외한다
발견 목록에 판단 근거(마지막 매출일·경과일·최근 영업일수)를 함께 붙인다
거대 테이블은 관심 윈도우를 sub-query로 먼저 좁힌 뒤 join한다
이 방식이 통하지 않는 경우도 분명히 있다
한계도 같이 적어 둡니다. POS만 보기 때문에 배달 비중이 큰 매장은 거짓 알람이 날 수 있습니다. '30일이면 종료'라는 기준은 도메인 가정이라, 시즌 영업이나 임시 휴업이 잦은 업종이면 윈도우를 더 길게 잡아야 합니다. 신규 오픈 직후 한 달은 자연스럽게 매출 데이터가 적어 거짓 알람이 날 수 있어, 오픈 후 N일은 제외하는 장치가 필요합니다. 19배라는 속도 개선도 인덱스와 실행 계획에 의존하는 수치입니다.
다음에 붙일 것도 정리해 뒀습니다. 발견 화면에서 바로 '폐점'으로 갱신하는 버튼, '휴업' status 신설, 한 번 검토한 매장은 7일간 알림에서 빼는 마킹, 새로 조건에 진입한 매장 알람, 대시보드 위젯입니다. 핵심은 발견과 액션을 한 화면에 두는 것입니다. 찾아만 주고 처리는 따로 하라고 하면 같은 누락이 또 쌓입니다.
정리하면, status가 말하는 시스템의 진실과 실제 운영 사이의 갭 그 자체가 가치 있는 정보였습니다. 들어오지 않은 매출은 빈칸이 아니라 신호였습니다. 사람이 매달 같은 누락을 반복하던 자리를, 시스템이 매일 발굴하도록 바꾼 것이 이번 작업의 전부입니다.