문제 상황
토스 페이먼츠 SDK를 사용하여 결제 로직을 구현하던 중, 토스 페이먼츠의 서버 문제나 클라이언트 오류가 아닌, 사용자가 결제를 취소했을 때의 처리가 필요했습니다.
예를 들어, 위 사진의 결제 화면에서 X 버튼을 클릭하여 사용자가 결제를 취소할 경우, 이를 인지하고 특정 페이지로 리다이렉트되도록 설정해야 합니다.
문제 원인
결제 처리일부 코드
서버로 결제 요청을 전송하고, 토스 페이먼츠(Toss Payments)를 통해 결제를 처리하는 일부 코드입니다.
// 서버에 결제 요청 전송
fetch('/api/payments/toss', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token.startsWith('Bearer ') ? token : 'Bearer ' + token
},
body: JSON.stringify({
payType: payType,
amount: amount,
orderName: orderName,
reservationId: reservationId,
couponId: couponId
})
})
.then(response => {
if (!response.ok) {
return response.text().then(text => {
throw new Error(text || '서버 응답이 올바르지 않습니다.');
});
}
return response.json();
})
.then(data => {
var clientKey = 'test_ck_6bJXmgo28eE5Y2gdPLJj8LAnGKWx';
var tossPayments = TossPayments(clientKey);
var paymentAmount = parseInt(data.amount, 10);
if (isNaN(paymentAmount) || paymentAmount <= 0) {
throw new Error('올바르지 않은 결제 금액입니다.');
}
tossPayments.requestPayment(data.payType, {
amount: paymentAmount, // 결제 금액
orderId: data.paymentId, // 주문 번호
orderName: orderName, // 결제 상품
successUrl: "http://localhost:19096/api/payments/toss/success",
failUrl: "http://localhost:19096/api/payments/toss/fail",
}).catch(error => {
if (error.code === 'USER_CANCEL') {
console.warn('결제가 사용자가 취소되었습니다:', error.message);
alert('결제가 취소되었습니다.');
} else {
console.error('결제 실패:', error.message);
alert('결제 처리 중 오류가 발생했습니다: ' + error.message);
}
});
})
});
위 사진을 보면 결제가 취소될 경우 failUrl로 리다이렉트되지 않고, 개발자 도구에만 취소 로그가 표시되는 문제가 발생했습니다.
토스페이먼츠(Toss Payments) SDK 문서를 확인한 결과, 문제의 원인을 파악할 수 있었습니다.
토스페이먼츠의 SDK 에러 코드에 따르면, 사용자가 결제를 취소했을 때 USER_CANCEL이라는 에러 코드가 반환됩니다. 이는 결제 실패와는 다른 경우로, 사용자가 결제를 직접 취소한 상황을 나타냅니다.
토스페이먼츠의 SDK 에러 코드를 보면 사용자가 취소했을 때 "USER_CANCEL"이라는 에러가 반환되는데, 다른 결제 실패가 아닌 사용자가 직접 취소한 것이기 때문에 failUrl의 쿼리 파라미터로 code와 message는 전달되지만, orderId는 전달되지 않습니다.
이는 주문이 생성되지 않고 고객이 이탈한 것이기 때문입니다.
즉, 사용자가 결제 화면에서 X 버튼을 눌러 결제를 취소하는 경우 PAY_PROCESS_CANCELED 에러가 발생하게 되며, 이때 failUrl로는 code와 message 파라미터만 전달되고 orderId는 포함되지 않습니다.
@GetMapping("/toss/fail")
public ResponseEntity tossPaymentFail(
@RequestParam String code,
@RequestParam String message,
@RequestParam String orderId
){
log.info("@@@@@@#@#");
PaymentFailDto responseDto = paymentService.tossPaymentFail(code,message,orderId);
return ResponseEntity.ok().body(responseDto);
}
현재 fail API가 code, message, orderId 세 가지 값을 필수로 받도록 설정되어 있기 때문에 로그가 출력되지 않았던 문제를 확인했습니다.
위와 같은 코드에서 orderId가 필수로 요청되지만, 사용자가 결제를 취소한 경우에는 orderId가 전달되지 않기 때문에 fail API가 제대로 호출되지 않았습니다.
이는 토스페이먼츠에서 사용자가 결제를 취소했을 때 failUrl로 code와 message만 전달하고, orderId는 포함하지 않는 상황과 관련이 있습니다.
따라서 이 문제를 해결하려면 orderId가 없어도 API가 호출될 수 있도록 변경하거나, orderId가 전달되지 않을 경우의 처리 로직을 추가해야 합니다.
문제 해결 시도 1
해당 문제를 해결하기 위해서 사용자가 결제를 취소한 경우에는 orderId를 전달하지 않기 때문에 orderId를 없어도 API가 호출될 수 있도록 수정을 했습니다.
@GetMapping("/toss/fail")
public ResponseEntity tossPaymentFail(
@RequestParam String code,
@RequestParam String message,
@RequestParam(required = false) String orderId
) {
log.info("@@@@@@#@#");
PaymentFailDto responseDto = paymentService.tossPaymentFail(code, message, orderId);
return ResponseEntity.ok().body(responseDto);
}
위와 같이 수정하면 결제 취소 시에도 fail API가 정상적으로 호출되고, orderId가 없는 상황에서도 예외 없이 동작할 것으로 예상했습니다.
그러나 예상과는 달리, API가 호출되지 않아 로그가 출력되지 않았습니다. 이유는 아래와 같은 클라이언트 코드에서 결제 에러를 catch하여 처리하고 있기 때문에 USER_CANCEL 에러가 발생할 경우 클라이언트가 에러를 catch하면서 서버의 fail API로 요청이 전달되지 않습니다.
tossPayments.requestPayment(data.payType, {
amount: paymentAmount, // 결제 금액
orderId: data.paymentId, // 주문 번호
orderName: orderName, // 결제 상품
successUrl: "http://localhost:19096/api/payments/toss/success",
failUrl: "http://localhost:19096/api/payments/toss/fail",
}).catch(error => {
if (error.code === 'USER_CANCEL') {
console.warn('결제가 사용자가 취소되었습니다:', error.message);
alert('결제가 취소되었습니다.');
} else {
console.error('결제 실패:', error.message);
alert('결제 처리 중 오류가 발생했습니다: ' + error.message);
}
});
이 catch 블록에서 USER_CANCEL 에러를 failUrl로 전송하지 않고 클라이언트에서 처리하고 있어, 서버의 fail API가 호출되지 않는 원인이 됩니다.
문제 해결
아래와 같이 문제 해결을 위해 클라이언트에서 에러를 catch한 후, 결제 취소 시 서버의 fail-cancel API로 직접적으로 요청을 보내는 방법을 구현했습니다.
tossPayments.requestPayment(data.payType, {
amount: paymentAmount,
orderId: data.paymentId,
orderName: orderName,
successUrl: "http://localhost:19096/api/payments/toss/success",
failUrl: "http://localhost:19096/api/payments/toss/fail",
}).catch(function (error) {
if (error.code === "USER_CANCEL") {
// 결제 고객이 결제창을 닫았을 때 에러 처리
console.log("사용자가 결제를 취소했습니다.");
fetch("http://localhost:19096/api/payments/toss/fail-cancel?code=" + error.code + "&message=" + error.message);
}
});
위와 같이 사용자가 결제를 취소했을 때, USER_CANCEL 에러 코드를 확인하고 서버의 fail-cancel API에 code와 message를 쿼리 파라미터로 전달하여 호출합니다. 이 방식으로 정상적으로 서버 측에서 결제 취소에 대한 처리를 할 수 있게 됩니다.
'Trouble Shooting' 카테고리의 다른 글
[트러블 슈팅] 사용자 직접 취소 시 다중 결과 반환 트러블 슈팅 (0) | 2024.10.11 |
---|---|
[트러블 슈팅] BeanCreationException 빈 충돌 해결을 위한 @ConditionalOnProperty 활용 (0) | 2024.10.11 |
[트러블 슈팅] 토스페이먼츠 결제 트러블 슈팅 (0) | 2024.10.09 |
[트러블 슈팅] @ModelAttribute의 자동 변환 에러 해결 (0) | 2024.10.07 |
[트러블 슈팅] 게시판 좋아요 동시성 문제 해결 (2) | 2024.10.02 |