티스토리 뷰

앞에서 정리한 내용을 바탕으로 실제 가상계좌 결제를 코드로 만들어보자.

 

먼저 카드결제만 존재하던 기존의 서비스를 가상결제도 가능하게 변경해줘야겠다.

기존 결제 요청 로직 수정

결제수단 추가

@Getter
@RequiredArgsConstructor
public enum PayType {
	CARD("카드"), VIRTUAL_ACCOUNT("가상계좌");
	private final String name;
}

 

 

카드든 가상계좌든 프론트에서 결국 그에 맞게 requestPayment("결제수단", {"결제 파라미터"}) 를 호출해줘야 한다. 이건 일단 프론트에게 맡기도록 하고 이후 사용자가 해당 호출로 뜬 토스페이먼츠의 결제 시스템을 이용해서 카드결제를 진행하거나 또는 가상계좌번호를 발급하게 될것이다.

 

위 과정이 성공이라면

  • https://{ORIGIN}/success?paymentKey={PAYMENT_KEY}&orderId={ORDER_ID}&amount={AMOUNT}

실패라면

  • https://{ORIGIN}/fail?code={ERROR_CODE}&message={ERROR_MESSAGE}&orderId={ORDER_ID}

동일한 콜백주소로 처리된다.

 

실패는 이전과 동일하고 성공에서 카드냐 가상계좌냐 에 맞춰서 다르게 진행되어야 하니 처리해주자.

 

가상계좌 정보 처리 DTO 추가

@Data
public class PaymentResHandleVirtualDto {
	String accountNumber;               // X6505636518308",						
	String accountType;                 // 일반",					
	String bank;                        // 우리",				
	String customerName;                // 박토스",						
	String dueDate;                     // 2021-02-05T21:05:09+09:00",				
	String expired;                     //				 
	String settlementStatus;            // INCOMPLETED",							
	String refundStatus;                // NONE"						
}
  • PaymentResHandleDto 에 위 객체 필드를 추가해주었다. 
  • 이제 가상계좌 결제든, 카드 결제든 PaymentResHandleDto 혼자서 처리할 수 있게 되었다.

Payment 엔티티 필드 추가

@Setter
@Column
private String virtualAccountNumber;

@Setter
@Column
private String virtualBank;

@Setter
@Column
private String virtualDueDate;		// 입금기한: 2021-02-05T21:05:09+09:00

@Setter
@Column
private String virtualRefundStatus;
  • 가상계좌 방식 결제에 대한 정보를 담을 수 있게 추가해주었다.

Payment Service

} else if (payType.equals(PayType.VIRTUAL_ACCOUNT)) {
    PaymentResHandleVirtualDto virtualAccount = payResDto.getVirtualAccount();
    paymentRepository.findByOrderId(payResDto.getOrderId())
            .ifPresent(payment -> {
                payment.setVirtualAccountNumber(virtualAccount.getAccountNumber());
                payment.setVirtualBank(virtualAccount.getBank());
                payment.setVirtualDueDate(virtualAccount.getDueDate());
                payment.setVirtualRefundStatus(virtualAccount.getRefundStatus());
            });
}
  • 기존에 카드정보를 추가해주는 구간에 분기절을 추가해준 후 가상계좌에 대한 정보를 담도록 해주었다.

가상계좌 방식 결제요청 후 결제 승인 요청 시 응답 확인

구매자가 입금해야할 계좌번호, 은행명, 예금주 명, 입금기한 등을 포함하고 있는것을 확인할 수 있다.

재정리

프론트는 이전과 동일하게 카드 / 가상계좌 + 파라미터 정보만 결정해서 requestPayment() 요청을 보내고, 이에 대한 과정 결과에 따라 토스페이먼츠는 설정한 콜백 주소로 파라미터와 함께 요청을 보낸다.

 

이전 포스팅에서는 결제 방식에 따라 다른 콜백 주소를 갖게 하였지만 그럴 필요성을 못느껴서 다시 동일한 콜백주소를 사용하도록 하였다. 따라서, 현재는 카드 / 가상계좌 둘다 동일한 콜백주소를 사용하도록 하고, 해당 요청을 처리하는 서비스로직에서 어떤 형태의 결제방법이냐에 따라서 다르게 값을 세팅해주었다.

 

흐름을 다시 제대로 정리해보자

구매자 / 프론트 / 백엔드 / 토스페이먼츠

  1. 구매자가 프론트를 통해 구매를 요청
  2. 프론트가 구매요청에 대한 검증을 위해 백엔드로 보냄
  3. 백엔드는 제대로 된 요청이라면 결제 방식에 맞게 필요한 정보들을 세팅해준 후 반환
  4. 프론트는 반환 데이터를 가지고 토스페이먼츠에게 requestPayment() 메소드로 결제시스템을 요청한다.
  5. 토스페이먼츠가 결제시스템을 전달한다. 구매자는 결제시스템을 통해 필요한 정보를 입력한 후 가상계좌 발급을 요청한다.
  6. 토스페이먼츠는 성공/실패 여부에 따라 등록된 성공/실패 시 콜백 URL로 리다이렉트 한다.
  7. 백엔드는 해당 요청을 수신하고 토스페이먼츠로 결제승인 요청을 보낸다.
  8. 가상계좌 방식의 경우 토스페이먼츠가 해당 요청을 검증 후 가상계좌 발급 정보가 담긴 JSON 응답 데이터를 반환한다.
  9. 백엔드는 반환받은 데이터를 가지고 서버에 저장할 건 저장한 후 프론트에 반환한다.
  10. 프론트는 해당 데이터를 통해 구매자에게 입금기한, 입금기관, 가상계좌 번호 등을 알려주게 된다.
  11. --구매자가 입금을 함--
  12. 구매자가 입금을 하면 토스페이먼츠가 가상계좌 입금 시 콜백 URL로 요청을 보내준다.
  13. 백엔드는 해당 요청을 확인하고 구매절차를 완료하고 이를 프론트에 반환한다.
  14. 프론트가 해당 반환값을 가지고 적절히 구매자에게 구매가 성공/실패 했음을 알려준다.

와 같이 정리할 수 있다.

 

현재 Service에서 9번까지 완료된 상태이다. 이제 프론트 영역을 뛰어넘고 12번 부분 입금 시 콜백 처리를 하도록 하겠다.


가상계좌 입금 알림 URL 콜백 처리

Payment Controller

@PostMapping("/virtual/income")
@ApiOperation(
        value = "가상계좌 입금 알림 콜백 처리",
        notes = "토스페이먼츠에서 가상계좌로 입금을 확인하면 주는 알림을 처리합니다."
)
public CommonResult confirmVirtualAccountIncome(
        @ApiParam(value = "요청 본문", required = true) @ModelAttribute TossVirtualDto tossVirtualDto
) {
    log.info("secret = " + tossVirtualDto.getSecret());
    log.info("status = " + tossVirtualDto.getStatus());
    log.info("orderId = " + tossVirtualDto.getOrderId());
    try {
        paymentService.handleVirtualAccountIncome(tossVirtualDto);
        return responseService.getSuccessResult();
    } catch (Exception e) {
        return responseService.getFailResult(
                FAIL,
                e.getMessage()
        );
    }
}
  • 가상계좌 입금 알림 콜백을 처리하기 위한 컨트롤러이다.

Payment Service

@Transactional
public void handleVirtualAccountIncome(TossVirtualDto tossVirtualDto) {
    String status = tossVirtualDto.getStatus();
    String orderId = tossVirtualDto.getOrderId();
    String secret = tossVirtualDto.getSecret();

    Payment payment = paymentRepository.findByOrderId(orderId)
            .orElseThrow(() -> new BussinessException(ExMessage.PAYMENT_ERROR_ORDER_NOTFOUND));
    Long reservationSeq = payment.getReservationSeq();
    Reservation reservation = reservationRepo.findById(reservationSeq)
            .orElseThrow(() -> new BussinessException(ExMessage.RESERVATION_ERROR_NOT_FOUND));


    if (status.equals("DONE")) {
        log.info("입금 확인");
        payment.getCustomer().getPayments()
                .stream()
                .filter(p -> p.getOrderId().equals(orderId))
                .filter(p -> p.getVirtualSecret().equals(secret))
                .findFirst()
                .ifPresentOrElse(P -> {
                    log.info("결제 성공 체크");
                    P.setPaySuccessYn("Y");
                    reservation.setPayYn("Y");
                }, () -> {
                    throw new BussinessException(ExMessage.PAYMENT_ERROR_ORDER_NOTFOUND);
                });
    } else if (status.equals("CANCELED")) {
        log.info("입금 취소");
        payment.getCustomer().getPayments()
                .stream()
                .filter(p -> p.getOrderId().equals(orderId))
                .findFirst()
                .ifPresentOrElse(P -> {
                    log.info("결제 취소 체크");
                    if (P.getPaySuccessYn().equals("Y")) {
                        log.info("결제 취소 체크 때 결제 완료라고 되있으면 롤백");
                        P.setPaySuccessYn("N");
                    }
                    P.setCancelYn("Y");
                    reservation.setPayYn("N");
                    reservation.setCancelYn("Y");
                }, () -> {
                    throw new BussinessException(ExMessage.PAYMENT_ERROR_ORDER_NOTFOUND);
                });
    }
}
  • 입금 알림 웹 훅의 상태에 따라 맞춰서 처리한다.
  • 이미 결제 요청 때 고객의 결제 리스트에 넣었으므로 결제를 안하면 취소 처리하고 취소 내역에 추가하며, 
    결제를 하면 결제했다고 체크해준다.
  • 결제 처리 시 secret 키를 통해 2차 검증을 해준다. 콜백 요청이 정상적인지 검증하기 위한 값이다. 조작해서 요청하는 것을 막으려는 용도로 생각된다.
  • 예약도 결제와 엮여 있으므로 예약에 대해서도 지불 여부를 체크해준다.

이렇게 등록한 가상계좌 입금 URL로 입금 관련 알림에 대한 처리를 완료했다.

 

추가적으로 로깅 용도로 간단하게 웹훅 기능도 구현해보겠다.

웹 훅 이벤트 처리

Payment Controller

@PostMapping("/webhook")
@ApiOperation(
        value = "토스페이먼츠 웹 훅 처리",
        notes = "토스페이먼츠에서 결제 단계별로 보내주는 웹 훅 이벤트를 처리합니다."
)
public CommonResult tossPaymentWebhook(
        @ApiParam(value = "웹 훅 본문") @ModelAttribute TossWebhookDto webhookDto
) {
    log.info("webhookDto.getEventType() = " + webhookDto.getEventType());
    log.info("webhookDto.getData().getPaymentKey() = " + webhookDto.getData().getPaymentKey());
    log.info("webhookDto.getData().getStatus() = " + webhookDto.getData().getStatus());
    log.info("webhookDto.getData().getOrderId() = " + webhookDto.getData().getOrderId());

    try {
        paymentService.registTossPaymentWebhook(webhookDto);
        return responseService.getSuccessResult();
    } catch (Exception e) {
        return responseService.getFailResult(
                FAIL,
                e.getMessage()
        );
    }
}

Payment Service

@Transactional
public void registTossPaymentWebhook(TossWebhookDto webhookDto) {
    String paymentKey = webhookDto.getData().getPaymentKey();
    Long seq = paymentRepository.findByPaymentKey(paymentKey)
            .orElseThrow(() -> new BussinessException(ExMessage.PAYMENT_ERROR_ORDER_NOTFOUND))
            .getSeq();
    PaymentWebhook paymentWebhook = webhookDto.toEntity();
    paymentWebhook.setPaymentSeq(seq);
    webhookRepository.save(paymentWebhook);
}

payment Repository의 seq만 추가해서 이벤트와 함께 저장해주었다. 아직은 사용용도를 모르겠어서 이정도만 해놓고 마무리하겠다.

 

마무리

이렇게 가상계좌를 이용한 결제요청 -> 가상계좌 발급 요청 -> 가상계좌 발급 후 입금 시 웹훅 이벤트 처리 -> 최종 결제 완료 과정을 완료했다.

 

다음 포스팅에서는 가상계좌 결제 완료 건에 대해 취소하는 방법에 대해 알아보겠다.

 

 


Reference

https://docs.tosspayments.com/guides/webhook

 

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