잔고 없는 사용자가 무료 서비스도 못 썼습니다 - 0원 결제가 폭로한 시스템의 숨은 가정
무료 서비스를 막아버린 결제 시스템
운영 중인 SaaS의 예약 흐름에서 사용자가 무료 예약을 시도했는데 시스템이 거부했습니다. 거부 사유는 단순했습니다. 포인트 계좌가 없는 회원이었기 때문입니다.
이 사용자는 포인트를 적립한 적이 없어서 포인트 계좌 자체가 생성되지 않은 상태였습니다. 무료 예약이라 결제할 금액도 없는데, 시스템은 "포인트 잔고를 확인할 수 없습니다"라며 흐름을 막았습니다.
사용자 입장에서는 황당한 경험이었습니다. "0원인데 왜 포인트 잔고가 필요한가?" 답할 수 없는 질문입니다. 시스템 입장에서는 정상 동작이었습니다. 모든 예약은 결제 흐름을 거치고, 결제 흐름은 잔고 검증을 포함하니까요.
문제는 그 가정 자체였습니다. "모든 예약은 결제를 거친다"는 가정을 0원 결제가 문제삼은 것입니다.
경계값이 시스템의 숨은 가정을 폭로합니다
기존 코드는 모든 예약에 대해 다음 흐름을 일률적으로 적용하고 있었습니다.
예약 요청 → 포인트 잔고 조회 → 검증 → 차감 → USE 로그 저장가격이 1,000원이든 10,000원이든 같은 흐름을 탔습니다. 0원도 예외 없이 같은 흐름이었습니다. 그래서 0원 케이스에서 다음 문제들이 누적되고 있었습니다.
잔고 0인 계좌에서 검증 실패: 포인트 계좌 없는 회원은 예약 자체가 막힘
무의미한 USE 로그: 거래액 0원짜리 로그가 계속 쌓임 (운영 통계 노이즈)
환불 흐름과의 불일치: 환불(REFUND)에는 "환불액 > 0" 가드가 있는데, USE에는 없음
이런 문제는 경계값이 시스템의 숨은 가정을 흔드는 전형적인 패턴입니다. 0원, null, 음수, 빈 배열, 한 글자짜리 입력 같은 것들이 정상 흐름의 가정을 드러냅니다.
이번 경우 폭로된 가정은 "모든 예약은 결제를 거친다"였습니다. 이 가정이 명시적으로 다시 정리되어야 했습니다.
4가지 처리 옵션과 그 트레이드오프
0원 케이스를 어떻게 처리할지 옵션 네 가지를 비교했습니다.
옵션 | 설명 | 평가 |
|---|---|---|
1. 정상 흐름 그대로 | 0원도 일반 결제와 동일하게 통과 | 현재 상태 — 잔고 에러 + 무의미 로그 누적 |
2. 잔고 검증만 스킵 | 0원이면 잔고 확인만 건너뛰고 차감·로그는 그대로 | 덜 안전 — 로그·환불 일관성 깨짐 |
3. 잔고·차감·로그 모두 스킵 | 0원이면 결제 부수효과 일체 발생 안 함 | 일관성 확보 |
4. 0원 전용 핸들러 분리 | 결제 라우터 자체를 분기 | 과도한 분리 — 코드 복잡도 증가 |
선택은 3번이었습니다. 이유는 두 가지였습니다.
첫째, 2번은 "검증은 스킵하지만 로그는 남긴다"는 어중간한 상태를 만듭니다. 로그 테이블에 거래액 0원짜리 행이 계속 쌓이고, 운영 통계에서 노이즈가 됩니다.
둘째, 4번처럼 별도 핸들러로 분리하면 같은 결제 로직이 두 군데에 존재합니다. 한 군데를 고치면 다른 곳도 같이 고쳐야 하는 이중 관리 부담이 생기고, 어느 하나를 빠뜨리면 동작이 달라집니다.
3번은 "결제 호출 자체는 통과시키되, 부수 효과는 발생시키지 않는다" 는 원칙으로 단순했습니다. 결제 라우터는 그대로 두고, 결제 함수 내부에 "금액 > 0일 때만 부수 효과 실행" 가드를 추가하는 방식입니다.

한 경로만 고치면 안 됩니다 — 다축 동시 정렬
여기서 중요한 깨달음이 있었습니다. 0원 가드를 한 군데만 적용하면 다른 곳에서 일관성이 깨집니다.
처음에는 USE 로그에만 "거래액 > 0" 가드를 넣으려고 했습니다. 그런데 점검해보니 같은 결의 가드를 적용해야 할 곳이 여러 군데였습니다.
영역 | 조치 |
|---|---|
USE 로그 | "거래액 > 0" 가드 신규 적용 |
REFUND (환불) | "환불액 > 0" 가드 기존에 있었음 — 확인만 |
다른 채널 예약 | 포인트 호출 자체 없음 — 영향 없음 |
기존 데이터 | 거래액 0인 USE 로그 6건 DELETE |
이렇게 여러 축을 동시에 정렬해야 시스템이 일관됩니다. USE에만 가드를 넣고 REFUND를 빠뜨리면, 차감과 환불의 동작이 어긋납니다. 기존에 쌓인 노이즈 데이터를 정리하지 않으면 운영 통계에서 계속 거짓 신호가 잡힙니다.
코드 변경과 데이터 정리는 한 묶음으로 처리해야 했습니다. 시스템 변경의 영향이 코드에만 머무는 게 아니라 이미 적재된 데이터에까지 미치기 때문입니다.

비기능 요구사항이 명시적이 되어야 합니다
이번 사건을 정리하면서 깨달은 또 하나의 포인트입니다. 가드를 추가하고 데이터를 정리한 결과로 시스템이 만족하게 된 새 요구사항은 다음과 같았습니다.
"포인트 계좌가 없는 회원도 무료 서비스를 사용할 수 있어야 한다."
이 요구사항은 처음 시스템을 설계할 때 누구도 명시적으로 적은 적이 없습니다. "결제 시스템은 잔고가 있을 때만 동작한다"는 가정이 암묵적으로 깔려 있었고, 그 가정이 깨질 가능성을 아무도 생각하지 않았습니다.
결과적으로 시스템이 모든 사용자에게 결제 흐름을 강제하면서, 결제할 게 없는 사용자를 부수적으로 막아버린 것입니다. 사용자가 불편함을 체감한 후에야 가정이 폭로됐습니다.
이런 비기능 요구사항은 운영 환경에서 사고가 나기 전까지 잘 보이지 않습니다. 그래서 경계값을 만났을 때 시스템이 무엇을 가정하고 있었는지 다시 점검하는 절차가 필요합니다. 가정을 명시적으로 적어두면 다음 변경 때 같은 사고가 재발하지 않습니다.
솔루션 도입 시 점검할 4가지
이 사건에서 나온 일반 원칙을 솔루션 도입 관점에서 정리하면 네 가지 체크리스트가 됩니다. SaaS, 결제 시스템, 예약 시스템, 이커머스 솔루션을 검토할 때 같은 기준이 적용됩니다.
1. 경계값을 처리하는 명시적 가드가 있는가
0원, null, 음수, 빈 배열 같은 경계값이 정상 흐름을 통과해도 부수 효과가 안전한지 확인합니다. "그냥 통과시키면 된다"는 답은 위험 신호입니다. 어떤 가정을 깨뜨리는지 모르고 있을 가능성이 높습니다.
2. 같은 결의 가드가 모든 채널에 일관되게 적용되는가
USE에 가드가 있는데 REFUND에 없거나, 한 채널에는 적용됐는데 다른 채널은 빠진 경우가 자주 보입니다. 솔루션이 채널별로 일관성 있게 가드를 적용하는지 확인합니다.
3. 코드 변경 시 기존 데이터 정리 절차가 있는가
가드를 새로 추가하면, 그 가드가 없었을 때 쌓인 노이즈 데이터를 어떻게 처리하는지가 핵심입니다. 마이그레이션 스크립트가 같이 제공되는지, 운영자가 수동으로 정리해야 하는지 확인합니다.
4. 비기능 요구사항이 명시적으로 적혀 있는가
"이 시스템은 어떤 사용자를 가정하는가?"라는 질문에 답할 수 있어야 합니다. 솔루션 명세서에 비기능 요구사항이 명시되어 있는지, 아니면 사고가 난 후에야 발견되는 구조인지 확인합니다.
같은 발상이 시스템 전반에 흐른다는 점이 중요합니다. 단일 경로에 모든 책임을 지우지 않는 것, 경계값에서 시스템 가정을 다시 점검하는 것, 변경의 영향을 다축으로 정렬하는 것. 솔루션 품질을 보는 시야입니다.
마무리
0원 결제는 단순히 "스킵해도 되는 케이스"가 아니었습니다. 잔고 검증·로그 저장·환불 가드·각 채널의 일관성을 같이 맞춰야 하는 사건이었습니다.
선택한 방식은 결제 호출 자체는 통과시키되 부수 효과(잔고 검증·차감·로그 저장)는 발생시키지 않는 것이었습니다. USE 로그에 "거래액 > 0" 가드를 신규 적용하고, 기존에 쌓인 거래액 0원 로그 6건을 정리했습니다. 환불 가드와 다른 채널은 이미 안전 상태인지 확인했습니다.
핵심 교훈은 세 가지입니다.
경계값은 시스템의 숨은 가정을 폭로한다 — 0원, null, 경계 입력에서 가정이 명시적으로 드러나야 합니다
한 경로만 고치면 안 된다 — 같은 결의 가드를 모든 축(USE/REFUND/다른 채널/기존 데이터)에 동시 적용해야 합니다
코드 변경과 데이터 정리는 한 묶음이다 — 새 가드가 들어가면 가드 없이 쌓인 노이즈도 같이 정리해야 합니다
결제·예약·포인트 시스템을 운영하거나 솔루션 도입을 검토 중이라면, 이번 글의 4가지 체크리스트(경계값 가드 / 채널 일관성 / 데이터 정리 절차 / 비기능 요구사항 명시)를 한 번 점검해보시길 추천합니다. 사용자 한 명이 시스템과 부딪힌 경험이 누적되기 전에 미리 막을 수 있습니다.