springboot 카카오페이 결제 취소 구현

msw-Hub·2025년 7월 11일

springboot

목록 보기
4/4
post-thumbnail

앞선 카카오페이 단건 결제 구현에 이어서 취소부분이다.
단건 결제 구현은 앞글을 확인.

카카오페이 단건 결제 취소를 Spring Boot 기반 웹 애플리케이션에 연동한 과정을 정리한다.


1️⃣ 카카오페이 결제 취소 흐름 (공식 문서 기준)

카카오페이 결제 취소는 공식 API 문서에 명확히 정의되어 있으며, 일반적으로 다음과 같은 흐름으로 이루어진다

  1. 취소 요청 준비
    • 결제 승인 시 발급받은 tid(거래 고유번호)와 취소할 금액을 포함한 요청 정보를 준비한다.
    • 필요에 따라 부분 취소도 가능하지만, 이번 글에서는 전체 취소를 기준으로 설명한다.
  2. 취소 API 호출
    • 카카오페이 서버에 취소 요청을 보낸다.
    • 성공적으로 처리되면 취소 완료 응답을 받는다.
  3. 응답 처리
    • 취소 성공 시, 응답 데이터를 바탕으로 주문 상태를 ‘취소’ 또는 ‘결제 취소 완료’로 갱신한다.
    • 실패 시 에러 메시지를 로깅하거나 사용자에게 알린다.

아래의 예시코드는 기존에 있던 취소 로직에 추가한 방식이다.
이전 취소 로직은 주문내역에서 취소버튼 누른면 아이템 재고 수량이 다시 늘어나고 주문내역이 취소로 바뀌는 로직이였다.


2️⃣ 카카오페이 결제 취소 기능 구현

  1. 주문 취소 요청 컨트롤러
    사용자가 특정 주문을 취소 요청하면, 로그인한 사용자 권한을 체크한 후 주문 취소 서비스를 호출한다.

    @PostMapping("/order/{orderId}/cancel")
    public @ResponseBody ResponseEntity<?> cancelOrder(
           @PathVariable Long orderId,
           Principal principal
    ) {
       if (!orderService.validateOrder(orderId, principal.getName())) {
           return new ResponseEntity<>("주문 권한이 없습니다.", HttpStatus.FORBIDDEN);
       }
    
       orderService.cancelOrder(orderId);
    
       return new ResponseEntity<Long>(orderId, HttpStatus.OK);
    }
  2. 주문 취소 서비스 메서드
    결제가 승인된 상태라면 kakaoPayService.cancelPayment를 호출해 카카오페이 서버에 취소 요청한다.
    이후 결제 상태를 CANCELLED로 변경해 저장한다.
    이후 주문 엔티티의 cancelOrder()로 주문 상태 변경 및 재고 복구를 수행한다.

    @Transactional
    public void cancelOrder(Long orderId) {
       Order order = orderRepository.findById(orderId)
               .orElseThrow(EntityNotFoundException::new);
    
       // 결제 완료된 주문이면 카카오페이 결제 취소 먼저 수행
       if (order.getPaymentStatus() == PaymentStatus.APPROVED) {
           KakaoPayCancelResponseDto dto = kakaoPayService.cancelPayment(order); // 카카오페이 취소 API 호출
           order.setPaymentStatus(PaymentStatus.CANCELLED); // 결제 상태 취소로 변경
       }
    
       // 주문 상태를 취소로 변경하고, 주문 상품 재고 복구 처리
       order.cancelOrder();
    }
  3. 카카오페이 취소 API 호출 서비스
    앞선 서비스 메서드를 타고 들어온 카카오 쉬소 api호출 메서드다.
    카카오페이 결제 취소 요청 DTO를 생성해 API 클라이언트에 전달한다.
    성공 시 Slack으로 상세 취소 정보를 알린다.
    실패 시 예외를 던지고 Slack으로 오류 메시지를 전송한다.

    @Transactional
    public KakaoPayCancelResponseDto cancelPayment(Order order) {
      try {
          KakaoPayCancelResponseDto response = kakaoPayApiClient.requestCancelPayment(
                  new KakaoPayCancelRequestDto(order.getKakaoTid(), order.getTotalPrice())
          );
    
          String message = String.format(
                  ":white_check_mark: 결제 취소 완료\n" +
                  "주문번호: %d\n" +
                  "결제금액: %,d원\n" +
                  "취소일시: %s\n" +
                  "결제수단: 카카오페이\n" +
                  "결제번호(TID): %s\n" +
                  "구매자: %s",
                  order.getId(),
                  order.getTotalPrice(),
                  LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
                  order.getKakaoTid(),
                  order.getMember().getEmail()
          );
    
          // Slack으로 결제 취소 성공 메시지 전송
          slackNotifier.sendMessage(message);
    
          return response;
    
      } catch (Exception e) {
          String errorMessage = String.format(
                  ":x: 결제 취소 실패\n" +
                  "주문번호: %d\n" +
                  "에러: %s",
                  order.getId(),
                  e.getMessage()
          );
    
          // Slack으로 실패 메시지 전송
          slackNotifier.sendMessage(errorMessage);
    
          throw new RuntimeException("결제 취소 중 오류 발생", e);
      }
    }
  4. 카카오페이 취소 API 호출 클라이언트
    restTemplate를 이용해 카카오페이 취소 API에 POST 요청을 전송한다.
    구성은 공식문서 기반으로 작성한다.

    public KakaoPayCancelResponseDto requestCancelPayment(KakaoPayCancelRequestDto dto) {
          HttpHeaders headers = new HttpHeaders();
          headers.setContentType(MediaType.APPLICATION_JSON);
          headers.set("Authorization", "SECRET_KEY " + adminKey);
    
          Map<String, Object> body = new HashMap<>();
          body.put("cid", cid);
          body.put("tid", dto.getTid());
          body.put("cancel_amount", dto.getCancelAmount());
          body.put("cancel_tax_free_amount", 0);
          body.put("cancel_vat_amount", 0);
          body.put("cancel_available_amount", dto.getCancelAmount());
    
          HttpEntity<Map<String, Object>> req = new HttpEntity<>(body, headers);
    
          ResponseEntity<KakaoPayCancelResponseDto> response = restTemplate.postForEntity(
                  "https://open-api.kakaopay.com/online/v1/payment/cancel",
                  req,
                  KakaoPayCancelResponseDto.class
          );
    
          return response.getBody();
      }

결제 취소 흐름도

사용자
  │
  │ 1. 주문 취소 요청 (/order/{orderId}/cancel)
  │─────────────────────────────▶
  │                            주문 컨트롤러
  │                            └─> 주문 권한 검증 (validateOrder)
  │                            └─> 권한 확인 실패 시 403 반환
  │                            └─> 권한 확인 성공 시 cancelOrder 호출
  │
  │                             주문 서비스 (cancelOrder)
  │                             └─> 주문 조회 (orderRepository.findById)
  │                             └─> 결제상태 확인 (PaymentStatus.APPROVED ?)
  │                             └─> 승인 상태면 카카오페이 결제 취소 API 호출
  │
  │                             카카오페이 서비스 (cancelPayment)
  │                             └─> 카카오페이 API 클라이언트 호출(requestCancelPayment)
  │                             └─> API 호출 성공 시 Slack 결제 취소 메시지 전송
  │                             └─> API 호출 실패 시 Slack 실패 메시지 전송 및 예외 발생
  │
  │                             주문 상태 결제 취소(CANCELLED) 변경 및 주문 취소 처리 (재고 복구 등)
  │
  │◀─────────────────────────────
  │ 2. 주문 취소 성공 응답 (HTTP 200 OK)
profile
천천히 시작하는 개발자

0개의 댓글