지난 포스팅에 이어서 작성하겠습니다~
실제 결제 API를 호출하기 위해서는 httpClient 기반의 모듈이 필요하다.
기존에는 동기방식, 멀티스레드를 이용한 RestTemplate을 사용했지만, 곧 사장되는 모듈이다.
Spring 5에서 출시한 모듈인 WebClient는 비동기방식, 논블로킹 방식으로 동작하며 블로킹 방식으로도 추가적으로 이용가능하다.
이를 설정하며 실제 결제 API를 호출하는 과정을 나열하겠습니다~
WebClient 설정
- 여러 개의 결제 API를 사용할 수 있다는 것을 고려하자.
- 상속을 위해 추상클래스로 정의
/**
* Web Client 추상화한 설정
* 상속 용도 클래스
*/
public abstract class WebClientConfig {
public WebClient createWebClientFrame(String baseUrl, int readTimeout, int connectTimeOut) {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeOut)
.responseTimeout(Duration.ofMillis(3000))
.doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(3000, TimeUnit.MILLISECONDS)));
return WebClient.builder()
.baseUrl(baseUrl)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
}
=> 기본 설정을 한뒤, 프레임을 만드는 메서드를 정의
KakaoPayClientConfig 구현
- 프레임 생성 메서드를 이용
- 프로퍼티를 이용해 카카오페이 클라이언트 필요 값 인자로 넘기자
/**
* 카카오페이 WebClient 생성
*/
@Configuration
public class KakaoPayClientConfig extends WebClientConfig {
@Value("${kakaopay.url}")
private String baseUrl;
@Value("${kakaopay.readTime}")
private int readTime;
@Value("${kakaopay.connectTime}")
private int connectTime;
@Bean
public WebClient kakaoPayWebClient() {
return createWebClientFrame(baseUrl, readTime, connectTime);
}
}
KakaopayProperty
- 카카오 페이 API 요청에 필요한 기본 설정 값들을 yml에 저장했는데 이를 클래스로 관리한다.
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "kakaopay")
public class KakaoPayClientProperty {
private Api api;
private Admin admin;
private String url;
private Uri uri;
private Parameter parameter;
private int readTime;
private int connectTime;
@Getter
@Setter
public static class Api {
private String approval;
private String cancel;
private String fail;
}
@Getter
@Setter
public static class Admin {
private String key;
}
@Getter
@Setter
public static class Uri {
private String ready;
private String approve;
private String cancel;
private String order;
}
@Getter
@Setter
public static class Parameter {
private String cid;
private int taxFree;
}
}
KakaoPayReqeust
- 요청에 필요한 파라미터들을 static 클래스로 관리했다.
- Response도 동일하다.
/**
* KakaoPay API 요청 모델
*/
public class KakaoPayRequest {
@Getter
@Builder
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public static class OrderInfoRequest {
private String cid;// 가맹점 코드, 10자
private String tid;// 결제 고유번호, 20자
}
@Getter
@Builder
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public static class PayReadyRequest {
private String cid; // 가맹점 코드
private String partnerOrderId; // 가맹점 주문 번호
private String partnerUserId; // 가맹점 회원 ID
private String itemName; // 상품명
private String itemCode; // 상품 코드
private Integer quantity; // 상품 수량
private Integer totalAmount; // 상품 총액
private Integer taxFreeAmount; // 상품 비과세 금액
private String approvalUrl; // 결제 성공 시 redirect url
private String cancelUrl; // 결제 취소 시 redirect url
private String failUrl; // 결제 실패 시 redirect url
}
@Getter
@Builder
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public static class PayApproveRequest {
private String cid;// 가맹점 코드
private String tid; // 결제 고유 번호 (결제 준비 API 응답에 포함)
private String partnerOrderId; // 가맹점 주문 번호, 결제 준비 API 요청과 일치해야 함
private String partnerUserId; // 가맹점 회원 id, 결제 준비 API 요청과 일치해야 함
private String pgToken; // 결제승인 요청을 인증하는 토큰 사용자 결제 수단 선택 완료 시, approval_url로 redirection해줄 때 pg_token을 query string으로 전달
private Integer totalAmount; // 상품 총액, 결제 준비 API 요청과 일치해야 함
}
@Getter
@Builder
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public static class PayCancelRequest {
private String cid; // 가맹점 코드
private String tid; // 결제 고유번호
private Integer cancelAmount; // 취소 금액
private Integer cancelTaxFreeAmount; // 취소 비과세 금액
}
}
KakaoPayClientConfig
- 카카오페이 요청에 필요한 Webclient를 생성했다.
**
* 카카오페이 WebClient 생성
*/
@Configuration
@RequiredArgsConstructor
public class KakaoPayClientConfig extends WebClientConfig {
private final KakaoPayClientProperty property;
@Bean
public WebClient kakaoPayWebClient() {
return createWebClientFrame(property.getUrl(), property.getReadTime(), property.getConnectTime());
}
}
KakaoPayClient
- 직접 요청을 하는 객체
- WebClient는 비동기, 논블로킹 방식으로 사용하는 것이 옳지만, 나는 동기방식 코드 구성이므로 block()을 사용했다. (웬만하면 사용하지 마세요, 저는 단순히 RestTemplate이 Deprecated 되어서 사용했습니다)
/**
* kakaoPay Web Client
*/
@Component
@RequiredArgsConstructor
public class KakaoPayClient {
private final KakaoPayClientProperty kakaoPayClientProperty;
private final WebClient kakaoPayWebClient;
private final ObjectMapper objectMapper;
public OrderInfoResponse getOrderInfo(String uri, OrderInfoRequest orderInfoRequest) {
return kakaoPayWebClient.post()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(setHeaders()))
.bodyValue(ParamConverter.convert(objectMapper, orderInfoRequest))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new KakaoPayException(FAILED_POST.getMessage())))
.onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new KakaoPayException(FAILED_POST.getMessage())))
.bodyToMono(OrderInfoResponse.class)
.block();
}
public PayReadyResponse ready(String uri, KakaoPayRequest.PayReadyRequest payReadyRequest) {
return kakaoPayWebClient.post()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(setHeaders()))
.accept(MediaType.APPLICATION_JSON)
.bodyValue(ParamConverter.convert(objectMapper, payReadyRequest))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new KakaoPayException(FAILED_POST.getMessage())))
.onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new KakaoPayException(FAILED_POST.getMessage())))
.bodyToMono(PayReadyResponse.class)
.block();
}
public PayApproveResponse approve(String uri, PayApproveRequest payApproveRequest) {
return kakaoPayWebClient.post()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(setHeaders()))
.bodyValue(ParamConverter.convert(objectMapper, payApproveRequest))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new KakaoPayException(FAILED_POST.getMessage())))
.onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new KakaoPayException(FAILED_POST.getMessage())))
.bodyToMono(PayApproveResponse.class)
.block();
}
public PayCancelResponse cancel(String uri, PayCancelRequest payCancelRequest) {
return kakaoPayWebClient.post()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(setHeaders()))
.bodyValue(ParamConverter.convert(objectMapper, payCancelRequest))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new KakaoPayException(FAILED_POST.getMessage())))
.onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new KakaoPayException(FAILED_POST.getMessage())))
.bodyToMono(PayCancelResponse.class)
.block();
}
private HttpHeaders setHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.AUTHORIZATION, "KakaoAK " + kakaoPayClientProperty.getAdmin().getKey());
headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
return headers;
}
}
이렇게 해서 요청에 대한 Webclient 구성을 완료했다.
지금은 단순히 블로킹 방식으로 구현한 것이지만, 추후에 비동기 논블로킹으로 구현할 수 있는 수준으로 끌어올려야겠다.
반응형
'📕 Spring Framework > Spring Project' 카테고리의 다른 글
[Redisson] 트랜잭션 문제 발생 및 해결 (0) | 2022.10.01 |
---|---|
[Redisson]을 이용한 분산 Lock 구현 & 동시성 문제 해결 (2) | 2022.09.27 |
결제 API 리팩토링 - [1] (feat. 전략 패턴) (2) | 2022.09.20 |
동시성 조회 문제 해결 및 성능에 관한 고민 [Lock, Queue, Redis] (0) | 2022.09.14 |
객체 간 매핑을 위한 MapStruct 사용 방법 (0) | 2022.08.29 |