티스토리 뷰

저번 포스트를 통해 이제 요청한 값들은 이제 유효함이 검증된 것이고 토스페이먼츠에 전달할 때 필요한 값들도 넣어준 상태이다. 이제 프론트에서 해당 값들을 잘 받았다는 가정하에 이어서 진행해보자.

 

결제요청 응답 데이터

{
  "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가지를 찾을 수 있었다. ❗️중요❗️

  1. Basic Authorization 인가코드를 보낼 때 시크릿키를 Base64를 이용하여 인코딩하여 보내게 된다. 이때 {시크릿키 + ":"}  조합으로 인코딩을 해야한다. 콜론을 꼭! 잊지말자.
  2. 요청을 보낼 때, 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

https://docs.tosspayments.com

 

반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday