티스토리 뷰
들어가는 글
지난번에 구독 시스템 설계기에 대해 글을 작성했었다.
그렇게 설계한 기능에 대해 현재는 테스트를 마치고 라이브 환경으로 전환하여 처리하게 되었다.
본 포스트에서는 구현 중 발생한 문제 및 해결과정을 간단하게 나열하고(경험 정리 목적) 돌아보도록 하겠다.
상황
기존에 B2B 계약 기반으로 운영되던 서비스를 B2C 구독 모델로 전환하면서 결제 기능을 새롭게 설계, 구현해야 했다.
이때 단순 결제 처리 외에 수반되는 작업들이 많았는데 나열하자면 다음과 같다.
- 요금제 → 허용된 서비스 내 하위 기능들과 생성할 수 있는 자원에 대한 Quota 정의
- 구독
- 비즈니스 로직들에 기능 제한 처리 및 Quota 처리
- 결제 수단 등록
- 정기 결제
- 요금제 업그레이드
- 결제 취소 및 부분 환불
- 구독 취소 및 만료 처리
- 청구서/영수증 생성
- PG 연동 및 카드사 심사 대응
이렇게 실서비스에서 실제로 돈이 도는 구조를 실제로 다루는 상황이었음
문제 및 해결 과정
1) 기능 제한 및 Quota 처리 (전역 적용 문제)
문제
- 기존 서비스에서 “자원 생성/사용” 로직이 여러 곳에 흩어져 있어, 요금제 기반 기능 제한과 Quota를 일관되게 적용하기 어려웠다.
- 특정 엔드포인트만 막으면 우회 경로가 생기고, 추후 기능이 늘어날수록 누락 위험이 커졌다.
해결
- 자원 생성/변경에 해당하는 동작을 Command 단위로 정리하고, 그 앞단에서 일원화된 검증(권한/요금제/Quota)을 통과해야만 실제 쓰기 로직이 실행되도록 정리했다.
- 결과적으로 “요금제 정책 변경/추가”가 생겨도 검증 로직 한 곳에서 통제할 수 있게 만들었다.
2) 경합/중복 실행 및 상태 변경 정합성
문제
- 결제/업그레이드/취소/부분환불은 “돈”이 걸려 있고, 같은 요청이 중복 실행되거나 레이스가 발생하면 아래와 같은 문제 발생 가능
- 중복 결제
- 결제는 됐는데 구독 상태가 갱신되지 않음
- 구독 상태는 바뀌었는데 결제가 실패
- 또한 결제는 외부 PG API 호출이 필수인데, DB 트랜잭션 안에서 외부 호출을 묶을 수 없어서 트랜잭션 경계 설계가 어려웠다.
해결
- “판단”과 “클레임(처리 권한 확보)”을 분리했다.
- 결제 대상/가능 여부를 판단
- 실제 처리 직전에 CAS 업데이트로 IN_PROGRESS 클레임 (try_mark_subscription_as_payment_started)
- 이후 단계에서 select_for_update로 클레임된 구독만 재확인 후 상태 전이
- 결제 대상/가능 여부를 판단
- 외부 PG API 호출은 트랜잭션 밖에서 수행하고, 호출 전후로 DB 상태가 꼬이지 않도록 재진입 가능한 흐름으로 구성했다.
- 트랜잭션에서는 “결제 시작/대상 확정/청구서(invoice) 생성” 같은 내부 원자 작업만 수행
- 외부 호출 결과에 따라 성공/실패를 후속 트랜잭션에서 확정
- 멱등성을 한 가지 키에 몰지 않고, 레이어를 나눴다.
- 업무 멱등성: invoice_key(정기결제/업그레이드별 고유 키)로 동일 청구의 중복 생성을 방지
- 실행 멱등성/클레임: 결제 시작 상태(IN_PROGRESS) + 요청 식별자(request_id 등)로 중복 실행을 방지
- 운영 관측 가능성(정합성 확인)을 위해 다음을 추가적으로 처리함
- BillingAuditLog 등 감사 로그
- 결제 성공/실패/이탈 시 알림(이메일)
- 문서 생성/발송은 transaction.on_commit으로 분리
3) 정기 결제/재시도/이탈 처리 스케줄링 (대상 선정과 작업 분리)
문제
- 정기 결제는 “매일 특정 시간”에 다수 구독을 대상으로 실행되며, 아래의 조건을 잘 지키지 못할 시 중복 청구/누락이 발생한다.
- 오늘 결제일 조건(말일 보정 포함)
- 이미 당일 업그레이드(adhoc)가 진행 중인 구독 제외
- 무료 플랜 제외
- 구독 해지 effective date 고려
- 실패 구독은 즉시 이탈시키면 안 되고, 일정 기간 재시도 후 이탈 처리해야 운영 정책에 맞는다.
해결
- 후보 조회를 별도 유틸로 분리해 조건을 명확히 만들었다.
- 말일에는 payment_day가 말일 초과인 구독까지 포함하는 보정
- adhoc 결제가 당일 존재하면 정기결제 대상에서 제외(중복 청구 방지)
- “정기 결제 → 재시도 → 이탈 처리”처럼 목적이 다른 작업을 스케줄 태스크로 분리했다.
- 실패 누적에 대한 정책도 최대한 코드로 관리했다.
- first_failed_at, retry_count로 추적
- 임계치(CHURNED_DAYS_THRESHOLD)를 넘기면 고객사 이탈 처리 + 알림
- 임계치 전에는 재시도 대상에 포함
4) 실패/예외 케이스 대응 (설계 + QA)
문제
- 실패 케이스가 다양하다: 카드 미등록/만료, 잔액 부족, 네트워크 타임아웃, 결제일 31일/윤년, 구독 취소/철회 반복 등.
- 실패 케이스에서 정합성이 깨지면 운영 사고 발생
해결
- QA 진입하기 전 최대한 설계를 꼼꼼하게 잡았다.
- 외부 호출 timeout을 설정하고 네트워크 예외를 결제 실패로 표준화
- 실패 시에도 invoice/billing_history/subscription 상태를 기준으로 재시도/추적 가능한 상태를 남김
- 문서 생성/발송은 커밋 이후 처리하여 “결제 트랜잭션”과 분리
- 이후 QA를 강하게 돌려 엣지 케이스(말일/윤년/결제일 보정/취소 타이밍)를 최대한 사전에 제거했다.
참고) PG사 연동
실제 사용자의 카드로부터 돈을 꺼내기 위해서는 PG사 계약 및 카드사 심사를 진행해야 한다.
여기서 PG와 카드사가 어떤 역할을 하는지는 다음의 그림을 첨부한다.

간단히 요약하자면, PG사로부터 결제 서비스를 사용하게 되는 것인데, 이를 위해서는 아래와 같은 심사과정을 거쳐야 한다.

우리는 PG사로 토스페이먼츠를 사용하게 되었다. 그쪽을 선택한 이유는 다음과 같다.
- 다른 PG사에 비해 비교적 개발 문서가 잘 되어 있음 -> 특히 프론트 측 연동하기 매우 쉬운 구조
- 테스트 환경과 라이브 환경을 분리하여 테스트하기 쉬움 -> 실 결제와 거의 비슷한 환경에서 금액 신경안쓰고 테스트 가능
- PG 계약, 카드사 심사 과정에서 피드백이 빠르고 확인하기 쉬움 -> 메일 보내면 빠르게 피드백이 온다
크게 어려운 부분은 없었고 그쪽에서의 가이드를 따라서 계약 및 심사 신청을 진행하면 된다.
주로 그에 맞춰 작업이 필요한 부분은 다음과 같았다.
- 홈페이지 내 사업자 정보 명확하게 기재
- 판매되는 상품의 가격을 명시하고 특정 금액을 넘지 않는지
- 심사를 위한 다양한 서류 작성 및 제출
- 심사 측에서 테스트할 수 있도록 비회원 결제 기능 처리 혹은 테스트 계정 처리
이에 맞춰서 수정하면 되는데, 카드사 심사가 좀 오래걸릴 수 있다. (우리는 7일 정도 걸렸다.)
느낀 점
개발자로서 이러한 기능이 좋은 경험인지 느낀 점들은 다음과 같다.
좋은 점
1. 정합성/동시성을 신경쓰게 된다.
앞서 문제 해결 과정에 적은 대로, 돈이 오가는 것이다 보니 최대한 안정적인 동작과 상태 변경을 보장해야 한다.
이를 위해서 설계, 개발 과정에서 훨씬 안정적인 코드를 작성하는 경험을 할 수 있다.
2. 꼼꼼한 테스트 및 QA를 하는 습관을 갖게 된다.
1번과 마찬가지로 엣지 케이스들이 꽤 존재하고, 외부에 의존하는 부분들도 꽤 많다.
여기서 우리가 제어할 수 있는 것들을 최대한 제어하고 실패 처리를 깔끔하게 하는 습관이 잡히게 된다.
이를 위해서 테스트 코드를 빡세게 작성하고 QA 를 꼼꼼하게 돌리는 식으로 최대한 대응하는 습관이 생길 수 있다.
3. 많은 곳에서 요구되는데, 실제로 경험할 기회가 많진 않다.
솔직히 결제를 사용하지 않는 서비스는 거의 없다. 이미 사용 중이거나, 앞으로 사용할 계획이 있는 경우가 대부분이다.
그럼에도 불구하고 신입 개발자가 이러한 결제 기능을 직접 개발하거나 운영까지 경험해볼 수 있는 기회는 생각보다 많지 않다.
대부분의 서비스에서는 결제 기능이 이미 연동되어 있고, 안정적으로 운영되고 있을 것이다.
따라서, 우리 같은 신입 개발자가 구조를 바꾸거나 핵심 로직을 다룰 일이 거의 없다.
이런 이유로 결제 기능은 많이 요구되는 영역임에도 불구하고, 실제 경험을 통해 학습하기는 어려운 영역이라고 느꼈다.
따라서 만약 결제 기능을 직접 설계하고 구현하며 운영까지 책임지는 경험을 할 수 있다면, 신입 개발자에게는 기술적인 성장 폭이 큰 좋은 경험이 될 수 있다고 생각한다.
단점
1. 외부랑 메일을 주고 받을 일이 많다.
결제는 PG사, 카드사, 내부 기획/운영 등 외부 및 타 부서와의 커뮤니케이션이 잦은 편이다.
기술적인 문제 외에도 행정적인 대응에 시간이 소요될 수 있다.
2.개발 및 검증에 시간이 오래 걸릴 수 있다.
계속 언급한 대로, 정합성과 안정성이 필요하므로 다른 기능들에 비해 시간이 많이 걸릴 수 있다.
따라서, 작업을 완료하는 대 걸리는 시간을 산정할 때, 내가 생각한 것보다 길게 잡는 편이 좋다.
실제로 그렇게 걸리기 때문이다. 물론 결제측을 경험하신 분이 있다면 빠르겠지만, 나는 처음이라 오래 걸렸다.
3. 충분히 테스트하고 대비되지 않는다면 많이 고통스러울 수 있다.
일단 라이브 환경으로 가고 나면, 돈 문제가 발생할 수 있다.
물론 부분 취소나 환불 기능을 PG사에서 제공해주긴 하지만, CS에서 정말 힘들 수 있다.
따라서, 다른 기능들에 비해 많이 준비하고 운영으로 넘어가는 편이 좋다.
4. 기획이 잘 잡혀 있어야 한다.
요금제, 결제 주기, 환불 정책, 이탈 기준 등 기획이 명확하지 않으면 개발 과정에서 잦은 수정이 발생할 수 있다.
물론 나는 기획이 비교적 잘 잡혀 있었고, 엣지 케이스에 대한 수정도 빠르게 주고받아서 큰 어려움은 없었다.
정리
회사에서 일하다 보면, 당장 매출과 직접적으로 연결되지 않는 작업들을 하는 경우도 많다.
버그를 수정하거나 리팩토링을 하는 일들 역시 서비스 운영에 중요한 역할을 한다.
하지만, 실제로 돈이 들어오는 지점과는 거리가 있다고 느껴질 때도 있다.
이번 개발은 그런 작업들과 다르게, 실제 매출과 바로 연결되는 기능을 직접 설계하고 운영해볼 수 있었던 경험이었다.
또한 결제 기능은 대부분의 서비스에서 필요하지만, 신입 개발자가 구조를 설계하고 운영까지 책임질 기회는 흔하지 않다.
그래서 개인적으로는, 만약 결제 기능을 직접 다뤄볼 기회가 있다면 부담이 되더라도 한 번쯤은 꼭 경험해볼 만한 영역 같다.
'연습' 카테고리의 다른 글
| Playwright를 운영 환경에서 돌리면 생기는 일들 (0) | 2026.04.11 |
|---|---|
| RTB: 수집 파이프라인 구조 개선안 (0) | 2026.03.08 |
| 구독 시스템 설계기 (0) | 2025.12.21 |
| GIL with dict (3) | 2025.12.09 |
| IDLE한 상태에서 배운 점 (1) | 2025.11.26 |
- Total
- Today
- Yesterday
- PREFECT
- kafka쓰고싶어요
- Remote
- 파이썬
- django testcase
- cipher suite
- 위상정렬
- 우선순위큐
- 최대한 간략화하기
- SSL
- Javascript
- 코딩테스트
- Til
- requests
- 백준
- BOJ
- vscode
- 스택
- jwt
- 힙
- 회고
- 이것도모르면바보
- SQL
- endl을절대쓰지마
- 프로그래머스
- 삽질
- docker-compose update
- 그리디
- Python
- 불필요한 값 무시하기
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |