Big Project · AI's EYE 08
24시간·7일·30일, KPI를 위한 시간 계약의 비밀
기간만 바꾸고 같은 timeline 응답을 재사용하면 충분해 보인다. 시간대와 구간 간격이 왜 KPI 결과까지 바꿀 수 있는지 놓치기 쉽다.
근거 · 프로젝트 문서 · 코드 · 테스트
1장: KST와 [start_at, end_at)으로 정확한 시간 경계 세우기
솔라는 노트북 화면에 떠 있는 두 개의 브라우저 창을 번갈아 보며 미간을 찌푸렸다. 왼쪽 창에는 ‘최근 24시간’ 매출 그래프가, 오른쪽 창에는 ‘최근 7일’ 매출 그래프가 펼쳐져 있었다. 둘 다 같은 매장의 데이터를 보여주는 대시보드였다. 언뜻 보기에는 문제가 없어 보였다. 하지만 솔라의 시선은 그래프의 한 지점에 머물러 있었다.
“이상하네… 분명히 어제 마감 직전에 큰 주문이 하나 더 있었는데.”
최근 24시간 그래프에는 자정을 막 넘긴 시점에 높은 막대가 하나 솟아 있었다. 그런데 7일치 데이터를 일별로 보여주는 그래프에서는 어제 날짜의 매출 총합이 솔라의 기억보다 적게 잡혀 있었다. 마치 그 마지막 주문이 어딘가로 증발해버린 것 같았다. 단순한 조회 기간 변경 문제라고 생각했는데, 숫자가 맞지 않자 머릿속이 복잡해졌다.
“기간만 바꾸면 알아서 보여주는 게 아니었나?”
혼잣말을 하며 고개를 갸웃거리던 솔라는 결국 노트북을 들고 거실에 있는 루나에게 다가갔다.
“언니, 잠깐 이것 좀 봐줄래? 데이터가 하나 비는 것 같아.”
루나는 솔라가 내민 노트북 화면을 잠시 들여다보았다. 솔라가 가리키는 두 개의 그래프와 어긋나는 숫자에 대한 설명을 조용히 듣고 있었다.
“이 주문, 정확히 언제 들어온 건지 알아?”
루나의 질문에 솔라는 잠시 망설이다가 주문 관리 시스템에 접속해 해당 주문의 상세 내역을 찾아냈다.
“여기 있다. ‘주문 시각: 5월 10일 23시 58분’.”
“그럼 그 주문은 5월 10일 매출에 포함되는 게 맞네.”
“그치? 그런데 7일치 그래프에서는 5월 10일 매출 집계에 이 금액이 빠져있어. 대신 오늘 날짜인 5월 11일 데이터에 합산된 것도 아니야. 그냥 사라졌어.”
솔라의 말처럼, 그 주문은 두 날짜 사이의 경계에서 실종된 듯 보였다. 솔라는 당연히 시스템이 ‘기간’만 바꾸면 그에 맞춰 같은 데이터를 다른 모양으로 보여줄 거라고 기대했다. 24시간, 7일, 30일. 버튼만 누르면 되는 일이라고 생각했다.
루나는 말없이 테이블 위에 놓인 메모지와 펜을 가져왔다. 그리고 이렇게 적었다.
주문 발생 시각 (KST): 5월 10일 23:58
“데이터베이스에는 이 시각이 어떻게 저장될까? 보통 글로벌 서비스를 고려해서 국제 표준시(UTC)를 사용하지.”
루나는 그 아래에 한 줄을 더 추가했다. 한국은 UTC보다 9시간 빠르므로, 이 시각을 UTC로 바꾸면 9시간을 빼야 했다.
저장된 시각 (UTC): 5월 10일 14:58
“여기까진 문제가 없어 보여.” 솔라가 말했다.
“그럼 ‘5월 10일 하루’의 데이터를 가져오라는 요청을 시스템에 보낸다고 상상해보자. 시스템은 어떻게 ‘5월 10일’이라는 기간을 알아들을까?”
루나는 솔라의 대답을 기다리지 않고, 흔히 저지르는 실수를 펜으로 그려 보였다.
잘못된 요청: 5월 10일 00:00:00 부터 5월 10일 23:59:59 까지
“이렇게 하면 간단할 것 같지만, 여기엔 두 가지 함정이 있어.”
루나는 첫 번째 함정으로 ‘시간대(Timezone)’를 가리켰다.
“만약 시스템이 우리가 한국 시간에 맞춰 ‘5월 10일’이라고 요청한 걸 깜빡하고, 그냥 저장된 UTC 시간으로만 데이터를 찾는다면 어떻게 될까? UTC 기준 ‘5월 10일’은 한국 시간으로 ‘5월 10일 오전 9시’부터 ‘5월 11일 오전 9시’까지야. 우리가 생각하는 ‘하루’와 전혀 다른 시간대가 되어버리지.”
“아… 그래서 자정 근처 데이터들이 다음날로 넘어가거나 어제 날짜로 당겨지는 오류가 생기는 거구나.”
“맞아. 그래서 데이터를 조회할 땐, 반드시 ‘어떤 시간대를 기준으로(KST) 하루를 셀 것인가’를 명확히 약속해야 해. 시스템이 내부적으로 UTC를 쓰더라도, 사용자에게 보여주는 모든 시간 경계는 KST 기준으로 변환해서 다시 그어줘야 하는 거지.”
루나는 이어서 두 번째 함정, 바로 ‘경계’ 자체의 문제를 설명했다. 그녀는 메모지에 새로운 기호를 적었다.
[start_at, end_at)
“이 괄호, 수학 시간에 본 적 있지? 시작점은 포함하고([), 종료점은 포함하지 않는다())는 뜻이야. ‘5월 10일 하루’를 이 방식으로 표현하면 이렇게 돼.”
시작: 5월 10일 00:00:00 KST (포함)
종료: 5월 11일 00:00:00 KST (포함 안 함)
“왜 굳이 이렇게 복잡하게 해? 그냥 ‘23시 59분 59초’까지라고 하면 안 돼?” 솔라가 물었다.
“‘59초’ 다음엔 뭐가 있지? 밀리초, 마이크로초… 끝이 없어. ‘23:59:59’라고 지정하면 ‘23:59:59.5’에 들어온 주문은 누락될 수 있잖아. 하지만 종료 시점을 다음 날 0시 정각으로 잡고 그 시점은 포함하지 않는다고 약속하면, 두 날짜 사이에 데이터가 끼어서 사라지거나 중복으로 집계될 위험이 없어지지. 5월 11일 00:00:00 정각에 들어온 주문은 정확히 5월 11일 데이터로 잡힐 테니까.”
솔라는 그제야 고개를 끄덕였다. 사라진 주문의 행방이 보이기 시작했다. 아마도 시스템 어딘가에서 시간대 변환이나, 경계 규칙 처리에 미세한 어긋남이 있었을 것이다. 단순히 기간만 바꿔서 API를 호출하면 될 거라는 생각은 너무 순진했다. ‘하루’라는 당연해 보이는 개념조차도, 기계가 오해하지 않도록 명확한 ‘시간 계약’으로 정의해주어야 했다.
“그럼… 기간을 조회한다는 건 그냥 시간만 휙 바꾸는 게 아니라, ‘어떤 시간대를 기준으로, 시작과 끝 경계를 어떻게 정의할지’를 매번 정확하게 약속하고 지키는 거였구나.”
솔라는 자신이 ‘시간 경계 결정자’가 된 기분으로 노트북 화면을 다시 보았다. 이제 그래프의 숫자가 아니라, 그 숫자를 만들어내는 보이지 않는 규칙이 보이기 시작했다.
문제가 해결되자 새로운 궁금증이 떠올랐다.
“알겠어. 시간의 경계를 정확하게 나누는 건 이제 이해했어. 그런데 언니, ‘최근 24시간’ 그래프는 시간 단위(1h)로 점을 찍어주는데, ‘최근 7일’은 왜 하루 단위(1d)로 큼직하게 보여주는 거야? 이것도 뭔가 공정한 비교를 위한 약속 같은 게 있는 걸까?”
2장: 기간 특성별 ‘의미 단위’에 맞는 집계 간격 선택하기
솔라의 질문에 루나는 바로 대답하는 대신, 솔라가 들고 있던 노트북을 받아 들었다. 그리고는 ‘최근 7일’ 조회 화면에서 개발자 도구를 열어 API 요청 주소를 복사했다. 잠시 무언가를 수정하더니, 브라우저 주소창에 직접 붙여넣고 엔터를 쳤다.
잠시 후 화면에 나타난 그래프는 가히 충격적이었다. 7일 치의 데이터가 1시간 단위로 쪼개져, 총 168개의 가느다란 막대가 빽빽하게 스크린을 채웠다. 개별 막대는 너무 얇아 값을 읽기조차 어려웠고, 전체적인 흐름을 파악하는 것은 불가능에 가까웠다. 마치 숲을 보려다 나뭇잎의 솜털만 현미경으로 들여다보는 기분이었다.
솔라가 어리둥절한 표정으로 화면을 보자, 루나는 이번엔 반대의 실험을 했다. ‘최근 24시간’ 조회 화면에서 집계 간격을 ‘하루(1d)’ 단위로 강제 변경했다. 그러자 24개의 시간별 막대는 온데간데없이 사라지고, 어제와 오늘에 걸친 24시간의 모든 매출이 뭉쳐진 거대한 막대 단 하나만 덩그러니 남았다. 점심시간의 급증도, 새벽의 주문량도 모두 이 하나의 막대 안에 섞여 아무런 의미도 읽을 수 없게 되었다.
“어때? 7일치 데이터를 시간 단위로 보거나, 24시간 데이터를 하루 단위로 보면 어떤 정보를 얻을 수 있을까?”
루나가 만든 극단적인 두 개의 그래프는 솔라의 질문에 대한 대답을 대신하고 있었다.
“하나는 너무 복잡해서 아무것도 모르겠고… 다른 하나는 너무 단순해서 아무것도 모르겠어.”
솔라는 투덜거리면서도 자신의 처음 생각이 틀렸을지도 모른다고 느꼈다. 처음에는 모든 기간을 24개 정도의 막대로 통일해서 보여주면, 시각적으로 일관성 있고 비교하기 편할 거라고 막연히 생각했다. 예를 들어 7일이면 7시간씩, 30일이면 30시간씩 묶어서 24개의 막대를 만드는 식으로 말이다. 하지만 방금 본 두 개의 실패한 그래프는 그 생각이 얼마나 순진했는지를 보여주었다.
“‘최근 24시간’ 데이터를 볼 때, 우리가 진짜 궁금한 건 뭘까? 어제 이 시간과 지금을 비교하거나, 점심시간처럼 특정 시간대에 주문이 몰리는지 같은 거 아닐까?”
루나가 테이블 위에 놓인 메모지를 가리키며 말했다.
“맞아. 하루 동안의 변화 흐름이 중요하지.”
“그럼 ‘최근 30일’ 데이터를 볼 때는? 한 달 전의 어느 날 오후 3시와 오늘 오후 3시의 매출을 직접 비교하는 게 중요할까, 아니면 주중과 주말의 매출 차이나 월초와 월말의 전반적인 경향을 보는 게 중요할까?”
“당연히 후자지. 한 달 치를 보는데 시간 단위까지 비교하는 건 의미 없잖아. 요일별, 주차별 패턴이 중요하지.”
솔라는 그제야 무언가 깨달은 듯 고개를 끄덕였다. 그녀는 두 개의 실패한 그래프와 원래의 정상적인 그래프를 번갈아 보았다. 문제는 ‘데이터의 양’이나 ‘막대의 개수’가 아니었다. 핵심은 ‘무엇을 보려고 하는가’에 있었다.
‘최근 24시간’ 조회는 단기적인 변화, 즉 시간의 흐름에 따른 실시간 변동을 파악하기 위함이다. 따라서 집계 단위를 시간(1h)으로 잘게 나누어야만 점심시간 피크나 새벽 주문 같은 의미 있는 패턴을 발견할 수 있다. 만약 이걸 하루(1d) 단위로 뭉뚱그린다면, 그건 마치 하루치 신문을 한 문장으로 요약해버리는 것처럼 모든 정보를 잃게 된다.
반면, ‘최근 7일’이나 ‘30일’ 조회는 장기적인 추세를 보기 위함이다. 이때 중요한 비교 단위는 ‘시간’이 아니라 ‘하루(1d)’다. 월요일과 화요일의 매출을 비교하고, 지난주와 이번 주의 실적을 비교하는 것이 목적이다. 이걸 굳이 시간 단위로 나누는 것은, 장편 소설을 읽으면서 줄거리는 놓치고 각 문장의 글자 수만 세는 것과 같았다. 너무 많은 세부 정보가 오히려 큰 그림을 보는 데 방해가 될 뿐이었다.
“아…! 기간마다 우리가 알고 싶어 하는 정보의 ‘의미 단위’가 다르기 때문이었구나.”
솔라는 나지막이 말했다.
“‘공정한 비교’라는 게 모든 걸 똑같은 모양, 똑같은 개수로 맞추는 게 아니었어. 24시간을 볼 때는 ‘시간’이 의미 있는 비교 단위가 되고, 7일을 볼 때는 ‘하루’가 의미 있는 비교 단위가 되는 거였네. 각 기간의 특성에 맞는 가장 적절한 돋보기를 선택하는 것처럼 말이야.”
자신이 ‘시간 경계 결정자’가 되었다고 생각한 지 얼마 지나지 않아, 솔라는 이제 각 데이터에 맞는 ‘의미 단위 선택기’의 역할까지 이해하게 된 기분이었다. 시각적인 통일성이라는 덫에 빠질 뻔했던 자신을 돌아보며, 그녀는 한 걸음 더 나아간 질문을 떠올렸다.
“그럼 언니, 시간 경계 규칙처럼 이 ‘집계 간격’ 규칙도 서버와 클라이언트 사이에 아주 중요한 약속이 되겠네. 서로 다른 기간의 KPI를 분석할 때, ‘우리는 합의된 의미 단위로 공정하게 비교하고 있습니다’라고 말하려면 이 약속이 반드시 지켜져야 하는 거니까.”
3장: 공정한 KPI 비교를 위한 API 계약의 필수성
솔라는 이전 대화에서 루나가 사용했던 메모지를 자신의 책상으로 가져왔다. 어지럽게 쓰여 있던 시뮬레이션과 계산 흔적들을 지우고, 깨끗한 면에 자신이 지금까지 이해한 것들을 정리하기 시작했다. 마치 중요한 시험의 핵심 요약 노트를 만들 듯, 그녀의 펜이 신중하게 움직였다.
첫 번째 원칙, ‘시간 경계의 명확성’.
1. 데이터 조회 시, 시간 경계는 KST 기준으로 명확히 하고, [start_at, end_at) 규칙을 따른다.
두 번째 원칙, ‘의미 단위의 적절성’.
2. 집계 간격(interval)은 기간의 특성에 맞는 의미 단위(24시간→1h, 7일/30일→1d)를 선택한다.
솔라는 자신이 적은 두 개의 명료한 문장을 보며 만족스럽게 고개를 끄덕였다. 데이터가 사라지거나 왜곡되는 일을 막기 위한 두 개의 든든한 방패처럼 보였다. 이제 이 두 원칙만 잘 지키면, 누구든 언제나 공정한 데이터를 볼 수 있을 거라는 확신이 들었다. 그녀는 노트를 들고 다시 거실에 있는 루나에게 향했다.
“언니, 내가 한번 정리해봤어. 결국 이 두 가지 규칙을 지키는 게 핵심인 거지? ‘정확한 시간 경계’랑 ‘의미 있는 집계 간격’.”
루나는 솔라가 내민 노트를 잠시 들여다보더니, 미소를 띠며 말했다. “훌륭한 요약이야, 솔라. 각 원칙의 중요성은 정확히 이해했어. 그런데, 만약 두 팀이 이 훌륭한 원칙들을 하나씩만 나눠 가지면 어떻게 될까?”
“나눠 가진다고?”
솔라의 의아한 표정에, 루나는 테이블 위에 새로운 시나리오를 그리기 시작했다. 마치 작은 역할극의 무대를 꾸미는 것 같았다.
“여기 ‘A팀’과 ‘B팀’이 있다고 하자. 두 팀 모두 ‘지난 7일간의 총매출’이라는 똑같은 KPI를 계산해야 해. A팀은 솔라 네가 정리한 1번 원칙, ‘시간 경계 규칙’은 칼같이 지켰지만, 바쁘다는 핑계로 집계 간격은 그냥 편한 대로 썼어. 반면 B팀은 2번 원칙, ‘기간별 집계 간격’은 정확히 따랐지만, 시간대 변환 같은 건 복잡하다고 생각해 UTC 기준으로 대충 날짜를 계산했지.”
루나는 솔라가 이해하기 쉽도록 가상의 데이터를 테이블에 그려 보였다.
| 팀 | 시간 경계 규칙 (1번) | 집계 간격 규칙 (2번) | 결과 (지난 7일 매출) |
|---|---|---|---|
| A팀 | O (KST, [start, end)) | X (임의 간격 사용) | 1억 250만 원 |
| B팀 | X (UTC 기준) | O (1d 간격) | 1억 210만 원 |
| 정답 | O (KST, [start, end)) | O (1d 간격) | 1억 230만 원 |
테이블은 충격적인 결과를 보여주었다. 두 팀 모두 일부 ‘옳은’ 규칙을 따랐음에도 불구하고, 어느 팀도 정답에 도달하지 못했다. 더 심각한 문제는, A팀과 B팀이 서로 다른 숫자를 들고 ‘자신들의 데이터가 맞다’고 주장할 것이라는 점이었다. 회의는 어느 숫자가 맞는지에 대한 소모적인 논쟁으로 번질 게 뻔했다.
솔라는 잠시 말을 잃었다. 그녀가 독립적이고 훌륭하다고 생각했던 두 원칙이, 함께 지켜지지 않았을 때 오히려 혼란을 가중시키는 모습을 목격한 것이다. 각각은 옳은 방향을 가리키고 있었지만, 함께 발을 맞추지 않자 전혀 다른 곳으로 향하고 있었다.
“아….”
낮은 탄성이 솔라의 입에서 흘러나왔다.
“두 규칙이 한 세트가 아니면 아무 의미가 없구나. 하나라도 어긋나면, ‘공정한 비교’는커녕 비교 자체가 불가능해지네. 각자 다른 자를 가지고 길이를 재는 거랑 똑같잖아.”
솔라는 비로소 깨달았다. 시간대, 경계 규칙, 집계 간격은 개별적으로 선택할 수 있는 옵션이 아니었다. 그것들은 ‘공정한 KPI 비교’라는 목표를 달성하기 위해 반드시 함께, 그리고 예외 없이 지켜져야 하는 하나의 묶음, 즉 ‘계약’이었다. 한쪽이 “나는 KST 기준으로 5월 10일 0시부터 5월 11일 0시 전까지, 하루 단위로 집계한 값을 원해”라고 명시적으로 요구하면, 다른 한쪽은 그 계약서의 모든 조항을 정확하게 이행한 결과를 내놓아야만 했다. 어느 한 조항이라도 자의적으로 해석하거나 무시하는 순간, 계약은 파기되고 신뢰는 깨진다.
“그래서 ‘API 계약’이라고 부르는 거구나. 클라이언트와 서버, 더 나아가 데이터를 사용하는 모든 사람들 사이의 깨뜨릴 수 없는 약속.”
솔라는 자신의 자리로 돌아왔다. 그리고 방금 작성했던 노트 위에 굵은 글씨로 제목을 새로 적었다.
<KPI 공정성 확보를 위한 타임라인 API 계약서 (v1)>
그녀는 더 이상 두 개의 분리된 원칙을 나열하지 않았다. 대신, 하나의 계약을 구성하는 필수 조항들로 내용을 재구성했다.
- 시간대 (Timezone): 모든 시간 경계는 한국 표준시(KST)를 기준으로 해석한다.
- 조회 구간 (Range):
start_at은 포함하고,end_at은 포함하지 않는다 ([start_at, end_at)). - 집계 간격 (Interval): 조회 기간에 따라 다음과 같이 사전에 합의된 간격을 사용한다.
- 최근 24시간:
1h - 최근 7일, 30일, 사용자 지정 기간:
1d
- 최근 24시간:
- 적용 범위: 상기 조항들은
summary와timeline등 매출 관련 모든 KPI 집계에 동일하게 적용된다.
자신이 만든 초안 계약서를 보며 솔라는 이제 ‘KPI 공정성 검증자’가 된 기분을 느꼈다. 이제 그녀는 단순히 그래프의 숫자를 읽는 것을 넘어, 그 숫자가 어떤 계약에 의해 산출되었는지, 그 계약이 공정한 비교를 보장하는지를 검증할 수 있는 눈을 갖게 된 것이다. 다른 기간의 데이터를 비교할 때, 그녀는 이제 “이 두 데이터는 같은 계약 아래에서 생성되었는가?”라는 가장 근본적인 질문을 던질 수 있게 되었다.