일기 slecs

카드 결제 PENDING 상태의 발생주의 정산 기준 정책화

목차

카드 결제 PENDING 상태를 발생주의 정산에 포함할 수 있는 조건을 내부 정책 문서에 추가했다.

배경: 카드 거래와 회계 원칙이 엇갈리는 지점

결제 플랫폼을 다루다 보면 회계 원칙과 실제 결제 상태가 묘하게 어긋나는 구간이 꼭 하나씩 있다. 발생주의는 "현금이 오고 가는 시점이 아니라 거래가 발생한 시점에 기록한다"는 원칙인데, 카드 결제는 이 원칙을 적용하기가 생각보다 까다롭다. 승인 요청을 보냈다고 해서 즉시 정산되는 게 아니라, 카드사에서 내부 처리가 끝날 때까지 몇 시간에서 길게는 며칠씩 PENDING 상태가 유지된다.

이 PENDING 구간이 문제다. 발생주의 관점에서 "거래가 발생했다"고 볼 수 있는 건 어느 시점인가 - 승인 요청을 보낸 시점인가, 카드사가 APPROVED를 돌려준 시점인가. 엄격하게 보면 APPROVED 이전까지는 확정이 아니니 기록하면 안 된다는 논리도 성립하고, 반대로 요청 자체가 확인된 이상 "발생한 거래"로 봐도 된다는 논리도 성립한다. 그 사이에서 팀마다, 개발자마다 다른 판단을 해왔고, 그게 조용히 쌓여 결국 정산 불일치를 만들어냈다.

이번 커밋은 그 모호함에 하나의 기준선을 그은 것이다.

결정: PENDING 포함은 조건부로 허용

정책의 핵심은 "발생주의 원칙을 따르면서도 PENDING 카드 거래를 기록할 수 있다"는 전제를 인정하되, 세 가지 조건을 모두 충족할 때만 허용한다는 거다.

  • 카드사로부터 요청 수신이 확인된 상태일 것 - 단순히 요청을 보낸 게 아니라 카드사 측에서 접수 확인을 준 경우
  • 단시간 내 APPROVED 또는 DECLINED로 확정될 가능성이 높을 것 - 무한정 PENDING으로 남는 케이스가 아닐 것
  • 요청 만료나 강제 취소 위험이 낮을 것

세 조건이 모두 맞으면, 회계 기준으로 "거의 확실한 거래"로 보고 PENDING 단계에서 기록을 허용한다. 반대로 하나라도 불확실하면 APPROVED 확인 이후로 기록을 미룬다.

코드로 치면 이런 흐름이다.

def should_record_as_accrual(payment: Payment) -> bool:
    if payment.status == PaymentStatus.APPROVED:
        return True

    if payment.status != PaymentStatus.PENDING:
        return False

    # PENDING 조건부 허용 체크
    is_acknowledged = payment.gateway_ack_at is not None
    is_short_lived = payment.expected_resolution_minutes <= 60
    low_expiry_risk = not payment.flags.has("HIGH_EXPIRY_RISK")

    return is_acknowledged and is_short_lived and low_expiry_risk

단순해 보여도 이 로직이 없으면 각 개발자가 status == PENDING이 뜰 때마다 즉흥 판단을 한다. 코드 리뷰 때 "왜 여기선 PENDING을 기록하고 저기선 안 기록하냐"는 질문이 나오는 이유가 거기서 비롯된다.

트레이드오프: PENDING을 미리 기록하면 뭘 잃나

이 정책이 완전한 해법은 아니다. PENDING 상태를 발생주의 기록에 포함하면 반드시 따라와야 하는 처리가 있다.

상황 필요한 후속 처리
PENDING → APPROVED 이미 기록된 항목을 확정 처리 (별도 작업 없음, 상태 업데이트만)
PENDING → DECLINED 기록된 항목을 취소 처리 (역분개 또는 상태 무효화)
PENDING 만료 정해진 TTL 이후 자동 취소 처리 필요

DECLINED나 만료 케이스를 처리하지 않으면 장부에 유령 거래가 남는다. 이게 쌓이면 정산 불일치의 원인이 되고, 감사 시 "왜 PENDING인 거래가 매출로 잡혀 있냐"는 질문을 받게 된다. 그래서 PENDING을 포함하기로 결정했다면, 역전 처리 로직도 반드시 같이 설계해야 한다.

현금주의처럼 APPROVED만 기록하는 방식은 이런 역전 케이스가 없다는 장점이 있지만, 발생주의가 주는 실시간 정산 근사값을 포기하게 된다. 어느 쪽이 맞냐기보다는, 시스템이 요구하는 정합성 수준과 운영 복잡도 사이의 선택 문제다.

왜 이걸 정책 문서로 만들었나

구두 합의는 기억에서 증발한다. 특히 결제/정산 관련 로직은 짧으면 수개월, 길면 수년 뒤에 비슷한 문제가 재발하는 경우가 많다. 그때 "우리가 이걸 왜 이렇게 짰지?"를 역추적하려면 커밋 메시지, 슬랙 아카이브, 심지어 퇴사한 사람한테 연락하는 지경까지 간다.

정책 문서를 먼저 쓰고 코드를 짜면 흐름이 달라진다. 코드 리뷰 때 "이 구현이 정책에 맞는가"를 체크리스트처럼 쓸 수 있고, 나중에 신규 입사자가 "PENDING 거래를 왜 기록해요?"라고 물어도 문서를 던져주면 된다. 정책이 바뀌면 문서를 바꾸고, 바뀐 문서를 기반으로 코드 수정 범위를 파악하는 흐름도 가능하다.

팀이 커질수록 이 구조가 더 유효하다. 개발자 한 명이 혼자 다 관리할 때는 머릿속에 들어있어도 돌아가지만, 여러 명이 같은 도메인을 건드리기 시작하면 반드시 해석 차이가 생긴다. 그 차이를 좁히는 가장 저비용 방법이 정책 문서화다.

결제/정산 로직은 특히 그렇다. 금액이 걸리면 "그냥 됐으니까 됐겠지" 식의 암묵적 합의가 가장 위험하다.

댓글 0

첫 댓글 달아줘.