
payment.requestPayment() 메서드를 호출하면 결제 요청이 시작되고, 결제창이 열려요. 메서드의 파라미터로 결제수단(CARD), 주문번호, 결제금액, successUrl, failUrl 등 필요한 정보를 설정하세요.
결제하기 버튼을 클릭하면 주문창의 orderId, amount 등의 정보가 넘어온다.
우선 결제 승인 요청을 위해서 필수 정보인 orderId와 amount의 값만 session에 저장한다.
사용자가 결제 방법을 선택한 후, 결제에 성공하면 결제 요청에 성공한거다. 그럼 미리 설정해둔 successUrl에 쿼리 파라미터로 paymentKey, orderId, amount가 입력되어서 돌아온다.
/success?orderId={ORDER_ID}&paymentKey={PAYMENT_KEY}&amount={AMOUNT}

쿼리 파라미터의 값을 아까 session에 저장해둔 orderId와 amount의 값을 비교해 중간에 값이 바뀌진 않았는지 검증한다.
검증에 성공했다면 toss에게 결제 승인 요청을 보낸다.
모든 과정이 완료되면 결제가 완료된 것.
DB에 값을 저장한다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>pay test</title>
<script src="https://js.tosspayments.com/v2/standard"></script>
</head>
<body>
<div id="order_id">
<ul>
<li>
<div class="order_item">
<span>상품아이디 : </span>
<span id="item_no1">1</span>
<span>상품명 : </span>
<span id="item_name1">티셔츠</span>
<span>가격 : </span>
<span id="item_amount1">5000</span>
<span>수량 : </span>
<span id="item_quantity1">3</span>
</div>
</li>
<li>
<div class="order_item">
<span>상품아이디 : </span>
<span id="item_no2">2</span>
<span>상품명 : </span>
<span id="item_name2">볼펜</span>
<span>가격 : </span>
<span id="item_amount2">500</span>
<span>수량 : </span>
<span id="item_quantity2">5</span>
</div>
</li>
</ul>
</div>
<button class="button" style="margin-top: 30px" onclick="requestPayment()">결제하기</button>
<script>
const clientKey = "test_ck_....";
const customerKey = "....";
const tossPayments = TossPayments(clientKey);
async function requestPayment() {
const items = [];
const itemCount = document.querySelectorAll('#order_id .order_item').length;
let orderId = generateRandomString();
for (let i = 1; i <= itemCount; i++) {
const itemId = document.getElementById(`item_no${i}`).innerText;
const amount = parseInt(document.getElementById(`item_amount${i}`).innerText);
const quantity = parseInt(document.getElementById(`item_quantity${i}`).innerText);
const itemName = document.getElementById(`item_name${i}`).innerText;
items.push({itemId, amount, quantity, itemName});
}
let totalAmount = 0;
let totalQuantity = 0;
let orderName = '';
if (items.length === 1) {
orderName = `${items[0].itemName}`;
} else {
orderName = `${items[0].itemName} 외 ${items.length - 1}건`;
}
items.forEach((item, index) => {
totalAmount += item.amount * item.quantity;
totalQuantity += item.quantity;
});
console.log("상품 종류 개수: ", itemCount);
console.log("총 상품 개수: ", totalQuantity);
console.log("총 결제 금액: ", totalAmount);
console.log("주문 이름: ", orderName);
try {
const response = await fetch('/api/payment/initiate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId, totalAmount })
});
if (!response.ok) {
throw new Error('서버에 결제 정보 저장 실패');
}
} catch (error) {
console.error('결제 정보 저장 중 오류 발생:', error);
return;
}
// 결제를 요청하기 전에 orderId, amount를 서버에 저장하세요.
// 결제 과정에서 악의적으로 결제 금액이 바뀌는 것을 확인하는 용도입니다.
// 결제 요청
const payment = tossPayments.payment({customerKey});
try {
await payment.requestPayment({
method: "CARD", // 카드 결제
amount: {
currency: "KRW",
value: totalAmount,
},
orderId: orderId, // 고유 주문번호
orderName: orderName,
successUrl: 'http://localhost:8080/success', // 결제 요청이 성공하면 리다이렉트되는 URL
failUrl: 'http://localhost:8080/fail', // 결제 요청이 실패하면 리다이렉트되는 URL
customerEmail: "customer123@gmail.com",
customerName: "김토스",
customerMobilePhone: "01012341234",
// 카드 결제에 필요한 정보
card: {
useEscrow: false,
flowMode: "DEFAULT", // 통합결제창 여는 옵션
useCardPoint: false,
useAppCardOnly: false,
},
});
} catch (error) {
console.error('결제 요청 중 오류 발생:', error);
}
}
function generateRandomString() {
return window.btoa(Math.random()).slice(0, 20);
}
</script>
</body>
</html>
우선 1차 코드다. totalAmount, quantity 등은 html 등에서 값 수정이 일어날 수 있기 때문에 보안상 나중에 fetch 등을 사용해서 백에서 구현할 예정이다.
그리고 orderId도 order Table에서 가져와야하는데 아직 없어서 우선 generateRandomString()으로 구현해뒀다.
여기서 가 버튼을 클릭하면 requestPayment가 동작한다는 코드다 -> 결제 요청
@PostMapping("/payment")
public ResponseEntity<?> initiatePayment(@RequestBody RequestOrderValueDto orderValue, HttpServletRequest request) {
HttpSession session = request.getSession(true);
session.setAttribute("orderId", orderValue.getOrderId()); // orderValue 필드명 - javascript fetch에서 보내는 데이터의 이름이랑 같아야 함 (json 역직렬화 규칙)
session.setAttribute("amount", orderValue.getTotalAmount());
return ResponseEntity.ok("Order information saved in session");
}



session에 임시 저장한 후, 결제 버튼에 대한 로직이 진행되는데 티셔츠 외 2건, 총 결제 금액을 javascript에 구현해둬서 동적으로 동작한다.

사용자가 결제 방법 및 결제까지 완료하면 미리 설정해둔 successUrl에 쿼리 파라미터로 paymentKey를 덧붙여서 Toss가 보내준다.
@GetMapping("/success")
public ResponseEntity<Success> paymentSuccess(@RequestParam String paymentKey,
@RequestParam String orderId,
@RequestParam Double amount,
HttpServletRequest request) {
Success successDto = new Success(paymentKey, orderId, amount);
HttpSession session = request.getSession();
// 임시 저장값과 같은지 검증
if (Objects.equals(orderId, (String) session.getAttribute("orderId"))) {
if (Objects.equals(amount, (Double) session.getAttribute("amount"))) {
// 같으면 세션 삭제
session.removeAttribute(orderId);
try {
rest.confirm(successDto);
} catch (URISyntaxException e) {
failUrl("INVALID_REQUEST", "잘못된 요청입니다.", orderId);
}
}
}
return ResponseEntity.ok().build();
}



session에 저장되어있는 값과 파라미터로 들어온 값이 일치해 검증에 성공했다.
-> session의 임시 저장 값을 삭제한다.
-> 그 후, toss에게 RestTemplate으로 결제 승인 요청을 보낸다.
public String confirm(@RequestBody Success success) throws URISyntaxException {
HttpHeaders headers = new HttpHeaders();
RestTemplate restTemplate = new RestTemplate();
URI uri = new URI("https://api.tosspayments.com/v1/payments/confirm");
headers.setBasicAuth("Basic base64("{API_SECRET_KEY}:")"... encoding한 값...);
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
Map<String, String > map = new HashMap<>();
map.put("paymentKey", success.getPaymentKey());
map.put("orderId", success.getOrderId());
map.put("amount", String.valueOf(success.getAmount()));
ObjectMapper objectMapper = new ObjectMapper();
HttpEntity<String> request = null;
try {
request = new HttpEntity<>(objectMapper.writeValueAsString(map), headers);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
ResponseEntity<String> responseEntity = restTemplate.exchange(
uri,
HttpMethod.POST,
request,
String.class
);
return responseEntity.getBody();
}
curl --request POST \
--url https://api.tosspayments.com/v1/payments/confirm \
--header 'Authorization: Basic dGVzdF9za196WExrS0V5cE5BcldtbzUwblgzbG1lYXhZRzVSOg==' \
--header 'Content-Type: application/json' \
--data '{"paymentKey":"{PAYMENT_KEY}","amount":{amount},"orderId":"{orderId}"'
이 형식에 맞춰서 restTemplate을 작성하면 된다.
그럼 최종적으로 이런 형태로 응답이 온다.
{
"mId": "tvivarepublica",
"lastTransactionKey": "txrd_a01jfvst07khb2fr75mn4vyn82p",
"paymentKey": "tviva20241224164056dL1N9",
"orderId": "MC44NDgyMDA5ODYxMzI2",
"orderName": "티셔츠 외 2건",
"taxExemptionAmount": 0,
"status": "DONE",
"requestedAt": "2024-12-24T16:40:56+09:00",
"approvedAt": "2024-12-24T16:41:21+09:00",
"useEscrow": false,
"cultureExpense": false,
"card": null,
"virtualAccount": null,
"transfer": null,
"mobilePhone": null,
"giftCertificate": null,
"cashReceipt": null,
"cashReceipts": null,
"discount": null,
"cancels": null,
"secret": "ps_XZYkKL4Mrjq9jp0n5a5oV0zJwlEW",
"type": "NORMAL",
"easyPay": {
"provider": "토스페이",
"amount": 19500,
"discountAmount": 0
},
"country": "KR",
"failure": null,
"isPartialCancelable": true,
"receipt": {
"url": "https://dashboard.tosspayments.com/receipt/redirection?transactionId=tviva20241224164056dL1N9&ref=PX"
},
"checkout": {
"url": "https://api.tosspayments.com/v1/payments/tviva20241224164056dL1N9/checkout"
},
"currency": "KRW",
"totalAmount": 19500,
"balanceAmount": 19500,
"suppliedAmount": 17727,
"vat": 1773,
"taxFreeAmount": 0,
"method": "간편결제",
"version": "2022-11-16",
"metadata": null
}
그리고 앱으로 결제했다면 이런 알림이 오고 결제가 완료된다.