Big Project · AI's EYE 06

OrderEvent 상태 이력 집계 기준 변경 과정: 정확한 주문 건수 KPI 찾기

주문 이벤트 테이블의 행 수를 세면 주문량이 되는 것처럼 보인다. 같은 주문의 received·preparing·ready·completed를 왜 따로 다뤄야 하는지 헷갈린다.

근거 · 프로젝트 문서 · 코드 · 테스트

1장: 왜 OrderEvent 행 수는 주문 건수가 아닌가요?

솔라가 노트북을 들고 거실로 나왔다. 화면 한쪽에는 데이터베이스 테이블로 보이는 창이, 다른 한쪽에는 간단한 막대그래프가 떠 있었다. 평소의 활기찬 걸음걸이와 달리, 미간에 옅은 주름이 잡혀 있었다.

“언니, 오늘 주문 건수 집계하는데 숫자가 이상해.”

소파에 앉아 책을 읽던 루나는 솔라 쪽으로 고개를 돌렸다. 질문에 바로 답하는 대신, 솔라가 자신의 생각을 마저 정리할 때까지 조용히 기다려 주었다.

OrderEvent 테이블을 보면 오늘 날짜로 기록된 이벤트가 총 480개거든. 그럼 오늘 들어온 주문이 480건이어야 하잖아? 그런데 대시보드에 표시되는 최종 주문 건수는 120건이야. 네 배나 차이가 나. 집계 로직이 완전히 잘못된 것 같은데.”

솔라의 목소리에는 확신이 섞여 있었다. OrderEvent 테이블의 행(row) 하나가 이벤트 하나를 의미하고, 따라서 행의 개수가 곧 주문의 개수여야 한다는 모델이 너무나 명백해 보였기 때문이다.

루나는 잠시 생각에 잠겼다가, 책상 서랍에서 빈 종이 한 장과 펜을 꺼내 솔라 옆에 앉았다.

“주문 하나가 우리 시스템에 들어와서 손님에게 전달될 때까지, 어떤 과정을 거치는지 여기에 한번 그려볼까?”

솔라는 고개를 갸웃했지만, 루나가 이끄는 대로 빈 종이를 바라봤다. 루나는 종이 위에 작은 표를 그리기 시작했다. order_id, status, timestamp 세 개의 칸을 만들었다.

“자, 지금 손님이 키오스크에서 ‘치즈버거 세트’를 주문했다고 상상해 봐. 주문 번호는 101번이야. 제일 먼저 어떤 일이 일어날까?”

“음… ‘주문 접수’가 되겠지? 시스템에 주문이 기록되는 거.”

루나는 고개를 끄덕이며 표의 첫 줄을 채웠다.

order_idstatustimestamp
101received2023-10-26 10:00:00

“좋아. 주문이 접수됐어. OrderEvent 테이블에 received 상태로 한 줄이 쌓였네. 그 다음은?”

“주방에서 주문을 확인하고 ‘조리 시작’을 누를 거야.”

루나는 두 번째 줄을 추가했다. 이번에도 주문 번호는 101번 그대로였다.

order_idstatustimestamp
101received2023-10-26 10:00:00
101preparing2023-10-26 10:01:00

“이제 버거가 다 만들어졌어.”

“그럼 ‘준비 완료’ 상태가 되겠지. 픽업대로 음식이 나갈 테니까.”

루나는 말없이 세 번째 줄을 채웠다. 마지막으로 솔라가 먼저 말했다.

“손님이 음식을 가져가면 ‘전달 완료’. 끝.”

루나의 펜이 움직여 표를 완성했다.

order_idstatustimestamp
101received2023-10-26 10:00:00
101preparing2023-10-26 10:01:00
101ready2023-10-26 10:05:00
101completed2023-10-26 10:06:00

루나는 펜을 내려놓고 솔라를 바라보았다.

“자, 이제 이 표에 데이터가 몇 줄 쌓였지?”

“네 줄.”

솔라는 망설임 없이 대답했다. 루나가 한 박자 늦게 다음 질문을 던졌다.

“그럼, 우리가 방금 처리한 ‘주문’은 총 몇 건이었을까?”

순간 솔라의 눈이 크게 뜨였다. 자신의 질문이 향하던 곳과 루나의 질문이 도달한 곳이 달랐지만, 답은 같은 곳을 가리키고 있었다. 솔라는 잠시 완성된 표와 자신의 노트북 화면을 번갈아 쳐다봤다. 표에 적힌 order_id는 전부 ‘101’이었다.

“…한 건. 주문은 딱 한 건이었네.”

“그런데 행은 네 개지.”

“아.”

탄식과 함께 깨달음이 찾아왔다.

“주문은 하나인데, 상태가 바뀔 때마다 이벤트가 새로 기록되니까… 그냥 행 수를 세면, 같은 주문을 여러 번 세게 되는 거구나. received 한 번, preparing 한 번, ready 한 번, completed 한 번. 그래서 네 배로 뻥튀기된 거였어.”

솔라는 자신이 세웠던 ‘하나의 행 = 하나의 주문’이라는 단순한 모델이 무너지는 것을 느꼈다. OrderEvent 테이블은 주문 목록이 아니었다. 하나의 주문이 시간의 흐름에 따라 어떻게 변해갔는지를 기록하는 ‘상태 변화의 발자취’였다. 각 행은 독립된 주문이 아니라, 같은 주문의 다른 순간들이었다. 눈앞의 데이터가 쌓이는 누적 패턴을 보고 나니, 왜 행 수를 세는 것이 틀렸는지 명확하게 보였다.

스스로 답을 찾았다는 안도감도 잠시, 솔라의 미간이 다시 좁혀졌다. 방금 전과는 다른 종류의, 더 근본적인 질문이 떠올랐기 때문이다.

“알겠어. 이제 행 수를 그대로 세면 안 된다는 건 이해했어. 주문 하나의 여정에 이벤트가 계속 쌓이는 거니까. 그런데… 그럼 도대체 ‘한 건의 주문’은 어떻게 세어야 하는 거야?”

2장: ‘주문’이라는 비즈니스 개체를 어떻게 정의할까요?

솔라의 질문이 거실의 고요함 속에 맴돌았다. 루나는 대답 대신, 두 사람 사이에 놓인 작은 표를 가만히 들여다봤다. 주문 번호 101번의 여정이 담긴 네 줄짜리 기록. 솔라는 그 표에서 눈을 떼지 못했다. ‘하나의 주문’을 어떻게 셀 것인가. 답은 저 표 어딘가에 숨어 있을 것만 같았다.

한참의 침묵 끝에, 루나가 다시 펜을 들었다. 이전처럼 질문을 던지는 대신, 말없이 표의 빈칸을 채워나가기 시작했다. 익숙한 주문 번호 101 옆에 새로운 번호들이 등장했다. 표는 순식간에 더 복잡하고 현실적인 모습으로 변해갔다.

order_idstatustimestamp
101received2023-10-26 10:00:00
101preparing2023-10-26 10:01:00
102received2023-10-26 10:02:00
101ready2023-10-26 10:05:00
101completed2023-10-26 10:06:00
102preparing2023-10-26 10:07:00
103rejected2023-10-26 10:08:00

루나가 추가한 마지막 줄, order_id: 103status: rejected가 솔라의 시선을 붙잡았다.

“알겠다!”

솔라는 무릎을 탁 쳤다.

“단순히 행 수를 세는 게 아니라, order_id가 몇 종류인지 세면 되는 거 아닐까? 고유한 주문 번호의 개수를 세는 거지. 이 표에서는 101, 102, 103이니까… 총 세 건!”

그럴듯한 해답이었다. 이벤트가 아무리 많이 쌓여도, 같은 주문은 같은 order_id를 가질 테니, 중복을 제거하고 order_id의 개수만 세면 실제 주문 건수가 나올 것이라는 논리였다. 하지만 솔라의 확신에 찬 목소리에도 루나는 한 박자 늦게, 조용한 질문을 던졌다.

“주문 103번도 우리가 오늘 처리한 ‘주문’에 포함해서 계산하는 게 맞을까?”

“어…?”

솔라의 눈동자가 다시 표로 향했다. 103번 주문의 상태는 ‘rejected’(거절됨)였다. 재고 부족이나 시스템 오류로 인해 아예 주문 자체가 생성되지 못한 경우일 수 있다. 이걸 ‘오늘의 주문’이라고 부를 수 있을까? 솔라의 머릿속이 복잡해졌다. 모든 이벤트 상태가 새로운 주문의 발생을 의미하는 건 아니었다.

루나는 표 옆의 빈 공간에 OrderEvent가 가질 수 있는 모든 상태를 나열했다.

received, preparing, ready, completed, cancelled, rejected

“이 상태들 중에서, 딱 하나의 상태만이 ‘새로운 주문이 세상에 태어났다’는 걸 의미해. 비즈니스 관점에서 ‘한 건의 주문’이 시작되는 바로 그 순간. 어떤 걸까?”

솔라는 나열된 단어들을 하나씩 짚어보았다.

‘completed’는 주문의 끝이다. 시작이 될 수 없다. ‘preparing’은 이미 주문이 들어온 다음에 일어나는 일이다. ‘cancelled’나 ‘rejected’는 있던 주문이 취소되거나 거절된 것이니, 이것도 시작은 아니다. ‘ready’도 마찬가지로, 조리가 끝났다는 의미일 뿐이다.

그렇게 하나씩 지워나가자, 마침내 하나의 단어만 남았다.

“…received.”

솔라가 나지막이 말했다.

“주문이 시스템에 처음으로 ‘접수’되는 순간. 이 이벤트가 바로 ‘하나의 주문’이 탄생했다는 신호구나.”

깨달음과 함께 모든 것이 명확해졌다. OrderEvent 테이블은 주문의 탄생부터 죽음까지 모든 순간을 기록하는 일지였다. 그중에서 ‘주문 건수’라는 KPI를 계산하기 위해 필요한 것은 바로 ‘탄생’의 순간뿐이었다. 다른 상태들은 주문의 진행 과정을 보여줄 뿐, 새로운 주문의 발생을 의미하지 않았다.

솔라는 방금 자신이 얻은 ‘개체 발생 지점 판별기’라는 새로운 관점으로 루나가 그렸던 표를 다시 바라봤다.

order_idstatus비고 (새로운 주문인가?)
101receivedO (1건째 주문 발생)
101preparingX (1건째 주문의 상태 변경)
102receivedO (2건째 주문 발생)
101readyX (1건째 주문의 상태 변경)
101completedX (1건째 주문의 상태 변경)
102preparingX (2건째 주문의 상태 변경)
103rejectedX (주문 발생 실패)

“그럼 이렇게 봐야 해. received 상태를 가진 주문만 세는 거야. 101번 주문에 received 이벤트가 있으니 한 건. 102번 주문도 received 이벤트가 있으니 또 한 건. 103번은 received 없이 rejected만 있으니 주문 건수에서 제외. 그러니까 이 표에 기록된 실제 주문은 총 두 건이 맞아.”

스스로 세운 가설을 스스로 논파하고 새로운 답을 찾아낸 솔라의 얼굴에 뿌듯함이 스쳤다. 드디어 복잡한 이벤트 스트림 속에서 ‘진짜 주문’을 골라내는 기준을 찾은 것 같았다.

“이제 알겠어. 특정 기간의 주문 건수를 알려면, 그 기간 안에 received 이벤트가 발생한 고유한 order_id의 개수를 세면 되는 거구나.”

완벽한 결론이라고 생각한 순간, 솔라의 머릿속에 작은 의문이 하나 더 떠올랐다. 방금 내린 결론의 두 조각, ‘received 상태’와 ‘고유한 order_id’가 서로 어떻게 맞물려 돌아가는지, 그 관계가 아직 선명하게 그려지지 않았다.

“그런데 언니, 왜 굳이 received 상태와 ‘고유 order_id’ 두 가지를 함께 써야만 하는 거지? 그냥 received 이벤트가 발생한 횟수를 세면 안 되는 건가?”

3장: received 상태와 고유 order_id로 주문을 세는 이유

솔라의 마지막 질문이 남긴 여운이 가시기도 전에, 루나는 말없이 펜을 들어 두 사람 앞에 놓인 표에 새로운 줄을 추가했다. 이미 여러 주문의 여정이 기록된 작은 표였다. 솔라는 언니의 손끝을 따라, 방금 더해진 한 줄의 데이터가 만들어낼 미세한 균열을 예감했다.

order_idstatustimestamp
101received2023-10-26 10:00:00
101preparing2023-10-26 10:01:00
102received2023-10-26 10:02:00
101ready2023-10-26 10:05:00
101completed2023-10-26 10:06:00
102preparing2023-10-26 10:07:00
103rejected2023-10-26 10:08:00
102received2023-10-26 10:09:00

마지막에 추가된 줄은 order_id: 102, status: received였다. 방금 전 received 상태가 주문의 탄생을 알리는 유일한 신호라는 사실을 깨달았던 솔라는 자신의 새로운 규칙을 바로 적용해 보았다.

“잠깐만… 그럼 이 표에서 주문 건수는 received 이벤트를 세면 되니까… 101번 하나, 102번 하나, 그리고 또 102번 하나. 총 세 건인가?”

솔라의 목소리 끝이 살짝 떨렸다. 스스로 내린 결론이었지만 어딘가 찜찜한 구석이 있었다. order_id가 102번인 주문이 두 번이나 ‘탄생’한 셈이 되기 때문이었다. 고객이 주문 버튼을 두 번 눌렀거나, 네트워크 오류로 같은 요청이 중복 전송된 상황을 떠올리게 했다.

루나는 대답 대신, 표에 있는 102번 주문의 received 이벤트 두 개를 펜 끝으로 가리켰다.

“이 두 줄이 서로 다른 두 개의 주문을 의미할까, 아니면 하나의 주문이 시스템에 두 번 기록된 흔적일까?”

루나의 질문에 솔라는 꽉 막혔던 부분이 뚫리는 듯한 느낌을 받았다.

“아니, 주문은 하나지. 주문 번호가 102번으로 같으니까. 실수로 두 번 기록됐을 뿐이야. 그럼 received 이벤트의 수를 세는 것만으로는 부족하구나. 중복이 있을 수 있으니까.”

솔라의 눈이 빛났다. 퍼즐의 마지막 조각이 맞춰지는 순간이었다.

received 상태인 이벤트들을 먼저 찾고, 거기서 끝나는 게 아니라… 그 이벤트들의 order_id를 모아서, 중복된 번호를 하나로 합쳐야 해. 그리고 나서야 비로소, 고유한 주문의 개수를 셀 수 있는 거야.”

received 상태와 ‘고유 order_id’. 두 개의 조건이 왜 한 쌍으로 움직여야 하는지, 그 필연성이 선명하게 보였다. received는 ‘주문 탄생’이라는 사건을 포착하는 필터고, 고유 order_id는 그 사건의 주인공이 누구인지 식별하여 중복을 걸러내는 기준점이었다.

“정확히 이해했어. 그럼 이 표에 기록된 오늘 주문은, received 상태를 가진 주문 ID 101번과 102번. 이렇게 두 건이 맞는 거지.”

솔라는 확신에 차서 말했다. 루나는 고개를 끄덕이는 대신, 이번에는 표 위에 가로로 긴 선을 하나 그었다. 선 위쪽은 ‘어제’, 아래쪽은 ‘오늘’이라고 작게 적었다. 그리고 ‘오늘’ 영역에 새로운 이벤트를 추가했다.

어제
104received2023-10-25 21:00:00
오늘
104preparing2023-10-26 11:00:00

“솔라, 만약 오늘 집계 시간(start_at) 이후에 이런 이벤트를 발견하면 어떡할까? 104번 주문이 오늘 ‘조리 중’ 상태가 됐어. 이건 오늘의 신규 주문 건수에 포함해야 할까?”

솔라는 잠시 망설였다. 분명 ‘오늘’ 일어난 이벤트였다. 주방에서는 실제로 이 주문을 처리하고 있을 것이다. 하지만 솔라는 방금 자신이 세운 원칙을 떠올렸다.

“아니. 포함하면 안 돼.”

한 박자 늦게 나온 대답이었지만, 목소리에는 단단함이 실려 있었다.

“104번 주문이 오늘 ‘조리’되고 있는 건 맞아. 하지만 우리가 지금 세고 있는 건 ‘오늘의 총 작업량’이 아니라 ‘오늘 새로 발생한 주문 건수’잖아. 이 주문의 received 이벤트는 어제 일어났어. 이 주문의 ‘탄생일’은 어제인 거지. 따라서 오늘의 신규 주문 KPI에는 포함시키면 안 돼.”

솔라는 루나가 그어놓은 ‘어제’와 ‘오늘’의 경계선을 손가락으로 쓸었다. 요청 기간 [start_at, end_at) 안에서 received 상태가 발생한 고유 order_id를 센다는 복잡한 문장의 의미가 비로소 몸으로 느껴졌다. 그것은 단순히 데이터를 필터링하는 기술적 규칙이 아니었다. ‘신규 주문’이라는 비즈니스 지표(KPI)를 정확하게 정의하고, 그 정의에 따라 데이터를 바라보는 하나의 ‘관점’이었다.

“이제 보니, 같은 OrderEvent 테이블이라도 무엇을 알고 싶은지에 따라 데이터를 완전히 다르게 봐야 하는구나.”

솔라는 스스로의 깨달음에 놀란 듯 중얼거렸다. ‘오늘의 주문 건수’를 알기 위한 규칙과, 당장 눈앞의 주문이 어떤 상태인지 확인하는 것은 다른 문제일 수 있겠다는 생각이 스쳤다.

“그럼 언니, KPI를 집계할 때는 방금 우리가 정한 규칙대로 ‘기간 내 received된 고유 주문’을 세는 게 맞는데… 만약 고객이 101번 주문에 대해 문의하면, 나는 ‘주문 접수됨’이라고 알려줘야 할까, 아니면 ‘전달 완료됨’이라고 해야 할까?”

4장: 주문 KPI와 최신 주문 상태 조회는 왜 기준이 다른가요?

솔라의 질문은 이전과는 결이 달랐다. ‘무엇이 맞는가’를 넘어 ‘어떻게 사용해야 하는가’에 대한 고민이 담겨 있었다. OrderEvent 테이블을 분석해 ‘신규 주문 건수’라는 KPI를 뽑아내는 규칙은 이제 명확해졌다. 하지만 그 규칙이 모든 상황에 적용되는 만능 열쇠는 아니라는 직감이 들기 시작한 것이다. 고객이 자신의 주문 상태를 물었을 때, KPI 집계의 기준이 된 ‘received 상태’를 앵무새처럼 되뇌는 것은 어딘가 이상했다.

루나는 잠시 솔라의 얼굴을 바라보다, 두 사람이 함께 채워왔던 작은 표에서 시선을 돌렸다. 그리고는 옆에 놓여있던 새로운 빈 종이를 끌어왔다. 이전과는 다른, 딱 두 개의 칸으로 이루어진 아주 간단한 표를 그리기 시작했다. 첫 번째 칸의 제목은 ‘알고 싶은 것 (질문)’, 두 번째 칸은 ‘찾아야 할 값 (답)’이었다.

루나는 먼저 첫 번째 칸에 익숙한 질문을 적었다.

알고 싶은 것 (질문)찾아야 할 값 (답)
오늘 발생한 신규 주문은 총 몇 건?

이 질문은 솔라에게 이미 편안한 것이었다. 솔라는 지난 몇 시간 동안의 여정을 압축하듯, 자신 있게 답을 찾아 나갔다.

“우선, OrderEvent 테이블에서 오늘 날짜 범위 안에 있는 이벤트만 골라내. 그리고 그중에서 statusreceived인 것들만 남기는 거야. 마지막으로, 남은 이벤트들의 order_id를 모아서 중복을 제거하고 개수를 세면 돼.”

솔라는 루나와 함께 만들었던 이전 표를 흘끗 보며 101번 주문을 예로 들었다.

“101번 주문은 오늘 10시에 received 이벤트가 발생했으니까, ‘오늘의 신규 주문’에 포함돼. 그래서 답은…”

루나는 솔라가 대답을 완성하기를 기다렸다가, 표의 빈칸에 그 결과를 채워 넣었다.

알고 싶은 것 (질문)찾아야 할 값 (답)
오늘 발생한 신규 주문은 총 몇 건?1건 (101번 주문. received 시점이 오늘이므로)

루나는 잠시 펜을 멈췄다. 그리고 한 박자 늦게, 솔라가 방금 던졌던 바로 그 질문을 두 번째 줄에 옮겨 적었다.

“그럼 이건 어떨까?”

알고 싶은 것 (질문)찾아야 할 값 (답)
오늘 발생한 신규 주문은 총 몇 건?1건 (101번 주문. received 시점이 오늘이므로)
고객이 문의한 101번 주문의 현재 상태는?

솔라는 순간 말문이 막혔다. 자신의 머릿속을 그대로 옮겨놓은 듯한 질문이었다. 만약 방금 전과 똑같은 규칙을 적용한다면, 101번 주문을 신규 주문으로 판단하게 만든 근거인 ‘received 상태’를 답으로 내놓아야 했다. 하지만 그게 고객이 원하는 답일 리 없었다. 고객은 ‘주문이 잘 들어갔는지’가 아니라, ‘내 음식이 지금 어디쯤 왔는지’를 궁금해하고 있을 것이다.

솔라의 눈동자가 빠르게 이전 표의 101번 주문 기록을 훑었다.

10:00 received 10:01 preparing 10:05 ready 10:06 completed

“아…”

짧은 탄식이 흘러나왔다.

“고객에게는… ‘주문 전달 완료’ 상태라고 알려줘야 해. completed.”

“왜 그렇게 생각했어?”

“101번 주문과 관련된 이벤트가 네 개나 있지만, 고객이 ‘지금’의 상태를 물었으니 가장 마지막에 일어난 일을 알려주는 게 맞아. timestamp가 가장 늦은 이벤트, 즉 10시 6분에 발생한 completed 이벤트가 이 주문의 최종 상태니까.”

솔라는 스스로 답을 찾고도 놀란 표정이었다. 루나는 솔라의 대답을 두 번째 질문의 빈칸에 채워 넣었다.

알고 싶은 것 (질문)찾아야 할 값 (답)
오늘 발생한 신규 주문은 총 몇 건?1건 (101번 주문. received 시점이 오늘이므로)
고객이 문의한 101번 주문의 현재 상태는?전달 완료 (101번 주문의 이벤트 중 가장 마지막 시점의 상태)

완성된 표는 명백한 사실을 보여주었다. 똑같은 OrderEvent 테이블과 101번 주문 데이터를 사용했지만, 두 질문은 전혀 다른 답을 요구했고, 그 답을 찾는 과정 또한 완전히 달랐다.

첫 번째 질문, ‘기간별 주문 건수’는 비즈니스의 성과를 측정하기 위한 집계(Aggregation)의 관점이었다. 특정 기간에 ‘탄생’한 주문이 몇 개인지를 세는 것이 목표이기에, received라는 탄생의 순간과 고유 order_id를 기준으로 삼아야 했다.

반면 두 번째 질문, ‘개별 주문의 현재 상태’는 특정 주문의 최신 상태(Latest State)를 조회하는 관점이었다. 이 주문이 시간의 흐름 속에서 어떤 여정을 거쳤고, 지금 이 순간 어떤 지점에 있는지가 중요했다. 그래서 가장 늦은 타임스탬프(occurred_at)를 가진 이벤트를 찾아야만 했다.

“같은 데이터라도, 질문의 목적에 따라 바라보는 관점과 사용하는 필터가 완전히 달라지는 거였구나.”

솔라는 중얼거렸다. ‘하나의 데이터 소스는 하나의 진실만을 말해야 한다’는 자신의 암묵적인 가정이 깨지는 순간이었다. 데이터는 그 자체로 존재할 뿐, 의미는 그것을 활용하려는 사람의 ‘질문’과 ‘관점’이 부여하는 것이었다. KPI 집계와 최신 상태 조회의 기준을 분리해야 한다는 문장은, 단순히 기술적인 선택이 아니라 데이터를 올바르게 사용하기 위한 근본적인 원칙이었던 것이다.

스스로의 손으로 두 가지 다른 길을 통해 답을 찾아낸 솔라의 얼굴에 안도감이 번졌다. 하지만 그 안도감은 이내 새로운 종류의 불안감으로 바뀌었다.

“알겠어. 이제 왜 두 기준이 다른지, 그리고 어떻게 분리해서 봐야 하는지는 완벽하게 이해했어. 그런데… 이렇게 세운 ‘기간 내 received된 고유 주문’이라는 기준이 정말로 정확하다고 어떻게 확신할 수 있지? 우리가 생각하지 못한 예외 케이스가 어딘가에 숨어있을 수도 있잖아.”

5장: 변경 된 집계 기준의 검증과 활용

솔라의 얼굴에 번졌던 안도감은 금세 새로운 질문의 무게로 가라앉았다. 올바른 집계 기준을 찾아냈다는 지적인 만족감과 별개로, 이 기준이 현실의 복잡하고 지저분한 데이터 속에서도 정말로 굳건할지에 대한 실무적인 불안감이 고개를 들었다. 우리가 미처 생각하지 못한 함정이 숨어있다면, 이 모든 과정은 또 다른 오류를 만드는 것에 지나지 않을 것이다.

루나는 솔라의 불안을 읽은 듯, 지금까지의 논의가 담긴 종이들을 옆으로 밀어두고 마지막으로 빈 종이 한 장을 꺼내 들었다. 그리고는 이전보다 훨씬 더 기이하고 압축적인 상황을 작은 표 안에 그려 넣기 시작했다.

“이런 경우는 어떨까?”

루나의 목소리는 평소처럼 차분했지만, 표 안에 담긴 내용은 그렇지 않았다.

order_idstatustimestamp
201received2023-10-26 12:30:00
201ready2023-10-26 12:30:00

표는 단 두 줄뿐이었지만, 그 안에 담긴 모순은 솔라를 혼란에 빠뜨리기에 충분했다. 주문 번호 201번이 ‘접수(received)’되는 순간과 ‘준비 완료(ready)’되는 순간이 타임스탬프 상으로 완벽히 동일했다. 시스템의 아주 짧은 랙(lag)이나, 비동기적으로 처리된 이벤트가 동시에 데이터베이스에 기록되는 극단적인 경우를 상상하게 했다.

“어… 이건… 주문이 접수되자마자 준비가 완료됐다고? preparing 단계도 없이? 이걸 어떻게 세어야 하지?”

솔라의 목소리에 당혹감이 묻어났다. 그녀가 힘들게 세운 ‘기간 내 received된 고유 주문’이라는 원칙이 이 기묘한 예외 앞에서 흔들리는 것 같았다. ‘단순히 코드를 수정하면 KPI가 자동으로 정확해질 것’이라는 안일한 믿음이 얼마나 위험한지, 이 작은 표가 경고하고 있었다.

“우리가 세운 규칙대로 한번 해봐.”

루나는 펜을 내려놓고 솔라가 스스로 길을 찾도록 한 걸음 물러섰다. 솔라는 심호흡을 한번 하고, 자신이 지금까지 쌓아 올린 논리의 계단을 차근차근 다시 밟아 내려갔다.

“첫째, 오늘 날짜(2023-10-26)를 기준으로 이벤트를 본다. 이 두 이벤트 모두 오늘 일어났으니 통과.”

“둘째, 이 중에서 statusreceived인 이벤트만 골라낸다. 그럼 첫 번째 줄이 남네.”

“셋째, 그렇게 골라낸 이벤트들의 order_id를 중복 없이 모은다. 그럼 ‘201’ 하나만 남지.”

“마지막으로, 모아진 고유 order_id의 개수를 센다.”

거기까지 생각이 미치자, 솔라의 눈이 커졌다.

“…한 건이네. ready 이벤트가 같은 시간에 있든 말든, 우리 규칙에 따르면 이 주문은 그냥 ‘한 건’의 신규 주문일 뿐이야.”

스스로 내린 결론에 솔라는 놀라움을 감추지 못했다. 자신의 규칙이 생각보다 훨씬 더 견고하다는 사실을 깨달은 것이다. ready 이벤트는 received를 찾는 필터링 단계에서 자연스럽게 무시되었다. 규칙이 명확하고 단순했기 때문에, 복잡해 보이는 예외 상황을 아무런 추가 조건 없이 우아하게 처리해냈다.

“알겠다… 정확성을 확신하는 방법은, 머릿속으로만 ‘이게 맞겠지?’ 하고 고민하는 게 아니구나. 오히려 반대로, 우리가 만든 기준을 깨뜨릴 수 있는 가장 극단적이고 이상한 예시들을 직접 만들어서, 그 테스트를 통과하는지 확인하는 거였어.”

솔라는 비로소 ‘테스트’의 진짜 의미를 깨달았다. 그것은 단순히 코드가 작동하는지 확인하는 절차가 아니었다. 논리와 현실 사이의 간극을 메우고, 불확실성을 신뢰로 바꾸는 가장 중요한 과정이었다. 좋은 KPI 집계 로직은 복잡한 예외 처리를 잔뜩 추가한 것이 아니라, 어떤 예외 상황이 닥쳐도 흔들리지 않는 단순하고 명확한 핵심 원칙을 가진 것이었다.

새로운 깨달음과 함께 솔라는 자신의 노트북을 다시 활짝 열었다. 처음 그녀를 괴롭혔던, 480개의 이벤트와 120건의 주문이라는 불일치의 근원지였다. 하지만 이제 그 화면은 문제 덩어리가 아닌, 새로 얻은 도구를 시험해 볼 완벽한 기회로 보였다.

솔라는 코드 편집기 대신, 빈 메모장을 먼저 열었다. 그리고 망설임 없이 타이핑을 시작했다.

[주문 건수 KPI 집계 로직 - 검증 케이스]

  1. 기본 케이스: 주문 1건이 received, preparing, ready, completed 상태를 순서대로 가짐.

    • 입력: order_id: 301, 4개 이벤트
    • 기대 결과: 신규 주문 1건으로 집계
  2. 중복 received 케이스: 네트워크 오류로 동일 주문의 received 이벤트가 2번 기록됨.

    • 입력: order_id: 302, received 이벤트 2개
    • 기대 결과: 신규 주문 1건으로 집계
  3. 기간 경계 케이스: 어제 received된 주문이 오늘 preparing 상태로 변경됨.

    • 입력: order_id: 303, received (어제), preparing (오늘)
    • 기대 결과: 오늘의 신규 주문 0건으로 집계
  4. 동시 발생 케이스: receivedready 이벤트가 동일한 타임스탬프로 기록됨.

    • 입력: order_id: 304, received/ready 동시 발생
    • 기대 결과: 신규 주문 1건으로 집계
  5. 주문 실패 케이스: received 없이 rejected 이벤트만 발생함.

    • 입력: order_id: 305, rejected 이벤트 1개
    • 기대 결과: 신규 주문 0건으로 집계

자신이 겪었던 모든 혼란과 깨달음의 과정이 고스란히 담긴 테스트 목록을 바라보며, 솔라는 희미하게 미소 지었다. 이제 더 이상 OrderEvent 테이블의 행 수를 보며 불안해하지 않을 것이다. 숫자의 함정에 빠지는 대신, 그 숫자가 어떤 질문에 답하고 있는지, 그리고 그 답이 얼마나 단단한 기준 위에 서 있는지를 증명할 수 있게 되었기 때문이다. 이제야 비로소, 정확한 주문 건수 대시보드를 만들 준비가 끝났다.