티스토리 뷰
저번 포스트를 통해 이제 요청한 값들은 이제 유효함이 검증된 것이고 토스페이먼츠에 전달할 때 필요한 값들도 넣어준 상태이다. 이제 프론트에서 해당 값들을 잘 받았다는 가정하에 이어서 진행해보자.
결제요청 응답 데이터
{
"success": true,
"code": 1,
"message": "성공",
"data": {
"payType": "카드",
"amount": 3000,
"orderId": "0023b4e8-2b58-4248-9750-664ce4f7f1db",
"orderName": --상품명--
"customerEmail": "test@test.com",
"customerName": "김바보",
"successUrl": "http://localhost:9090/success",
"failUrl": "http://localhost:9090/fail",
"createDate": "2022-03-17 23:11:02",
"paySuccessYn": null
}
}
서버에서 프론트로 응답 값이 위와 전달되었을 때 해당 값들을 가지고 토스페이먼츠로 결제요청을 보내보자.
프론트 작성
결제 요청 화면
<!DOCTYPE html>
<html lang="ko">
<head>
<title>결제하기</title>
<meta charset="utf-8">
<script src="https://js.tosspayments.com/v1"></script>
</head>
<body>
<section>
<!-- ... -->
<span>총 주문금액</span>
<span>3000 원</span>
<button id="payment-button">3000원 결제하기</button>
</section>
<script>
var clientKey = '테스트_클라이언트_키'
var tossPayments = TossPayments(clientKey)
var button = document.getElementById('payment-button') // 결제하기 버튼
button.addEventListener('click', function () {
tossPayments.requestPayment('CARD', {
amount: 3000,
orderId: '0023b4e8-2b58-4248-9750-664ce4f7f1db',
orderName: --상품명--
customerName: '김바보',
customerEmail: '주문자_이메일',
successUrl: 'http://localhost:9090/success',
failUrl: 'http://localhost:9090/fail',
})
})
</script>
</body>
</html>
- 지금은 정적으로 값을 직접 넣어줬지만 실제로는 서버의 응답값을 가지고 파싱하게 될것이다.
그럼 프론트는 전달받은 값을 가지고 위 사진처럼 버튼을 통해 토스페이먼츠의 requestPayment()으로 결제를 요청하게 된다. 지금은 화면에서 클릭시 바로 요청이 가지만 추후 프론트에서는 백에 요청 후 응답값으로 재 요청을 하게 될 부분이다.
요청을 하게 되면 위와같이 토스에서 제공하는 결제 시스템이 뜨게 된다. 결제금액 및 상품명이 요청 시 작성한 내용과 동일한 것을
볼 수 있다.
정상적으로 결제가 완료가 되면
이렇게 지정해둔 성공 시 콜백 주소로 orderId, paymentKey, amount 3개의 파라미터 값이 넘어오게 된다.
이때, orderId는 주문 별 UUID이므로 유니크하다. 따라서 이 값을 가지고 저장된 결제 호출 내역을 조회해서 현재 amount값과 결제 요청때 저장된 amount값과 비교하면 정상적으로 가격 이상없이 주문이 이뤄졌는지 알 수 있다.
이때 받은 paymentKey가 결제 취소및 결제 조회에 사용되므로 결제 Entity에 추가해줄 필요가 있다. 필자는 아래와 같이 Payment 엔티티에 추가해줬다.
@Setter
@Column
private String paymentKey;
그럼 이제 프론트에서 요청한 데이터를 반환하였고, 프론트가 다시 응답데이터로 결제요청을 해서 -> 토스페이먼츠가 결제 결과로 성공 시 콜백 주소로 응답을 보낸것을 처리해보겠다.
가격 검증 로직 + 성공 시 콜백 응답 처리
Payment Controller
@GetMapping("/success")
@ApiOperation(value = "결제 성공 리다이렉트", notes = "결제 성공 시 최종 결제 승인 요청을 보냅니다.")
public SingleResult<String> requestFinalPayments(
@ApiParam(value = "토스 측 결제 고유 번호", required = true) @RequestParam String paymentKey,
@ApiParam(value = "우리 측 주문 고유 번호", required = true) @RequestParam String orderId,
@ApiParam(value = "실제 결제 금액", required = true) @RequestParam Long amount
) {
try {
System.out.println("paymentKey = " + paymentKey);
System.out.println("orderId = " + orderId);
System.out.println("amount = " + amount);
paymentService.verifyRequest(paymentKey, orderId, amount);
String result = paymentService.requestFinalPayment(paymentKey, orderId, amount);
return responseService.getSingleResult(result);
} catch (Exception e) {
e.printStackTrace();
throw new BussinessException(e.getMessage());
}
}
}
- 성공 시 콜백 요청을 처리하는 곳이다. 토스페이먼츠가 보낸 파라미터를 잘 취합해서 검증을 위해 서비스로 보내게 된다.
Payment Service - (1) verifyRequest()
@Transactional
public void verifyRequest(String paymentKey, String orderId, Long amount) {
paymentRepository.findByOrderId(orderId)
.ifPresentOrElse(
P -> {
// 가격 비교
if (P.getAmount().equals(amount)) {
P.setPaymentKey(paymentKey);
} else {
throw new BussinessException(ExMessage.PAYMENT_ERROR_ORDER_AMOUNT);
}
}, () -> {
throw new BussinessException(ExMessage.UNDEFINED_ERROR);
}
);
}
- Controller에서 전달 받은 값을 이용해서 요청 했던 결제 금액과 실제 토스페이먼츠에서 결제된 금액이 동일한지 검증한다. 이때 조회는 UUID로 생성한 orderId로 하였다.
- 가격이 동일하다면 정상적인 결제가 된 것이므로 추후 결제 취소 및 결제 조회를 위해 paymentKey도 넣어주었다.
Payment Service - (2) requestFinalPayment()
@Transactional
public String requestFinalPayment(String paymentKey, String orderId, Long amount) {
RestTemplate rest = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
String encodedAuth = new String(Base64.getEncoder().encode(testSecretApiKey.getBytes(StandardCharsets.UTF_8)));
headers.setBasicAuth(encodedAuth);
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("orderId", orderId);
params.add("amount", amount+"");
HttpEntity formEntity = new HttpEntity<>(params, headers);
ResponseEntity<String> response = rest.postForEntity(
tossOriginUrl + paymentKey,
formEntity,
String.class
);
return response.getBody();
}
- 검증이 되었으므로(결제 요청에 이상이 없고, 요청 가격과 결제된 금액이 같고, 토스 측 결제 고유 ID도 잘 저장된 상태), 이제 토스페이먼츠에게 최종 결제 승인 요청을 보내면 된다.
- POST 요청으로 보내며 헤더에는 토스에서 제공해준 시크릿키를 Basic Authorization 방식으로 인코딩해서 보내야한다.
참고로 요청 URL은 {https://api.tosspayments.com/v1/payments/} + {paymentKey} 로 이뤄진다. - 여기에 Header를 추가하고, Body에는 orderId, amount를 추가해주면 된다.
이렇게 최종적으로 요청을 보내면!
아주 슬프게도 실패가 뜬다...
원인으로 딱 2가지를 찾을 수 있었다. ❗️중요❗️
- Basic Authorization 인가코드를 보낼 때 시크릿키를 Base64를 이용하여 인코딩하여 보내게 된다. 이때 {시크릿키 + ":"} 조합으로 인코딩을 해야한다. 콜론을 꼭! 잊지말자.
- 요청을 보낼 때, amount는 숫자이다. 이 부분을 무심코 문자열로 보내는 실수를 하면 Bad Request가 뜨면서 필수 매개변수를 포함하지 않았다는 에러를 맞이하게 된다.
net.minidev.json.JSONObject를 사용해서키 : 값 쌍을 보내주도록 하자. 이것을 이용하면 값을 문자열이아닌 오브젝트로 보낼 수 있으므로 숫자를 그대로 보낼 수 있다.
이렇게 2가지 사항을 수정하여 다시 만든 코드는 아래와 같다.
@Transactional
public String requestFinalPayment(String paymentKey, String orderId, Long amount) {
RestTemplate rest = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
testSecretApiKey = testSecretApiKey + ":";
String encodedAuth = new String(Base64.getEncoder().encode(testSecretApiKey.getBytes(StandardCharsets.UTF_8)));
headers.setBasicAuth(encodedAuth);
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
JSONObject param = new JSONObject();
param.put("orderId", orderId);
param.put("amount", amount);
return rest.postForEntity(
tossOriginUrl + paymentKey,
new HttpEntity<>(param, headers),
String.class
).getBody();
}
이렇게 작성 후 테스트를 진행해 보면
성공적인 응답값을 확인할 수 있다.
그럼 백엔드는 프론트에게 값을 예쁘게 전달해줘야 할 의무가 있으니 토스페이먼츠의 응답을 처리할 반환용 객체를 만들어서 해당 객체로 받도록 하자.
성공 응답 처리 DTO
@Data
public class PaymentResHandleDto {
String mId; // : "tosspayments", 가맹점 ID
String version; // : "1.3", Payment 객체 응답 버전
String paymentKey; // : "5zJ4xY7m0kODnyRpQWGrN2xqGlNvLrKwv1M9ENjbeoPaZdL6",
String orderId; // : "IBboL1BJjaYHW6FA4nRjm",
String orderName; // : "토스 티셔츠 외 2건",
String currency; // : "KRW",
String method; // : "카드", 결제수단
String totalAmount; // : 15000,
String balanceAmount; // : 15000,
String suppliedAmount; // : 13636,
String vat; // : 1364,
String status; // : "DONE", 결제 처리 상태
String requestedAt; // : "2021-01-01T10:01:30+09:00",
String approvedAt; // : "2021-01-01T10:05:40+09:00",
String useEscrow; // : false,
String cultureExpense; // : false,
PaymentResHandleCardDto card; // : 카드 결제,
PaymentResHandleCancelDto cancels; // : 결제 취소 이력 관련 객체
String type; // : "NORMAL", 결제 타입 정보 (NOMAL, BILLING, CONNECTPAY)
}
응답 카드 정보 DTO
@Data
public class PaymentResHandleCardDto {
String company; // "현대",
String number; // "433012******1234",
String installmentPlanMonths; // 0,
String isInterestFree; // false,
String approveNo; // "00000000",
String useCardPoint; // false,
String cardType; // "신용",
String ownerType; // "개인",
String acquireStatus; // "READY",
String receiptUrl; // "https://merchants.tosspayments.com/web/serve/merchant/test_ck_jkYG57Eba3G06EgN4PwVpWDOxmA1/receipt/5zJ4xY7m0kODnyRpQWGrN2xqGlNvLrKwv1M9ENjbeoPaZdL6"
}
이렇게 응답값을 처리할 DTO를 생성하고, 토스페이먼츠에 요청을 보내는 postForEntity()부분에 응답처리를 위한 객체에
해당 DTO를 넣어주면된다.
... 생략
return rest.postForEntity(
tossOriginUrl + paymentKey,
new HttpEntity<>(param, headers),
PaymentResHandleDto.class
).getBody();
최종 결제 성공 응답 JSON
그러면 이렇게 깔끔하게 출력이 된다.
이렇게 최종적으로 성공 시 콜백 처리를 완료하였다. 다음 포스팅에서는 실패 시 콜백 처리를 진행하겠다.
Reference
'프로젝트 > 토스페이먼츠 PG 연동 시리즈' 카테고리의 다른 글
토스페이먼츠 시리즈 (6) _ 금융결제원 계좌실명조회 (8) | 2022.04.20 |
---|---|
토스페이먼츠 시리즈 (5) _ 카드 결제취소 (0) | 2022.04.16 |
토스페이먼츠 시리즈 (4) _ 카드 결제 3 (3) | 2022.04.12 |
토스페이먼츠 시리즈 (2) _ 카드 결제 1 (25) | 2022.04.04 |
토스페이먼츠 시리즈 (1) _ 도입 (4) | 2022.03.31 |
- Total
- Today
- Yesterday