한 SaaS에서 고객에게 ‘처리한 문서 수만큼’, ‘API를 쓴 만큼’, ‘AI가 생성한 결과만큼’ 청구하려고 한다. 요금표를 만드는 일보다 먼저 해야 할 것은 무엇이 한 번의 사용인지, 언제 그 사용이 확정되는지 정하는 일이다. 재시도한 요청을 두 번 세면 고객은 청구서를 신뢰하지 않는다. 반대로 실패한 사용을 누락하면 회사의 비용과 매출이 어긋난다.
결제 서비스에 이벤트를 보내는 것만으로 계측 문제가 사라지지는 않는다. Stripe의 미터 이벤트 API는 이벤트 이름, 고객, 값, 고유 식별자를 받아 사용량을 기록한다. 하지만 외부 서비스의 중복 방지는 자체 업무 이벤트의 정의와 장기 대사를 대신하지 않는다. 청구의 출발점은 제품 내부의 사용량 계약이다.
‘API 호출 한 번’이 정말 청구 단위인가
문서 변환 API를 예로 들어 보자. 고객이 요청을 보냈지만 유효하지 않은 파일이라 즉시 거부됐다. 서버 오류로 같은 요청을 세 번 재시도했다. 10페이지 파일에서 9페이지까지 처리한 뒤 실패했다. 이 셋을 모두 같은 ‘1회 호출’로 청구할 것인가? 고객 계약과 제품 화면에 쓰인 단위가 먼저다. 성공한 작업, 처리한 페이지, 저장한 바이트, 생성된 결과 중 무엇을 세는지 결정해야 한다.
한 단위를 여러 시스템에서 각자 계산하면 숫자가 달라진다. API 게이트웨이는 요청 수를 세고, 작업 큐는 시도 횟수를 세고, 모델 서버는 토큰을 세기 때문이다. 과금 가능한 사건은 업무 결과가 확정된 경계에서 한 번 만들고, 그 사건이 어떤 원시 처리 기록을 근거로 하는지 연결한다. 환불·무상 재처리·테스트 계정 제외 규칙도 이 경계에서 명시한다.
사용량 이벤트는 작은 회계 원장처럼 다룬다
최소 이벤트에는 고객사 ID, 과금 단위, 수량, 발생 시각, 업무 객체 ID, 고유 이벤트 ID, 가격 정책 버전의 참조가 필요하다. 돈의 최종 금액을 매번 이벤트에 넣어야 한다는 뜻은 아니다. 그날 어떤 정책으로 어떤 양을 확정했는지 재구성할 수 있어야 한다. 가능하면 확정된 원시 이벤트를 덮어쓰지 않고 보정 이벤트나 별도의 조정 이력으로 남긴다. 그래야 고객 문의가 왔을 때 숫자의 변화를 설명할 수 있다.

이벤트 ID는 ‘재시도할 때마다 새 UUID’가 아니라 같은 업무 사건이면 다시 보내도 같은 ID가 나오도록 연결해야 한다. 결제 제공자가 제공하는 중복 방지 범위에는 시간 제한이 있을 수 있다. Stripe의 미터 이벤트 문서도 고유 식별자의 중복 검사 기간을 설명한다. 따라서 내부 원장에 고유 제약을 두고, 외부 전송 상태를 별도로 관리한다. 네트워크 오류로 응답을 못 받았다고 무작정 새 이벤트로 다시 보내면 이중 청구의 출발점이 된다.
월말이 아니라 매일 숫자를 맞춘다
제품의 원장 합계와 외부 과금 서비스의 집계 합계는 서로 비교해야 한다. 기간, 고객사, 미터, 단위, 취소·보정 상태가 같아야 의미 있는 비교가 된다. Stripe의 미터 이벤트 요약 API는 기간별 집계를 제공하지만, 미터 이벤트가 비동기로 집계될 수 있으므로 즉시 동일한 값을 기대하면 안 된다. 대사 작업에는 처리 지연을 고려한 시간 창과 재확인 절차가 필요하다.

차이를 발견하면 단순히 부족한 양을 한 번 더 전송하지 않는다. 누락 이벤트인지, 늦게 도착한 이벤트인지, 고객사 매핑이 틀렸는지, 요금 정책이 바뀌었는지 원인을 나눈다. Stripe처럼 이벤트 조정 기능이 있는 서비스라도 조정 가능한 조건과 기간이 있다. 정정 경로를 확인하고, 이미 확정된 청구서라면 별도의 크레딧이나 다음 청구 조정 등 계약에 맞는 절차를 따라야 한다.
고객이 볼 수 있는 숫자를 만든다
사용량 화면에는 현재 기간, 포함된 기본 제공량, 집계 지연 가능성, 청구 대상에서 빠진 테스트·실패 요청의 기준을 보여 준다. 고객이 합계를 클릭했을 때 업무 객체나 날짜별 내역으로 내려갈 수 있어야 문의가 줄어든다. 다만 토큰별 세부 사용이나 민감한 입력 원문을 모두 보여 줄 필요는 없다. 설명 가능성과 개인정보 최소화를 함께 고려한다.
요금 정책이 바뀌는 날은 특히 위험하다. 월 중간에 플랜이 바뀌면 변경 전후 이벤트에 어느 가격을 적용하는가? 시간대 기준은 무엇인가? 고객사 병합이나 계정 이전이 발생하면 과거 사용량은 어디에 귀속되는가? 이 질문의 답을 코드에 흩뿌리지 말고 정책 버전과 전환 시각으로 관리한다. 정책을 수정할 때는 대표 고객의 이전 기간 데이터를 재계산해 기대한 청구액과 비교한다.
실패는 숨기지 말고 상태로 남긴다
전송 실패 이벤트는 재시도 대기, 영구 실패, 수동 검토 같은 상태가 있어야 한다. 청구 마감 전에 누락 규모를 볼 수 있도록 경보를 둔다. 외부 과금 서비스가 일시적으로 느려도 핵심 제품 작업이 멈추지 않도록 계측 전송을 분리하되, 원시 이벤트 자체는 안전하게 남겨야 한다. 고객에게 보여 주는 예상 금액과 최종 청구 금액이 다른 경우, 어떤 추가 이벤트나 조정이 있었는지도 설명할 수 있어야 한다.
구현 체크리스트
- 청구 가능한 업무 사건과 실패·재시도 규칙을 정의했는가?
- 고유 이벤트 ID와 내부 중복 방지 제약이 있는가?
- 가격 정책과 고객사 매핑의 버전을 추적하는가?
- 원장·외부 집계·청구서 금액을 정기적으로 대사하는가?
- 늦은 이벤트와 정정 요청의 처리 경로가 있는가?
- 고객이 사용량의 기간·단위·내역을 이해할 수 있는가?
사용량 기반 과금은 ‘미터에 숫자를 보내는 기능’이 아니라, 어떤 사용을 왜 청구했는지 끝까지 설명할 수 있는 데이터 흐름이다. 제품과 재무 시스템이 같은 사건을 같은 방식으로 셀 때 비로소 신뢰할 수 있는 청구서가 나온다.