๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ“• Spring Framework/Spring Project

๊ฒฐ์ œ API ๋ฆฌํŒฉํ† ๋ง - [2] (feat. WebClient)

by GroovyArea 2022. 9. 22.

https://sweeeetgoguma.tistory.com/entry/%EA%B2%B0%EC%A0%9C-API-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-1-feat-%EC%A0%84%EB%9E%B5-%ED%8C%A8%ED%84%B

 

๊ฒฐ์ œ API ๋ฆฌํŒฉํ† ๋ง - [1] (feat. ์ „๋žต ํŒจํ„ด)

๊ฒฐ์ œ API๋ฅผ ๋ฆฌํŒฉํ† ๋ง ์‹œ์ž‘ํ•˜๋ฉฐ ์™ธ๋ถ€ API๋ฅผ ์—ฐ๋™ ๋ถ€๋ถ„์— ๋Œ€ํ•ด์„œ ์ƒ๊ฐํ•ด๋ดค๋‹ค. ๊ธฐ์กด์—๋„ ์นด์นด์˜คํŽ˜์ด๋ฅผ ์ด์šฉํ–ˆ์—ˆ๊ณ , ์ง€๊ธˆ๋„ ์นด์นด์˜คํŽ˜์ด๋ฅผ ์ด์šฉํ•  ๊ฒƒ์ด์ง€๋งŒ, ์ถ”๊ฐ€์ ์œผ๋กœ ๋‹ค๋ฅธ ๊ฒฐ์ œ API๋ฅผ ์—ฐ๋™ํ•  ์ˆ˜ ์žˆ

sweeeetgoguma.tistory.com

 

์ง€๋‚œ ํฌ์ŠคํŒ…์— ์ด์–ด์„œ ์ž‘์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค~

์‹ค์ œ ๊ฒฐ์ œ 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 ๊ตฌ์„ฑ์„ ์™„๋ฃŒํ–ˆ๋‹ค.

์ง€๊ธˆ์€ ๋‹จ์ˆœํžˆ ๋ธ”๋กœํ‚น ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•œ ๊ฒƒ์ด์ง€๋งŒ, ์ถ”ํ›„์— ๋น„๋™๊ธฐ ๋…ผ๋ธ”๋กœํ‚น์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์œผ๋กœ ๋Œ์–ด์˜ฌ๋ ค์•ผ๊ฒ ๋‹ค. 

๋ฐ˜์‘ํ˜•