1P by GN⁺ | ★ favorite | 댓글 1개
  • exe는 제품 로직 곳곳에서 결제 API를 직접 호출하는 대신, 상태 변화를 청구 가능 사실(billable facts) 로 기록하고 확정된 상태를 Stripe와 조정함
  • 기존 구조에서는 데이터베이스 트랜잭션과 결제 API 호출이 얽혀 부분 실패, 비정상 구독 상태, 결제 거절 같은 예외가 제품 흐름까지 흔들었음
  • 팀 좌석이 추가되면 상태를 dirty로 표시하고, 후속 워커가 비즈니스 규칙에 따라 수량을 계산해 변경된 경우에만 Stripe 구독 수량을 갱신함
  • 결제를 분리하면 신규 팀원 온보딩이 결제 코드에 의존하지 않고, 좌석 계산 규칙을 바꿔도 초대와 가입 흐름에 영향을 주지 않음
  • 같은 조정 구조를 활성 VM과 디스크 사용량 같은 종량제 청구와 iOS 인앱 구매에도 적용해, 제품 이벤트는 유지하면서 결제 제공자별 연동만 바꿀 수 있음

제품 흐름에서 결제 로직 분리하기

  • 결제 로직이 일반 비즈니스 로직과 뒤섞이면 청구가 필요한 모든 핵심 경로에 관련 코드가 퍼지고, 가격 구조도 취약해져 변경하기 어려움
  • exe는 한 사람이 결제 지식을 독점하지 않고 누구나 관련 코드를 변경할 수 있도록 하되, 복잡한 예외는 전문 담당자가 처리하는 방식을 지향함
  • 초기 팀 좌석 청구는 초대 수락부터 결제까지 하나의 큰 흐름으로 묶여 있었음
    • 사용자가 초대를 수락하고 계정을 확인한 뒤 팀에 가입함
    • 공유 VM 접근 권한과 요금제에 따른 컴퓨팅 자원을 할당받음
    • 이 과정에서 결제 API까지 호출함
  • 이렇게 데이터베이스 변경과 외부 API 호출을 결합하면 한쪽만 성공하는 부분 실패가 발생할 수 있음
    • 팀 구독 상태가 비정상적일 수 있음
    • 추가 좌석에 대한 결제가 거절될 수 있음
    • 예외가 쌓일수록 전체 구조가 취약해짐

청구 가능 사실과 사후 조정

  • 청구 가능 사실은 특정 상태가 바뀌었음을 나타내는 원자적 작업임
    • 제품 로직을 먼저 실행해 자원의 새 상태를 확정함
    • 이후 확정된 사실을 바탕으로 결제 제공자의 상태를 조정함
    • Stripe에는 그 상태에 도달한 과정이 아니라 최종 수량만 필요함
  • 팀 좌석 조정

    • 초대가 수락되면 팀 좌석 상태를 dirty로 표시함
    • 후속 워커가 dirty 상태를 감지하고 비즈니스 규칙에 따라 좌석 증감분을 계산함
    • 수량이 달라진 경우에만 Stripe의 구독 수량을 갱신함
    • 팀원 추가와 결제 코드가 분리되므로 초대 흐름을 다시 작성해도 청구가 함께 깨지지 않으며, 좌석 계산 방식도 독립적으로 바꿀 수 있음
  • 종량제 청구와 인앱 구매

    • 같은 조정 과정은 모든 종량제 청구에 적용됨
      • 시스템이 활성 VM과 디스크 사용량에 관한 사실을 기록함
      • 계량 워커가 이를 결제 제공자의 상태와 조정함
      • 새로운 청구 방식을 추가해도 사실은 그대로 유지되고, 각 API와 조정하는 방법만 달라짐
    • iOS 앱 역시 누군가 인앱 구매로 구독했다는 사실만 전달하고, 실제 결제 상태는 이후 조정함
    • 결제 구조가 다른 구성원도 다룰 수 있는 영역이 되었으며, 초대 흐름 같은 제품 기능 변경이 청구 시스템을 손상시킬 가능성도 낮아짐

댓글과 토론

Lobste.rs 의견들
  • 글의 핵심인 변경분을 비동기로 감지해 처리하는 방식은 결합도를 낮추고 부수 효과를 구현하기에 좋음
    하지만 LLM은 코드가 곳곳으로 퍼지는 현상을 막기보다 가속하는 도구라서 아키텍처상 부채가 되기 쉬움. 이해하는 속도보다 빠르게 생성되는 코드를 어떻게 검토할 수 있는지도 의문인데, Exe가 코드 검토조차 하지 않는다는 대목은 더욱 두려움

    • 원래도 진지한 적이 별로 없던 기술 업계가 지금은 유난히 비진지한 시기를 지나고 있음을 기억해야 함
    • 청구 시스템을 다루는 입장에서 꽤 무서운 접근임. 청구 플랫폼은 여러 경계가 명확한 컨텍스트로 구성되는데, LLM은 도메인 경계를 잘 지키지 못하므로 큰 고통을 부를 가능성이 큼
    • “LLM을 써도”가 아니라 “특히 LLM을 쓰면” 코드가 더 퍼지는 것 아닌가 싶었음
    • 그 인용문은 Orwell이 아니라 Upton Sinclair의 말로 알고 있음
  • 이 아키텍처가 흥미롭기는 하지만, 글 앞부분에서 제시한 문제를 어떻게 해결하는지는 불분명함. 결제가 거절되면 모든 리소스의 결제를 보장하기보다 미결제 리소스를 먼저 제공하게 될 것으로 보임
    청구 작업자가 declined 사실을 발행해 리소스를 회수할 수도 있지만, 깔끔한 단방향 흐름과 비교하면 순환 구조가 됨. Exe처럼 월별로 청구하는 컴퓨팅 서비스에는 잘 맞겠지만, 물리 장비를 배송하거나 다른 서비스의 좌석을 재판매하는 사업에서는 받아들이기 어려운 절충안임

    • 제품이 추상적이고 대체 가능한 소프트웨어 중심 청구 방식임. 사용량 기반 청구는 과금 이벤트가 발생했다는 분석 데이터를 토대로 하고, 청구 시스템이 이를 집계해 일관된 청구 내역을 만드는 편이 좋음. 과금 이벤트는 여러 곳에서 생길 수 있으므로 제품 코드 밖으로 분리하는 것도 건전함
      다만 API 호출이나 데이터베이스 트랜잭션 실패, 비정상적인 구독 상태, 좌석 결제 거절을 어떻게 처리하는지는 직접 답하지 않음. 리팩터링으로 분석 수집이 깨지거나, 특정 행을 더 이상 변경 상태로 표시하지 않거나, 새 경로에서 변경 표시를 빠뜨리는 문제도 그대로 남음
      좌석·무료 할당량 제한, 최대 지출액 설정, 선불 잔액 차감처럼 제품 내부에 존재하는 청구 관련 상태도 해결하지 못함. 제품 서비스가 이벤트를 발행하고 청구 서비스가 준수 상태를 다시 제품에 반영할 수 있지만, 그러면 상태를 가진 분산 시스템에 두 행위자가 생김
  • Stripe가 숫자 하나만 원하는 것처럼 보이지만, 일정 규모에서는 결제의 세부 항목까지 제공하면 교환 수수료를 절감하고 승인율을 높일 수 있음

  • Exe가 코드 검토를 하지 않는 방식은 신선함. 그렇다면 릴리스와 테스트는 어떤 방식으로 운영하는지 궁금함