FitPass의 결제 시스템은 토스페이먼츠와 연동하여 안전하고 신뢰할 수 있는 결제 서비스를 제공합니다. 포인트 충전을 위한 결제 처리와 결제 실패/취소에 대한 완벽한 처리 로직을 포함합니다.
- 토스페이먼츠 연동: 국내 대표 PG사와의 안전한 결제
- 실시간 결제: 즉시 결제 승인 및 포인트 충전
- 완벽한 예외처리: 결제 실패/취소 시나리오 완벽 대응
- 결제 이력 관리: 모든 결제 내역 추적 및 관리
- 자동 환불: 결제 취소 시 포인트 자동 차감
상태 |
설명 |
발생 시점 |
다음 상태 |
PENDING |
결제 대기 |
결제 요청 생성 시 |
CONFIRMED, FAILED |
CONFIRMED |
결제 완료 |
토스 결제 승인 시 |
CANCELLED |
FAILED |
결제 실패 |
결제 승인 실패 시 |
- |
CANCELLED |
결제 취소 |
관리자/사용자 취소 시 |
- |


@Transactional
public PaymentUrlResponseDto preparePayment(Long userId, Integer amount, String orderName) {
User user = userRepository.findByIdOrElseThrow(userId);
// 고유한 주문 ID 생성 (UUID + timestamp)
String orderId = generateOrderId();
// 결제 정보 저장 (PENDING 상태)
Payment payment = Payment.builder()
.orderId(orderId)
.orderName(orderName)
.amount(amount)
.status(PaymentStatus.PENDING)
.user(user)
.build();
paymentRepository.save(payment);
return new PaymentUrlResponseDto(
orderId, amount, orderName, user.getEmail(), user.getName(),
tossPaymentConfig.getSuccessUrl(), tossPaymentConfig.getFailUrl()
);
}

@Transactional
public PaymentResponseDto confirmPayment(String paymentKey, String orderId, Integer amount) {
// 1. 주문 정보 조회
Payment payment = paymentRepository.findByOrderIdOrElseThrow(orderId);
// 2. 결제 금액 검증
if (!payment.getAmount().equals(amount)) {
throw new BaseException(ExceptionCode.PAYMENT_AMOUNT_MISMATCH);
}
try {
// 3. 토스페이먼츠 결제 승인 요청
PaymentResponseDto tossResponse = tossPaymentClient.confirmPayment(
paymentKey, orderId, amount);
// 4. 결제 정보 업데이트
payment.updatePaymentKey(paymentKey);
payment.updateStatus(PaymentStatus.CONFIRMED);
payment.updateMethod(tossResponse.method());
// 5. 포인트 충전
pointService.chargePoint(
payment.getUser().getId(),
payment.getAmount(),
"토스페이먼츠 충전 - " + payment.getOrderName()
);
return tossResponse;
} catch (Exception e) {
payment.updateStatus(PaymentStatus.FAILED);
payment.updateFailureReason(e.getMessage());
throw new BaseException(ExceptionCode.TOSS_PAYMENT_CONFIRM_FAILED);
}
}
@Component
public class TossPaymentClient {
// 결제 승인
public PaymentResponseDto confirmPayment(String paymentKey, String orderId, Integer amount);
// 결제 조회
public PaymentResponseDto getPayment(String paymentKey);
// 결제 취소
public PaymentResponseDto cancelPayment(String paymentKey, String cancelReason);
}
private String encodeSecretKey(String secretKey) {
return Base64.getEncoder()
.encodeToString((secretKey + ":").getBytes(StandardCharsets.UTF_8));
}
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.AUTHORIZATION, "Basic " + auth);
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
기능 |
HTTP Method |
URL |
설명 |
결제 승인 |
POST |
/v1/payments/confirm |
결제 승인 처리 |
결제 조회 |
GET |
/v1/payments/{paymentKey} |
결제 상태 조회 |
결제 취소 |
POST |
/v1/payments/{paymentKey}/cancel |
결제 취소 처리 |
- 네트워크 오류: 토스페이먼츠 API 통신 실패
- 결제 거부: 카드사/은행 결제 거부
- 잔액 부족: 사용자 계좌 잔액 부족
- 카드 문제: 카드 유효성 문제
@Transactional
public void failPayment(String orderId, String failureReason) {
Payment payment = paymentRepository.findByOrderIdOrElseThrow(orderId);
payment.updateStatus(PaymentStatus.FAILED);
payment.updateFailureReason(failureReason);
log.info("결제 실패 처리 완료 - orderId: {}, reason: {}", orderId, failureReason);
}
- 결제 상태가 CONFIRMED인 경우
- paymentKey가 존재하는 경우
- 토스페이먼츠에서 취소 가능한 경우

@Transactional
public PaymentResponseDto cancelPayment(String orderId, String cancelReason) {
// 1. 주문 정보 조회
Payment payment = paymentRepository.findByOrderIdOrElseThrow(orderId);
// 2. 취소 가능한 상태인지 확인
if (payment.getStatus() != PaymentStatus.CONFIRMED) {
throw new BaseException(ExceptionCode.PAYMENT_NOT_CANCELLABLE);
}
try {
// 3. 토스페이먼츠 결제 취소 요청
PaymentResponseDto tossResponse = tossPaymentClient.cancelPayment(
payment.getPaymentKey(), cancelReason);
// 4. 결제 정보 업데이트
payment.updateStatus(PaymentStatus.CANCELLED);
payment.updateFailureReason(cancelReason);
// 5. 포인트 차감 (충전했던 포인트 되돌리기)
pointService.usePoint(
payment.getUser().getId(),
payment.getAmount(),
"결제 취소로 인한 포인트 차감 - " + payment.getOrderName()
);
return tossResponse;
} catch (Exception e) {
throw new BaseException(ExceptionCode.TOSS_PAYMENT_CANCEL_FAILED);
}
}
@Entity
public class Payment extends BaseEntity {
private String orderId; // 주문 ID (유니크)
private String paymentKey; // 토스 결제 키
private String orderName; // 주문명
private Integer amount; // 결제 금액
private PaymentStatus status; // 결제 상태
private String method; // 결제 수단
private User user; // 결제 사용자
private String failureReason; // 실패 사유
}
@Transactional(readOnly = true)
public List<Payment> getPaymentHistory(Long userId) {
return paymentRepository.findByUserIdOrderByCreatedAtDesc(userId);
}
// 결제 금액 검증
if (!payment.getAmount().equals(amount)) {
throw new BaseException(ExceptionCode.PAYMENT_AMOUNT_MISMATCH);
}
// 결제 상태 검증
if (payment.getStatus() != PaymentStatus.PENDING) {
throw new BaseException(ExceptionCode.INVALID_PAYMENT_STATUS);
}
- API 키 Base64 인코딩
- HTTPS 통신 강제
- 민감 정보 로깅 방지
- 동일한 orderId로 중복 요청 방지
- 토스페이먼츠 측 멱등성 활용
Method |
Endpoint |
설명 |
권한 |
POST |
/api/payments/prepare |
결제 준비 |
USER |
POST |
/api/payments/confirm |
결제 승인 |
USER |
POST |
/api/payments/fail |
결제 실패 처리 |
USER |
Method |
Endpoint |
설명 |
권한 |
GET |
/api/payments/history |
결제 내역 조회 |
USER |
GET |
/api/payments/status/{paymentKey} |
결제 상태 조회 |
USER |
Method |
Endpoint |
설명 |
권한 |
POST |
/api/payments/cancel/{orderId} |
결제 취소 |
USER, ADMIN |
{
"amount": 50000,
"orderName": "포인트 충전"
}
{
"statusCode": 200,
"message": "결제 준비가 완료되었습니다.",
"data": {
"orderId": "ORDER_20250702143022_a1b2c3d4",
"amount": 50000,
"orderName": "포인트 충전",
"customerEmail": "[email protected]",
"customerName": "김피트",
"successUrl": "https://fitpass.com/payment/success",
"failUrl": "https://fitpass.com/payment/fail"
}
}
{
"paymentKey": "5zJ4xY7m0kODnyRpQWGrN2xqGlNvLrKwv1M9ENjbeoPaZdL6",
"orderId": "ORDER_20250702143022_a1b2c3d4",
"amount": 50000
}
{
"statusCode": 200,
"message": "결제가 성공적으로 완료되었습니다.",
"data": {
"paymentKey": "5zJ4xY7m0kODnyRpQWGrN2xqGlNvLrKwv1M9ENjbeoPaZdL6",
"orderId": "ORDER_20250702143022_a1b2c3d4",
"orderName": "포인트 충전",
"method": "카드",
"totalAmount": 50000,
"status": "DONE",
"requestedAt": "2025-07-02T14:30:22",
"approvedAt": "2025-07-02T14:30:25"
}
}
- 최소 결제 금액: 1,000원
- 최대 결제 금액: 1,000,000원
- 결제 단위: 1,000원 단위
- 일일 결제 한도: 3,000,000원
- 취소 가능 기간: 결제 완료 후 30일
- 부분 취소: 지원하지 않음 (전액 취소만)
- 취소 수수료: 없음
- 취소 처리 시간: 즉시 처리
- 포인트 환불: 즉시 차감
- 현금 환불: 토스페이먼츠 정책 따름
- 카드 환불: 영업일 기준 3-5일
- 계좌 환불: 영업일