분할결제·BNPL 연동: 카드 할부·후불결제 시스템 설계

이커머스

BNPL분할결제결제 시스템할부핀테크

이 글은 누구를 위한 것인가

  • 고가 상품 구매 전환율을 높이기 위해 분할결제를 도입하려는 팀
  • BNPL 서비스(카카오페이 나중결제, 토스페이 등)를 연동해야 하는 개발자
  • 무이자 할부 프로모션 조건을 동적으로 관리하려는 개발팀

들어가며

100만원짜리 상품의 전환율과 10만원 × 10회 할부의 전환율은 다르다. 분할결제와 BNPL은 객단가를 높이면서 전환율도 유지하는 강력한 도구다.

이 글은 bluefoxdev.kr의 결제 시스템 설계 가이드 를 참고하여 작성했습니다.


1. 할부 유형 정리

[분할결제 유형]

1. 신용카드 할부
   - 2~36개월
   - 무이자: 카드사가 비용 부담 (or 가맹점 부담)
   - 이자: 사용자 부담 (연 10~20%)

2. BNPL (Buy Now Pay Later)
   - 30일 후 결제, 3개월 무이자 분할
   - 대표: 카카오페이 나중결제, 토스페이 나중에 결제
   - 특징: 신용카드 불필요, 즉시 승인

3. 선구매 후납 (가맹점 자체)
   - 계약금 + 잔금 구조
   - 예약 주문, 고가 가구, 맞춤 제작품

[무이자 할부 조건 예시]
  5만원 이상: 2~3개월 무이자
  10만원 이상: 2~6개월 무이자
  50만원 이상: 2~12개월 무이자
  특정 카드: 추가 혜택

2. 할부 조건 관리 시스템

from dataclasses import dataclass
from typing import Optional

@dataclass
class InstallmentPlan:
    months: int
    interest_free: bool
    min_amount: int
    max_amount: Optional[int]
    card_codes: list[str]  # 특정 카드만 (빈 리스트 = 전체)
    valid_from: str
    valid_until: str

# 동적 할부 조건 조회
INSTALLMENT_PLANS = [
    InstallmentPlan(2, True, 50000, None, [], "2026-01-01", "2026-12-31"),
    InstallmentPlan(3, True, 50000, None, [], "2026-01-01", "2026-12-31"),
    InstallmentPlan(6, True, 100000, None, ["BC", "KB", "SHINHAN"], "2026-04-01", "2026-06-30"),
    InstallmentPlan(12, True, 500000, None, ["HYUNDAI"], "2026-04-01", "2026-04-30"),
]

def get_available_installments(amount: int, card_code: str | None = None) -> list[dict]:
    """결제 금액과 카드 기준으로 가능한 할부 옵션 반환"""
    from datetime import date
    today = date.today().isoformat()
    
    available = []
    for plan in INSTALLMENT_PLANS:
        if amount < plan.min_amount:
            continue
        if plan.max_amount and amount > plan.max_amount:
            continue
        if plan.card_codes and card_code not in plan.card_codes:
            continue
        if not (plan.valid_from <= today <= plan.valid_until):
            continue
        
        monthly = amount // plan.months
        available.append({
            "months": plan.months,
            "interest_free": plan.interest_free,
            "monthly_amount": monthly,
            "total_amount": amount,
            "label": f"{plan.months}개월 {'무이자' if plan.interest_free else '할부'}",
        })
    
    return sorted(available, key=lambda x: x["months"])

3. BNPL 결제 상태 관리

from enum import Enum

class BNPLStatus(Enum):
    PENDING = "pending"          # 승인 대기
    APPROVED = "approved"        # 승인 완료, 배송 가능
    FIRST_PAYMENT_DUE = "due"   # 첫 납부일 도래
    PAID = "paid"                # 완납
    OVERDUE = "overdue"          # 연체
    CANCELLED = "cancelled"      # 취소

class BNPLPaymentSchedule:
    def __init__(self, order_id: str, total_amount: int, months: int, start_date: str):
        self.order_id = order_id
        self.total = total_amount
        self.monthly = total_amount // months
        self.schedules = self._generate_schedules(start_date, months)
    
    def _generate_schedules(self, start_date: str, months: int) -> list[dict]:
        from datetime import date, timedelta
        base = date.fromisoformat(start_date)
        
        return [
            {
                "installment_no": i + 1,
                "due_date": (base.replace(month=base.month + i) if base.month + i <= 12
                            else base.replace(year=base.year + 1, month=base.month + i - 12)).isoformat(),
                "amount": self.monthly,
                "status": "pending"
            }
            for i in range(months)
        ]

async def handle_bnpl_webhook(provider: str, event: dict):
    """BNPL 공급사 웹훅 처리"""
    order_id = event.get("merchant_order_id")
    
    if event["type"] == "payment.approved":
        await update_bnpl_status(order_id, BNPLStatus.APPROVED)
        await fulfill_order(order_id)
    
    elif event["type"] == "payment.overdue":
        await update_bnpl_status(order_id, BNPLStatus.OVERDUE)
        await notify_customer_overdue(order_id)
        await pause_account_benefits(order_id)  # 포인트 사용 제한 등
    
    elif event["type"] == "payment.cancelled":
        await update_bnpl_status(order_id, BNPLStatus.CANCELLED)
        await initiate_return(order_id)

마무리

BNPL과 분할결제는 단순한 결제 옵션 추가가 아니다. 고가 상품 카테고리의 구매 장벽을 낮추고, 신규 고객층(신용카드 없는 MZ 세대)을 유입하는 전략이다. 연체 및 취소 시 처리 프로세스를 반드시 사전에 설계해야 운영 리스크를 줄일 수 있다.