2025. 4. 29. 12:12ㆍSpringBoot

이번 포스팅에선 토스페이먼츠의 결제연동 방법에 대해서 알아보겠습니다.
토스페이먼츠 측에서 제공하고있는 다음 공식문서를 참고하여 구현하였습니다.
https://docs.tosspayments.com/guides/v2/payment-widget/integration
연동하기 | 토스페이먼츠 개발자센터
토스페이먼츠의 간편한 결제 연동 과정을 한눈에 볼 수 있습니다. 각 단계별 설명과 함께 달라지는 UI와 코드를 확인해보세요.
docs.tosspayments.com
결제 과정은 큰 흐름으로 인증과 승인으로 나눌 수 있습니다.

PG Server를 가지고 있는 곳이 토스페이먼츠라 생각하시면 됩니다.
구매자가 결제창을 통해 결제 수단방법 선택, 카드사 인증 까지가 결제 인증의 과정이며
PG Server를 통해 결제 데이터를 생성하고 응답 받은 후
해당 결제 데이터를 통해 결제를 승인하고 카드사에 요청하여 결제를 완료하는 과정까지가 결제 승인입니다.
인증 -> 승인까지의 과정을 모두 마쳐야 실제 계좌에서 돈이 빠져나갑니다.

유저, 클라이언트(프론트엔드), 서버(백엔드), 토스페이먼츠(PG Server) 사이에서 요청과 응답이 어떻게 오가는지 상세하게 나타낸 시퀀스다이어그램입니다.
결제과정에서 프론트엔드가 담당하는 일은 크게 3가지 업무로 토스페이먼츠 측에 결제 위젯 렌더 요청, 결제 요청, 결제 정보 전달입니다.
백엔드가 담당하는 일은 결제 정보 임시 저장, 결제 정보 검증, 결제 정보를 가지고 토스페이먼츠 측의 결제 승인 API 호출입니다.
프론트엔드
- 결제 위젯 렌더 요청
- 결제 요청
- 결제 정보 전달
백엔드
- 결제 정보 임시 저장
- 결제 정보 검증
- 결제 승인 API 호출
이번 포스팅에선 백엔드에서 결제 연동을 하는 과정을 소개하기 때문에 프론트 엔드의 업무는 토스페이먼츠 측에서 제공하는 샘플 코드와 Swagger를 통한 API 호출로 프론트엔드를 대신하겠습니다.
결제 데이터
카드사 승인까지 완료하면 PG Server(토스 페이먼츠)로부터 결제 데이터를 응답 받습니다. 결제 데이터를 응답받는 형식은 요청 결과에 따라 successUrl, failUrl로 리다이렉트 됩니다.
리다이렉트 URL의 쿼리파라미터엔 다음과 같은 데이터가 담겨있습니다.
- paymentKey
- 하나의 결제마다 생성되는 key입니다. 후에 결제 취소 시 동일한 paymentKey로 결제 취소 요청을 보냅니다. (토스페이먼츠에서 응답받는 값입니다.)
- orderId
- 하나의 주문마다 고유하게 부여되는 orderId입니다. (프론트엔드에서 생성합니다.)
- amount
- 결제 되는 금액입니다. (프론트 엔드에서 생성합니다.)
- paymentType
- 결제 타입입니다. (토스페이먼츠에서 응답받는 값입니다.)
위의 4가지 데이터를 프론트엔드가 토스페이먼츠로부터 응답받고 백엔드로 넘겨주어야 합니다.
하지만 여기서 토스페이먼츠는 다음과 같이 권고하고 있습니다.

프론트에서 결제를 요청해 successUrl, failUrl로 응답받기 전 orderId와 amount를 서버에 임시 저장하도록 되어있습니다. 결제 요청과 승인 사이에 데이터 무결성을 확인할 때 필요하므로 백엔드에선 결제 정보를 임시로 저장하는 API를 추가적으로 구현해야합니다.
백엔드에서 구현해야 할 API

백엔드에서 구현해야할 API는 결제 정보 임시저장 API, 결제 승인을 요청하는 API로 2가지입니다.
임시 저장 API
결제 정보를 임시 저장하기 위해서 Redis를 이용하기로 하였습니다.
@Service
@RequiredArgsConstructor
public class PaymentCommandService {
private static final String KEY_PREFIX = "PAYMENT-INFO:USER:";
private final StringRedisTemplate redisTemplate;
private final PaymentRepository paymentRepository;
public void temporarySavePaymentInfo(String orderId, BigDecimal amount, Long userId) {
String userIdToSave = KEY_PREFIX + userId.toString();
String orderIdToSave = orderId;
BigDecimal amountToSave = amount;
redisTemplate.opsForHash().put(userIdToSave, "orderId", orderIdToSave);
redisTemplate.opsForHash().put(userIdToSave, "amount", amountToSave.toString());
redisTemplate.expire(userIdToSave, 600, TimeUnit.SECONDS);
}
}
Hash 구조로 Key 값엔 userId가 들어가고 필드에 orderId, amount가 들어갑니다.
결제 승인 API
@Service
@RequiredArgsConstructor
public class BookingService {
...
private final StringRedisTemplate stringRedisTemplate;
private final WebClient webClient;
private static final String SECRET_KEY = "test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6";
private static final String TOSS_PAY_BASE_URL = "https://api.tosspayments.com/v1/payments/";
private static final String PAYMENT_INFO_KEY_PREFIX = "PAYMENT-INFO:USER:";
@Transactional
public Order booking(Long showId, Long showDateId, Long userId, List<Long> seatIds, String paymentKey, String orderId, BigDecimal amount) throws InterruptedException {
// Redis에서 결제 인증 정보(orderId, amount) 가져오기
String key = PAYMENT_INFO_KEY_PREFIX + userId;
String orderIdFromRedis = (String) stringRedisTemplate.opsForHash().get(key, "orderId");
String amountFromRedis = (String) stringRedisTemplate.opsForHash().get(key, "amount");
// Redis에 저장된 orderId와 요청값(orderId)이 다르면 예외 발생
if (orderIdFromRedis == null || !orderIdFromRedis.equals(orderId)) {
throw new CustomException(BAD_REQUEST, "결제 인증 정보(orderId)가 변경되었습니다.");
}
// Redis에 저장된 amount와 요청값(amount)이 다르면 예외 발생
if (amountFromRedis == null || !amountFromRedis.equals(amount.toString())) {
throw new CustomException(BAD_REQUEST, "결제 인증 정보(amount)가 변경되었습니다.");
}
// 토스페이 결제 승인 요청 준비
String encodedSecretKey = "Basic " + Base64.getEncoder().encodeToString((SECRET_KEY + ":").getBytes());
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("orderId", orderId);
requestBody.put("amount", amount);
requestBody.put("paymentKey", paymentKey);
// 토스페이에 결제 승인 요청 API 호출 (WebClient 사용)
Map responseBody = webClient.post()
.uri(TOSS_PAY_BASE_URL + "/confirm")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", encodedSecretKey)
.bodyValue(requestBody)
.retrieve()
.onStatus(HttpStatusCode::isError, response -> {
return Mono.error(new CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "결제 승인에 실패하였습니다."));
})
.bodyToMono(Map.class)
.block();
// 결제 승인이 완료되었으므로 Redis에 저장된 인증정보 삭제
stringRedisTemplate.delete(key);
// 결제 승인 응답 데이터 파싱
String tossPaymentKey = (String) responseBody.get("paymentKey");
String tossOrderId = (String) responseBody.get("orderId");
String tossOrderName = (String) responseBody.get("orderName");
String tossStatus = (String) responseBody.get("status");
Number totalAmount = (Number) responseBody.get("totalAmount");
BigDecimal tossTotalAmount = new BigDecimal(totalAmount.toString());
// 예매 로직 (Ticket, Order, Payment 저장)
...
return order;
}
@Transactional
public List<Ticket> cancelBooking(Long showId, Long showDateId, Long userId, Long paymentId, List<Long> ticketIds, String cancelReason) throws InterruptedException {
// 취소할 티켓들의 총 가격 계산
BigDecimal amountToCancel = ticketQueryService.addUpTicketPrice(ticketIds);
// 결제 정보 조회
Payment payment = paymentQueryService.getPayment(paymentId);
String foundTossPaymentKey = payment.getTossPaymentKey();
Order foundOrder = payment.getOrder();
// 토스페이 결제 취소 요청 준비
String encodedSecretKey = "Basic " + Base64.getEncoder().encodeToString((SECRET_KEY + ":").getBytes());
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("cancelReason", cancelReason);
requestBody.put("cancelAmount", amountToCancel);
// 토스페이에 결제 취소 요청 API 호출 (WebClient 사용)
Map responseBody = webClient.post()
.uri(TOSS_PAY_BASE_URL + "/" + foundTossPaymentKey + "/cancel")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", encodedSecretKey)
.bodyValue(requestBody)
.retrieve()
.onStatus(HttpStatusCode::isError, response -> {
return Mono.error(new CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "결제 취소에 실패하였습니다."));
})
.bodyToMono(Map.class)
.block();
// 결제 취소 응답 데이터 파싱
String tossPaymentKey = (String) responseBody.get("paymentKey");
String tossOrderId = (String) responseBody.get("orderId");
String tossOrderName = (String) responseBody.get("orderName");
String tossStatus = (String) responseBody.get("status");
List<Map<String, Object>> cancels = (List<Map<String, Object>>) responseBody.get("cancels");
// cancels 리스트에서 이번 요청의 실제 취소된 금액 가져오기
BigDecimal cancelAmount = BigDecimal.ZERO;
if (cancels != null && !cancels.isEmpty()) {
Map<String, Object> firstCancel = cancels.get(cancels.size()-1);
Number cancelAmountNumber = (Number) firstCancel.get("cancelAmount");
cancelAmount = new BigDecimal(cancelAmountNumber.toString());
}
// Order 갱신
BigDecimal foundTotalPrice = foundOrder.getTotalPrice();
BigDecimal totalPriceToSave = foundTotalPrice.subtract(cancelAmount);
if (totalPriceToSave.compareTo(BigDecimal.ZERO) == 0) {
foundOrder.updateTotalPrice(totalPriceToSave);
foundOrder.updateOrderStatus(OrderStatus.ORDER_CANCELED);
} else {
foundOrder.updateTotalPrice(totalPriceToSave);
}
// 예매 취소 로직 (Ticket, Order 상태 업데이트, Payment 저장)
...
return canceledTickets;
}
...
}
외부 API 호출 시 HTTP Client로 WebClient를 이용하였습니다.
HTTP Client에는 RestTemplate과 WebClient가 있습니다.
| RestTemplate | WebClient | |
| 등장 시기 | Spring 3 | Spring 5 |
| 방식 | 동기 (Blocking) | 비동기 (Non-Blocking) |
| 기반 기술 | Java Servlet API, Apache HttpClient | Spring Reactive Framework, Reactor |
| 반응형 지원 | 지원하지 않음 (비반응형) | 지원함 (반응형 프로그래밍) |
| 요청 처리 방식 | 요청-응답 동안 스레드 점유 (Block) | 요청-응답 동안 스레드 점유 없음 (Non-Block) |
| 적합한 상황 | 요청 수가 적고, 단순한 HTTP 호출 | 요청량이 많거나 고성능 병렬 처리가 필요한 경우 |
| 오류 처리 | 예외(Exception) 발생 방식 | 반응형 스트림을 통한 오류 전파 및 처리 |
| 직렬화 / 역직렬화 | 외부 라이브러리 사용 (예: Jackson) | Spring 내장 지원 기능 사용 |
| 리소스 효율성 | 스레드 수에 비례해 리소스 소모 증가 | 적은 스레드로 많은 요청 처리 가능 (효율적) |
| 현재 권장 여부 | 유지보수는 가능하나 신규 프로젝트엔 비권장 | Spring 공식 권장 (신규 프로젝트용) |
많은 곳에서 WebClient 사용을 권장하고 있기도 하며, WebClient는 Non-Blocking 방식으로 로직 내에서 응답이 느린 API 호출이있거나 API 호출이 많아질 경우 스레드 풀을 고갈 시키지 않고 실행한다는 장점이 있습니다.
따라서 WebClient를 사용해서 구현하였습니다.

결제 승인 API 호출 시 시크릿 키가 필요합니다.
시크릿 키는 토스페이먼츠 전자결제 신청 이후에만 확인할 수 있으나 신청 전에 위와같은 테스트 키로 연동하여 테스트할 수 있습니다.
위 코드에선 테스트키를 통해 연동하였습니다.
결제 승인과 결제 취소로 오는 응답의 예시입니다.
결제 승인
{
"mId": "tosspayments",
"lastTransactionKey": "9C62B18EEF0DE3EB7F4422EB6D14BC6E",
"paymentKey": "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1",
"orderId": "a4CWyWY5m89PNh7xJwhk1",
"orderName": "토스 티셔츠 외 2건",
"taxExemptionAmount": 0,
"status": "DONE",
"requestedAt": "2024-02-13T12:17:57+09:00",
"approvedAt": "2024-02-13T12:18:14+09:00",
"useEscrow": false,
"cultureExpense": false,
"card": {
"issuerCode": "71",
"acquirerCode": "71",
"number": "12345678****000*",
"installmentPlanMonths": 0,
"isInterestFree": false,
"interestPayer": null,
"approveNo": "00000000",
"useCardPoint": false,
"cardType": "신용",
"ownerType": "개인",
"acquireStatus": "READY",
"amount": 1000
},
"virtualAccount": null,
"transfer": null,
"mobilePhone": null,
"giftCertificate": null,
"cashReceipt": null,
"cashReceipts": null,
"discount": null,
"cancels": null,
"secret": null,
"type": "NORMAL",
"easyPay": {
"provider": "토스페이",
"amount": 0,
"discountAmount": 0
},
"country": "KR",
"failure": null,
"isPartialCancelable": true,
"receipt": {
"url": "https://dashboard.tosspayments.com/receipt/redirection?transactionId=tviva20240213121757MvuS8&ref=PX"
},
"checkout": {
"url": "https://api.tosspayments.com/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/checkout"
},
"currency": "KRW",
"totalAmount": 1000,
"balanceAmount": 1000,
"suppliedAmount": 909,
"vat": 91,
"taxFreeAmount": 0,
"metadata": null,
"method": "카드",
"version": "2022-11-16"
}
결제 취소
{
"mId": "tosspayments",
"lastTransactionKey": "090A796806E726BBB929F4A2CA7DB9A7",
"paymentKey": "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1",
"orderId": "a4CWyWY5m89PNh7xJwhk1",
"orderName": "토스 티셔츠 외 2건",
"taxExemptionAmount": 0,
"status": "CANCELED",
"requestedAt": "2024-02-13T12:17:57+09:00",
"approvedAt": "2024-02-13T12:18:14+09:00",
"useEscrow": false,
"cultureExpense": false,
"card": {
"issuerCode": "71",
"acquirerCode": "71",
"number": "12345678****000*",
"installmentPlanMonths": 0,
"isInterestFree": false,
"interestPayer": null,
"approveNo": "00000000",
"useCardPoint": false,
"cardType": "신용",
"ownerType": "개인",
"acquireStatus": "READY",
"amount": 1000
},
"virtualAccount": null,
"transfer": null,
"mobilePhone": null,
"giftCertificate": null,
"cashReceipt": null,
"cashReceipts": null,
"discount": null,
"cancels": [
{
"transactionKey": "090A796806E726BBB929F4A2CA7DB9A7",
"cancelReason": "테스트 결제 취소",
"taxExemptionAmount": 0,
"canceledAt": "2024-02-13T12:20:23+09:00",
"transferDiscountAmount": 0,
"easyPayDiscountAmount": 0,
"receiptKey": null,
"cancelAmount": 1000,
"taxFreeAmount": 0,
"refundableAmount": 0,
"cancelStatus": "DONE",
"cancelRequestId": null
}
],
"secret": null,
"type": "NORMAL",
"easyPay": {
"provider": "토스페이",
"amount": 0,
"discountAmount": 0
},
"country": "KR",
"failure": null,
"isPartialCancelable": true,
"receipt": {
"url": "https://dashboard.tosspayments.com/receipt/redirection?transactionId=tviva20240213121757MvuS8&ref=PX"
},
"checkout": {
"url": "https://api.tosspayments.com/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/checkout"
},
"currency": "KRW",
"totalAmount": 1000,
"balanceAmount": 0,
"suppliedAmount": 0,
"vat": 0,
"taxFreeAmount": 0,
"method": "카드",
"version": "2022-11-16",
"metadata": null
}
해당 응답에서 필요한 정보들을 파싱해 DB에 저장합니다.
결제 취소의 경우 부분 취소를 여러 번 하면 아래와 같이 Payment.cancels 필드에 취소 객체가 여러 개 돌아옵니다.
각 취소 거래마다 거래를 구분하는 transactionKey를 가지고 있습니다.
따라서 부분 취소 시 cancels필드에 가장 최신 취소 객체를 이용하여 결제 정보를 생성하였습니다.
// cancels 리스트에서 이번 요청의 실제 취소된 금액 가져오기
BigDecimal cancelAmount = BigDecimal.ZERO;
if (cancels != null && !cancels.isEmpty()) {
Map<String, Object> firstCancel = cancels.get(cancels.size()-1);
Number cancelAmountNumber = (Number) firstCancel.get("cancelAmount");
cancelAmount = new BigDecimal(cancelAmountNumber.toString());
}
// Order 갱신
BigDecimal foundTotalPrice = foundOrder.getTotalPrice();
BigDecimal totalPriceToSave = foundTotalPrice.subtract(cancelAmount);
if (totalPriceToSave.compareTo(BigDecimal.ZERO) == 0) {
foundOrder.updateTotalPrice(totalPriceToSave);
foundOrder.updateOrderStatus(OrderStatus.ORDER_CANCELED);
} else {
foundOrder.updateTotalPrice(totalPriceToSave);
}
결제 테스트 과정
결제를 테스트하기 위해서 필요한 프론트엔드 코드를 토스페이먼츠 측에서 제공하는 샘플코드로 구현하였습니다.
이 페이지로 접속하였을 때 오른쪽 위젯에 나오는 코드를 이용하였습니다.
https://docs.tosspayments.com/guides/v2/payment-widget/integration
연동하기 | 토스페이먼츠 개발자센터
토스페이먼츠의 간편한 결제 연동 과정을 한눈에 볼 수 있습니다. 각 단계별 설명과 함께 달라지는 UI와 코드를 확인해보세요.
docs.tosspayments.com
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<script src="https://js.tosspayments.com/v2/standard"></script>
</head>
<body>
<!-- 결제 UI -->
<div id="payment-method"></div>
<!-- 이용약관 UI -->
<div id="agreement"></div>
<!-- 주문 ID 표시 -->
<div style="margin-top:20px;">
<strong>주문 ID:</strong> <span id="order-id"></span>
</div>
<!-- 결제하기 버튼 -->
<button class="button" id="payment-button" style="margin-top: 30px">결제하기</button>
<script>
// 랜덤한 주문 ID 생성 함수
function generateRandomOrderId() {
// 현재 시간을 밀리초로 가져와 문자열로 변환
const timestamp = new Date().getTime().toString();
// 랜덤 문자열 생성 (영문 대소문자와 숫자 조합)
const randomChars = Math.random().toString(36).substring(2, 10);
// 타임스탬프와 랜덤 문자를 합쳐서 고유한 ID 생성
return randomChars + timestamp.substring(timestamp.length - 6);
}
main();
async function main() {
const button = document.getElementById("payment-button");
const coupon = document.getElementById("coupon-box");
const orderIdSpan = document.getElementById("order-id"); // 추가: order-id span 가져오기
// ------ 결제위젯 초기화 ------
const clientKey = "test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm";
const tossPayments = TossPayments(clientKey);
// 회원 결제
const customerKey = "XKwDYa_QHmgRjLFkas1fA";
const widgets = tossPayments.widgets({
customerKey,
});
// 비회원 결제
// const widgets = tossPayments.widgets({ customerKey: TossPayments.ANONYMOUS });
// ------ 주문의 결제 금액 설정 ------
// value에 결제할 금액을 설정하고 프로젝트를 시작하세요
await widgets.setAmount({
currency: "KRW",
value: 600,
});
await Promise.all([
// ------ 결제 UI 렌더링 ------
widgets.renderPaymentMethods({
selector: "#payment-method",
variantKey: "DEFAULT",
}),
// ------ 이용약관 UI 렌더링 ------
widgets.renderAgreement({ selector: "#agreement", variantKey: "AGREEMENT" }),
]);
// 페이지 로드 시 orderId 미리 생성하고 보여주기
let currentOrderId = generateRandomOrderId();
orderIdSpan.textContent = currentOrderId;
// ------ '결제하기' 버튼 누르면 결제창 띄우기 ------
button.addEventListener("click", async function () {
const orderId = currentOrderId;
await widgets.requestPayment({
orderId: orderId,
orderName: "2026 FIFA 월드컵 아시아 3차 예선 요르단전 수원월드컵경기장 E17",
successUrl: window.location.origin + "/success.html",
failUrl: window.location.origin + "/fail.html",
customerEmail: "customer123@gmail.com",
customerName: "김픽켓",
customerMobilePhone: "01012341234",
});
});
}
</script>
</body>
</html>
위 html 코드를 렌더링 하였을 때 화면입니다. 사용자가 결제 수단을 선택하고 결제하기를 누르면 카드사 인증으로 이동합니다.
원래 프론트 엔드에서 랜덤으로 orderId를 생성하고 서버에 결제정보 임시저장 API를 호출해야하지만 현재 프론트 엔드의 부재로 임시로 화면에 생성한 orderId를 보여준 뒤 swagger로 직접 결제정보 임시저장 API를 호출하는 방식으로 구현하였습니다.

결제 요청을 보내기 전 결제정보 임시저장 API를 통해서 서버에 결제 정보를 저장합니다.

사용자가 카드사 인증을 수행합니다. 카카오 페이를 선택해서 인증하였을 때 화면입니다.

모바일 핸드폰을 통해서 본인인증을 한 후 인증이 성공적으로 완료 되었다면 프론트엔드에서 PG Server로 결제 요청을 보냅니다.
PG Server에 결제 요청을 보내면 다음과 같은 리다이렉트Url로 응답받습니다.

success.html 코드입니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8" />
</head>
<body>
<h2>결제 성공</h2>
<p id="paymentKey"></p>
<p id="orderId"></p>
<p id="amount"></p>
<script>
// 쿼리 파라미터 값이 결제 요청할 때 보낸 데이터와 동일한지 반드시 확인하세요.
// 클라이언트에서 결제 금액을 조작하는 행위를 방지할 수 있습니다.
const urlParams = new URLSearchParams(window.location.search);
const paymentKey = urlParams.get("paymentKey");
const orderId = urlParams.get("orderId");
const amount = urlParams.get("amount");
const paymentKeyElement = document.getElementById("paymentKey");
const orderIdElement = document.getElementById("orderId");
const amountElement = document.getElementById("amount");
orderIdElement.textContent = "주문번호: " + orderId;
amountElement.textContent = "결제 금액: " + amount;
paymentKeyElement.textContent = "paymentKey: " + paymentKey;
</script>
</body>
</html>
fail.html 코드입니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8" />
</head>
<body>
<h2> 결제 실패 </h2>
<p id="code"></p>
<p id="message"></p>
</body>
</html>
<script>
const urlParams = new URLSearchParams(window.location.search);
const codeElement = document.getElementById("code");
const messageElement = document.getElementById("message");
codeElement.textContent = "에러코드: " + urlParams.get("code");
messageElement.textContent = "실패 사유: " + urlParams.get("message");
</script>
이 후 리다이렉트로 응답받았던 paymentKey, orderId, amount 정보와 함께 예매 API를 실행합니다. 해당 API 로직 안에 결제 승인 API 호출과정이 포함되어 있습니다.

다음과 같이 성공적으로 결제와 예매가 이루어진 모습을 볼 수 있습니다.
좌석이 예매된 모습

티켓이 생성된 모습

주문이 생성된 모습

결제 정보가 저장된 모습

예매 취소 API를 실행합니다. 해당 API 로직 안에 결제 취소 API 호출 과정이 포함되어 있습니다.

다음과 같이 성공적으로 결제 취소와 예매 취소가 이루어진 모습을 볼 수 있습니다.
티켓이 취소된 모습

주문이 취소된 모습

결제정보가 저장된 모습

'SpringBoot' 카테고리의 다른 글
| [SpringBoot] Bulk insert를 활용한 속도 개선 (0) | 2025.05.09 |
|---|---|
| [SpringBoot] Facade 패턴을 활용한 동시성 트랜잭션 범위 문제 해결하기 (1) | 2025.05.08 |
| [SpringBoot] Redis의 Atomic Opertaion을 이용한 좌석 선점 시스템 만들기 (1) | 2025.04.24 |
| [SpringBoot] Scale-out 환경에서 발생하는 Scheduler 중복 실행 문제 Shedlock으로 해결하기 (0) | 2025.04.21 |
| [SpringBoot] @Scheduled를 이용한 스케줄러 구현 (0) | 2025.04.21 |