2025년 11월 12일
9

인앱 결제 정기 구독 구현 시 체크리스트

서비스 운영
KKingmo

Changmo Oh

@KKingmo

전체 글 보기

인앱 결제, 특히 '구독(Subscription)' 모델을 구현하다 보면 "이게 이렇게까지 복잡할 일인가?"라는 생각이 절로 든다. 단순히 결제 성공/실패만 나누면 되는 단건 결제와 달리, 구독은 '상태'와 '시간'의 싸움이기 때문이다.

기획서에는 없지만 개발자가 반드시 챙겨야 할, 인앱 구독 구현 시 체크리스트를 정리했다.


1. 기본 원칙: "클라이언트는 믿지 마라"

가장 먼저 박고 시작해야 할 대전제다. 앱(클라이언트)에서 "나 결제 성공했어!"라고 보내는 신호는 조작될 수 있다.

  • 영수증 검증(Receipt Validation)은 무조건 서버에서: 앱이 스토어에서 받은 영수증 데이터(receipt-data or purchaseToken)를 백엔드로 보내고, 백엔드가 직접 애플/구글 서버와 통신해서 유효성을 검증해야 한다.
  • 로직의 주도권은 서버에: "언제까지 프리미엄 기능이 유효한가?"에 대한 판단은 앱 내부 DB나 로컬 스토리지가 아닌, 서버가 내려주는 expiry_date를 기준으로 해야 한다.

2. '구독 해지'와 '환불'은 완전히 다른 세계다

사용자가 "나 그만 쓸래" 하는 상황도 기술적으로는 전혀 다르다.

  • 자동 갱신 끄기 (Cancel): 사용자가 스토어 설정에서 '구독 취소'를 누른 경우다. 이때는 즉시 권한을 뺏으면 안 된다. 이미 낸 돈만큼(이번 달 말일)까지는 서비스를 이용하게 해줘야 한다.
  • 환불 (Refund): 사용자가 고객센터를 통해 돈을 돌려받은 경우다. 이때는 웹훅(REFUND, REVOKE)을 받자마자 즉시 권한을 박탈해야 한다.
  • 구독 일시정지 (Pause - 구글): 구글은 구독을 잠시 멈추는 기능이 있다. 이 기간에는 서비스 접근을 막아야 하고, 재개되면 자동으로 다시 열어줘야 한다.

3. 돈이 안 들어왔는데 서비스는 줘야 한다? (결제 실패 시나리오)

결제가 실패했다고 바로 "너 나가!" 하면 안 된다. 여기가 가장 까다로운 부분이다.

  • 유예 기간 (Grace Period): 카드 잔액 부족 등으로 결제가 실패해도, 스토어 정책(보통 3~14일) 동안은 서비스를 계속 쓰게 해줘야 한다. 스토어가 재결제를 시도하는 기간이다. 이때 서비스를 끊으면 유저 이탈률이 폭증한다.
  • 계정 보류 (Account Hold): 유예 기간이 끝날 때까지 결제가 안 되면 진입하는 상태다. 이때는 서비스 권한을 차단해야 한다. 단, 구독이 완전히 취소된 건 아니므로 앱 실행 시 "결제 수단을 업데이트하세요"라는 안내를 띄워야 한다.
  • 복구 (Recovered): 보류 상태에서 유저가 카드를 변경해서 결제가 성공하면? 즉시 권한을 다시 부여해야 한다.

4. 업그레이드와 다운그레이드

사용자가 '월간' 쓰다가 '연간'으로 바꾸거나, 'Silver'에서 'Gold'로 바꿀 때의 처리다.

  • 업그레이드 (즉시 적용): 보통 즉시 변경된다. 남은 기간의 차액을 계산해서 갱신일이 밀리거나 추가 과금된다. 서버는 바뀐 productId를 즉시 반영해야 한다.
  • 다운그레이드 (예약): Gold에서 Silver로 내리면, 이번 달까지는 Gold 혜택을 유지하고 다음 갱신일부터 Silver로 바꿔야 한다. 즉, 서버는 '현재 등급'과 '다음 예약 등급'을 구분해서 관리해야 한다.

5. 계정 연동과 '구매 복원'

모바일 앱의 영원한 난제다. 애플 ID 하나로 아이폰 5대에서 로그인할 수 있다.

  • 구매 복원 로직: 앱을 지웠다 깔았거나 기기를 바꿨을 때, Restore Purchase 버튼을 누르면 과거 구매 내역을 찾아 권한을 줘야 한다.
  • 어뷰징 방지 (중요): A라는 계정(이메일)에 연결된 영수증(original_transaction_id)을, B라는 계정에서 복원하려고 하면 어떻게 할 것인가?
    • 정책 1: 기존 A의 연결을 끊고 B로 옮겨준다. (유저 편의성 위주)
    • 정책 2: "이미 다른 계정에 사용 중입니다"라며 막는다. (보안 위주)
    • 이 정책을 미리 정해두지 않으면 나중에 CS 폭탄 맞는다.

6. 서버 대 서버 알림 (Webhooks) 필수

클라이언트는 믿을 수 없고, 사용자가 앱을 켜지 않으면 갱신 여부를 알 수 없다. 그래서 스토어 서버가 우리 서버에 직접 쏴주는 알림(RTDN, App Store Server Notifications)이 필수다.

  • 모든 이벤트 수신: RENEWAL(갱신 성공), EXPIRATION(만료), CANCELLATION(해지), DID_FAIL_TO_RENEW(결제 실패) 등 핵심 이벤트를 다 처리해야 한다.
  • 멱등성(Idempotency) 보장: 스토어 서버가 실수로 같은 알림을 두 번 보내거나, 순서가 뒤바뀌어 올 수 있다. "이미 처리된 영수증이면 패스"하는 로직이 반드시 있어야 한다.
  • 폴백(Fallback) 크론잡: 웹훅도 가끔 누락된다. 하루에 한 번씩 만료 임박한 유저들의 영수증을 능동적으로 검증(Polling)하는 배치 작업(Cron job)을 돌려야 안전하다.

7. 테스트 환경 (Sandbox)의 함정

  • 시간 가속: 샌드박스에서는 1개월 구독이 5분 만에 갱신된다. 하루에 최대 6번까지만 갱신되고 그 뒤엔 강제 만료된다. 이 라이프사이클을 이해하고 테스트해야 "왜 갑자기 결제가 끊기지?" 하고 당황하지 않는다.
  • 테스트 계정 분리: 실제 결제가 일어나지 않는 샌드박스 테스터 계정을 별도로 파서 관리해야 한다.

8. 기타 챙겨야 할 것들

  • 가격 인상 동의: 구독료를 올리면 기존 유저에게 동의를 받아야 한다. 동의 안 하면 자동 해지된다. 이 상태값(price_consent_status)도 체크해야 한다.
  • 미성년자 결제 승인 대기 (Ask to Buy): 아이가 결제 요청하고 부모가 승인할 때까지 '대기(Pending)' 상태가 된다. 이때 미리 아이템을 주면 안 된다. 승인 완료(PURCHASED) 알림이 올 때 지급해야 한다.

요약

사용자가 앱을 켜지 않은 상태에서도 그들의 구독 상태(갱신, 실패, 환불 등)는 계속 변한다.
이 변화를 실시간으로(Webhooks), 그리고 주기적으로(Cron) 감지해서 DB를 최신 상태로 유지하는 것이 인앱 구독 구현의 핵심이다.