정기 구독 서비스나 월간 자동 결제 기능을 구현하다가 카드 정보를 어떻게 안전하게 다뤄야 할지 난감했던 경험이 있을까?
다만 전자금융거래법과 보안 표준(PCI-DSS) 때문에 서버에 고객의 카드 번호나 CVC를 직접 저장할 수 없어, 원리를 정확히 모르면 개발 막바지에 막히는 경우가 많다.
이번에는 토스페이먼츠(Toss Payments) 빌링 API가 정확히 뭔지, 왜 빌링키(Billing Key)가 필요한지, 그리고 클라이언트 연동부터 PHP 서버 승인까지 완벽하게 정리해서 소개하겠다.
구독형 서비스에서 매달 사용자의 결제를 자동으로 처리하기 위해 필요한 것이 바로 '빌링키(Billing Key)'다. 빌링키는 고객의 카드 정보를 암호화하여 토스페이먼츠 서버에 저장한 뒤, 이를 안전하게 대체하기 위해 발급받는 일종의 '결제용 토큰'이다.
다만 대부분의 개발자들은 카드 번호를 직접 DB에 저장하려는 위험한 시도를 하거나, 1회성 결제 키(paymentKey)와 빌링키의 차이를 구분하지 못해 삽질을 겪곤 한다. 아래 표로 두 방식의 차이를 한눈에 정리해보자.
| 구분 | 1회성 일반 결제 | 빌링 결제 (정기/자동 결제) |
|---|---|---|
| 카드정보 입력 | 결제 시마다 매번 입력 및 승인창 호출 | 최초 1회 카드 등록 시에만 인증 입력 |
| 결제 시점 및 주체 | 고객이 화면에서 직접 결제 버튼 클릭 | 서버가 배치(Batch) 작업으로 원하는 주기에 실행 |
| 식별 키 종류 | paymentKey (결제 건당 1회용) | billingKey (재사용 가능한 결제 토큰) |
| 보안성 | 서버에 카드 정보 저장 불가 | 빌링키만 저장하므로 카드 정보 유출 위험 없음 |
빌링 결제 시스템 구축 프로세스는 크게 클라이언트 인증과 서버 간 승인 통신이라는 3단계로 나뉜다.
- 1단계 (클라이언트): 사용자가 토스페이먼츠 SDK를 통해 카드 등록 인증을 진행하고 `authKey`와 `customerKey`를 전달받는다.
- 2단계 (서버): 성공 리다이렉트 URL로 넘어온 `authKey`를 사용해 토스페이먼츠 API로 진짜 `billingKey`를 발급받아 DB에 저장한다.
- 3단계 (서버/배치): 정기 결제 날짜가 되면 DB에 저장된 `billingKey`와 `customerKey`를 이용해 서버 대 서버 요청으로 결제를 승인한다.
웹 브라우저에서 토스페이먼츠 자바스크립트 SDK를 로드한 뒤 `requestBillingAuth` 메서드를 호출하는 코드다.
<script src="https://js.tosspayments.com/v1/payment"></script>
<script>
const tossPayments = TossPayments('test_ck_docs_O2L5955Pz2L55D3md24A3189'); // 클라이언트 키
function registerCard() {
tossPayments.requestBillingAuth('카드', {
customerKey: 'USER_UNIQUE_ID_1234', // 고객 고유 식별키
successUrl: 'https://your-domain.com/billing-success.php',
failUrl: 'https://your-domain.com/billing-fail.php'
})
.catch(function (error) {
console.error('인증 요청 실패:', error.message);
});
}
</script>
<button onclick="registerCard()">카드 등록하기</button>
인증 성공 시 `successUrl`로 전달되는 `authKey`와 `customerKey`를 받아 토스페이먼츠 API서버에 `billingKey` 발급 요청을 보낸다.
<?php
// billing-success.php
$authKey = $_GET['authKey'] ?? '';
$customerKey = $_GET['customerKey'] ?? '';
$secretKey = 'test_sk_docs_O2L5955Pz2L55D3md24A3189'; // 서버용 시크릿 키
if (empty($authKey) || empty($customerKey)) {
exit('잘못된 접근입니다.');
}
// Basic Auth 헤더 생성 (Secret Key 뒤에 콜론을 붙이고 Base64 인코딩)
$credential = base64_encode($secretKey . ':');
$ch = curl_init('https://api.tosspayments.com/v1/billing/authorizations/issue');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Basic ' . $credential,
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'authKey' => $authKey,
'customerKey' => $customerKey
]));
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$result = json_decode($response, true);
if ($httpCode === 200) {
$billingKey = $result['billingKey'];
// TODO: DB의 해당 회원 정보에 $billingKey와 $customerKey를 저장 처리
echo "빌링키 발급 성공: " . htmlspecialchars($billingKey);
} else {
echo "빌링키 발급 실패: " . htmlspecialchars($result['message'] ?? '에러 발생');
}
?>
배치 프로그램이나 CRON 등을 통해 정기 결제일에 회원 DB에 저장해 둔 `billingKey`로 실제 결제를 실행하는 코드다.
<?php
// execute-pay.php (정기 결제 실행)
$billingKey = 'saved_billing_key_here'; // DB에서 조회한 빌링키
$customerKey = 'USER_UNIQUE_ID_1234';
$secretKey = 'test_sk_docs_O2L5955Pz2L55D3md24A3189';
$credential = base64_encode($secretKey . ':');
$orderId = 'ORDER_' . date('YmdHis') . '_' . uniqid();
$ch = curl_init('https://api.tosspayments.com/v1/billing/' . $billingKey);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Basic ' . $credential,
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'customerKey' => $customerKey,
'amount' => 9900,
'orderId' => $orderId,
'orderName' => '프리미엄 멤버십 월간 정기결제'
]));
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$result = json_decode($response, true);
if ($httpCode === 200) {
// 결제 성공 DB 업데이트 (결제 상태 완료, 다음 결제일 갱신 등)
echo "결제 성공! PaymentKey: " . htmlspecialchars($result['paymentKey']);
} else {
// 결제 실패 처리 (잔액 부족, 카드 한도 초과 등 예외 처리)
echo "결제 실패: " . htmlspecialchars($result['message']);
}
?>
시크릿 키(Secret Key) 인증 관리와 결제 요청 시 예외 처리는 보안 및 시스템 안정성의 핵심이다.
✗ 잘못된 코드 (프론트엔드에 Secret Key 노출):
// 프론트엔드 JS 스크립트에 시크릿 키를 직접 적어 결제를 승인하는 위험천만한 방식
fetch('https://api.tosspayments.com/v1/billing/issue', {
headers: {
'Authorization': 'Basic ' + btoa('test_sk_docs_SecretKeyHere:') // 보안 사고 발생 원인!
}
});✓ 올바른 코드 (서버단 통신 및 인증키 분리):
// 프론트엔드에는 Client Key만 사용하고, API 호출은 반드시 인증된 PHP 백엔드 서버에서만 수행
$secretKey = getenv('TOSS_SECRET_KEY'); // 환경변수에서 보안키 로드
$credential = base64_encode($secretKey . ':');결과/출력값:
시크릿 키 유출 위험을 방지하고, 클라이언트가 결제 금액이나 요청 파라미터를 임의로 위변조하는 악의적인 공격을 철저히 차단할 수 있다.
토스페이먼츠 빌링 API는 구독형 서비스를 안전하고 효율적으로 운영하기 위한 핵심 도구다. 클라이언트 키와 시크릿 키를 엄격히 분리하고 예외 처리를 꼼꼼하게 다루는 작은 습관이 모여 안전한 결제 시스템을 만든다는 점을 잊지 말자. 이 글의 연동 코드를 참고해 서버 단 빌링키 발급 및 자동 결제 프로세스를 구축하면, 손쉽게 구독 결제 시스템을 완성할 수 있을 것이다.