๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Web Development/Spring

[Spring] WebClient - 2. ์š”์ฒญ ํ•ธ๋“ค๋ง ๋ฐ API ๊ตฌ์„ฑ

by graycode 2025. 2. 26.

๐Ÿ”—[Spring] WebClient - 1. ๊ฐœ๋… ๋ฐ ๊ธฐ๋ณธ ์„ค์ •

์ด์ „ ์žฅ์—์„œ ๊ธฐ์ˆ ํ•œ WebClient ๊ธฐ๋ณธ ์„ค์ •์ด ์™„๋ฃŒ๋˜๊ณ  ๋‚˜๋ฉด,

์ด๋ฅผ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์š”์ฒญ์„ ๋ฐ›๊ณ  ํ•ธ๋“ค๋งํ•  ์ˆ˜ ์žˆ๋Š” API ๊ตฌ์„ฑ์ด ํ•„์š”ํ•˜๋‹ค.

 

๐Ÿ“ƒโ€‹ RequestHandler

๋จผ์ € ์‹ค์ œ HTTP ์š”์ฒญ์„ ์‹คํ–‰ํ•˜๊ณ  ํ•ธ๋“ค๋งํ•˜๋Š” ์ธํ”„๋ผ ๊ณ„์ธต ์„œ๋น„์Šค๋‹จ์„ ์ž‘์„ฑํ•œ๋‹ค.

@Service
@RequiredArgsConstructor
public class RequestHandler {

    private final ObjectMapper mapper;

    /**
     * WebClient GET ์š”์ฒญ ํ›„ ๊ฒฐ๊ณผ ๋ฌธ์ž์—ด ๋ฐ˜ํ™˜
     *
     * @param webClient WebClient
     * @param url String
     * @param params Map<String, String>
     * @return String
     */
    public String sendGetRequest(WebClient webClient, String url, Map<String, String> params) { // GET request
        MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
        queryParams.setAll(params);

        URI uri = UriComponentsBuilder
                .fromUriString(url)
                .queryParams(queryParams)
                .build().toUri();

        Mono<String> mono = webClient.get()
                .uri(uri)
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .onStatus(HttpStatusCode::is4xxClientError, response ->
                        Mono.error(new RuntimeException("URL : " + url + "] Bad Request - Error Code " + response.statusCode().value())))
                .onStatus(HttpStatusCode::is5xxServerError, response ->
                        Mono.error(new RuntimeException("URL : " + url + "] Internal Server Error - Error Code " + response.statusCode().value())))
                .bodyToMono(String.class);

        return mono.block();
    }

    /**
     * WebClient POST ์š”์ฒญ ํ›„ ๊ฒฐ๊ณผ ๋ฌธ์ž์—ด ๋ฐ˜ํ™˜
     *
     * @param webClient WebClient
     * @param url String
     * @param params Map<String, String>
     * @return String
     */
    public String sendPostRequest(WebClient webClient, String url, Map<String, String> params) { // POST request
        String body;
        try {
            body = mapper.writeValueAsString(params);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }

        URI uri = UriComponentsBuilder
                .fromUriString(url)
                .build().toUri();

        Mono<String> mono = webClient.post()
                .uri(uri)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(body))
                .retrieve()
                .onStatus(HttpStatusCode::is4xxClientError, response ->
                        Mono.error(new RuntimeException("URL : " + url + "] Bad Request - Error Code " + response.statusCode().value())))
                .onStatus(HttpStatusCode::is5xxServerError, response ->
                        Mono.error(new RuntimeException("URL : " + url + "] Internal Server Error - Error Code " + response.statusCode().value())))
                .bodyToMono(String.class);

        return mono.block();
    }

}

 

๐Ÿ“‘โ€‹ sendGetRequest

๋ธ”๋กœํ‚น ๋ฐฉ์‹์œผ๋กœ GET ์š”์ฒญ์„ ์ฒ˜๋ฆฌ ํ›„ ๋ฌธ์ž์—ด๋กœ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

public String sendGetRequest(WebClient webClient, String url, Map<String, String> params) { // GET request
    // MultiValueMap ์ƒ์„ฑ
    MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
    queryParams.setAll(params); // ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด, Map ์„ MultiValueMap ์œผ๋กœ ๋ณ€ํ™˜

    // URI ๊ตฌ์„ฑ
    URI uri = UriComponentsBuilder
            .fromUriString(url) // ๊ธฐ๋ณธ URL ์„ค์ •
            .queryParams(queryParams) // ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€
            .build().toUri(); // URI ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜

    // WebClient ์š”์ฒญ ๊ตฌ์„ฑ
    Mono<String> mono = webClient.get() // GET ๋ฉ”์†Œ๋“œ ์ง€์ •
            .uri(uri) // ์š”์ฒญ URI ์„ค์ •
            .accept(MediaType.APPLICATION_JSON) // Accept ํ—ค๋” ์„ค์ •
            .retrieve() // ์‘๋‹ต ์ถ”์ถœ ์‹œ์ž‘
            .onStatus(HttpStatusCode::is4xxClientError, response -> // 4xx ์—๋Ÿฌ ์ฒ˜๋ฆฌ
                    Mono.error(new RuntimeException("URL : " + url + "] Bad Request - Error Code " + response.statusCode().value())))
            .onStatus(HttpStatusCode::is5xxServerError, response -> // 5xx ์—๋Ÿฌ ์ฒ˜๋ฆฌ
                    Mono.error(new RuntimeException("URL : " + url + "] Internal Server Error - Error Code " + response.statusCode().value())))
            .bodyToMono(String.class); // ์‘๋‹ต ๋ณธ๋ฌธ์„ String ์œผ๋กœ ๋ณ€ํ™˜

    return mono.block(); // ๋น„๋™๊ธฐ ์‹คํ–‰์„ ๋ธ”๋กœํ‚นํ•˜์—ฌ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
}

 

์‘๋‹ต์„ ์ถ”์ถœ ์‹œ, retrieve ์™€ exchange ์ฒ˜๋ฆฌ ๋ฐฉ์‹์„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ๊ฐ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์˜ ์ฐจ์ด์ ์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

 

๐Ÿ“Œ retrieve

  • ๊ฐ„๋‹จํ•œ ์‘๋‹ต ์ฒ˜๋ฆฌ์— ์ ํ•ฉํ•˜๋ฉฐ ์‘๋‹ต ๋ณธ๋ฌธ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • ๊ฐ„๋‹จํ•˜๊ณ  ์•ˆ์ „ํ•œ API ๊ตฌ์„ฑ์— ์‚ฌ์šฉ
  • ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์ •์ 

๐Ÿ“Œ exchange

  • ์ „์ฒด ClientResponse ์— ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • ์‘๋‹ต ํ—ค๋”, ์ฟ ํ‚ค ๋“ฑ ์„ธ๋ถ€ ์ •๋ณด ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • ์ˆ˜๋™์œผ๋กœ ๋ฆฌ์†Œ์Šค ํ•ด์ œ ํ•„์š”
  • ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฐ€๋Šฅ์„ฑ ์กด์žฌ

์ผ๋ฐ˜์ ์œผ๋กœ retrieve ์‚ฌ์šฉ์„ ์šฐ์„  ๊ณ ๋ คํ•˜๊ณ , ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ exchange ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

exchange ์‚ฌ์šฉ ์‹œ, ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ํ•ญ์ƒ ์‘๋‹ต์„ ์†Œ๋น„ํ•˜๊ณ  doFinally ๋“ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฆฌ์†Œ์Šค๋ฅผ ์ •๋ฆฌํ•˜์—ฌ์•ผํ•œ๋‹ค.

 

โ€‹๐Ÿ“‘ sendPostRequest

๋ธ”๋กœํ‚น ๋ฐฉ์‹์œผ๋กœ POST ์š”์ฒญ์„ ์ฒ˜๋ฆฌ ํ›„ ๋ฌธ์ž์—ด๋กœ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

public String sendPostRequest(WebClient webClient, String url, Map<String, String> params) { // POST request
    // ์š”์ฒญ ๋ฐ”๋”” ์ƒ์„ฑ
    String body;
    try {
        body = mapper.writeValueAsString(params); // Map ์„ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
    } catch (JsonProcessingException e) {
        throw new RuntimeException(e);
    }

    // URI ๊ตฌ์„ฑ
    URI uri = UriComponentsBuilder
            .fromUriString(url) // ๊ธฐ๋ณธ URL ์„ค์ •
            .build().toUri(); // URI ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜

    Mono<String> mono = webClient.post() // POST ๋ฉ”์†Œ๋“œ ์ง€์ •
            .uri(uri) // ์š”์ฒญ URI ์„ค์ •
            .contentType(MediaType.APPLICATION_JSON) // Content-Type ํ—ค๋” ์„ค์ •
            .accept(MediaType.APPLICATION_JSON) // Accept ํ—ค๋” ์„ค์ •
            .body(BodyInserters.fromValue(body)) // ์š”์ฒญ ๋ฐ”๋”” ์„ค์ •
            .retrieve() // ์‘๋‹ต ์ถ”์ถœ ์‹œ์ž‘
            .onStatus(HttpStatusCode::is4xxClientError, response -> // 4xx ์—๋Ÿฌ ์ฒ˜๋ฆฌ
                    Mono.error(new RuntimeException("URL : " + url + "] Bad Request - Error Code " + response.statusCode().value())))
            .onStatus(HttpStatusCode::is5xxServerError, response -> // 5xx ์—๋Ÿฌ ์ฒ˜๋ฆฌ
                    Mono.error(new RuntimeException("URL : " + url + "] Internal Server Error - Error Code " + response.statusCode().value())))
            .bodyToMono(String.class); // ์‘๋‹ต ๋ณธ๋ฌธ์„ String ์œผ๋กœ ๋ณ€ํ™˜

    return mono.block(); // ๋น„๋™๊ธฐ ์‹คํ–‰์„ ๋ธ”๋กœํ‚นํ•˜์—ฌ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
}

 

๋…ผ ๋ธ”๋กœํ‚น ๋ฐฉ์‹์— ๋Œ€ํ•œ ํ•ธ๋“ค๋ง์˜ ๊ฒฝ์šฐ, ๋ฉ”์†Œ๋“œ์˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ String ์—์„œ Mono<String> ์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ ,

๋ฐ˜ํ™˜ ์‹œ block ์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๊ณ  Mono ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

// ๋ฐ˜ํ™˜ ํƒ€์ž…์„ Mono<String> ์œผ๋กœ ์ง€์ •
public Mono<String> sendNonBlockingRequest(WebClient webClient, String url, Map<String, String> params) { // POST request
    String body;
    try {
        body = mapper.writeValueAsString(params);
    } catch (JsonProcessingException e) {
        return Mono.error(e); // ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ Mono ๋กœ ๊ฐ์‹ธ ๋ฐ˜ํ™˜
    }

    URI uri = UriComponentsBuilder
            .fromUriString(url)
            .build().toUri();

    return webClient.post() // ์‘๋‹ต Mono ๊ฐ์ฒด๋ฅผ ๋ฐ”๋กœ ๋ฐ˜ํ™˜
            .uri(uri)
            .contentType(MediaType.APPLICATION_JSON)
            .accept(MediaType.APPLICATION_JSON)
            .body(BodyInserters.fromValue(body))
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, response ->
                    Mono.error(new RuntimeException("URL : " + url + "] Bad Request - Error Code " + response.statusCode().value())))
            .onStatus(HttpStatusCode::is5xxServerError, response ->
                    Mono.error(new RuntimeException("URL : " + url + "] Internal Server Error - Error Code " + response.statusCode().value())))
            .bodyToMono(String.class);
}

๐Ÿ“ƒ RequestService

๋‹ค์Œ์€ ์ง€์ •ํ•œ ์š”์ฒญ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์— ๋”ฐ๋ผ RequestHandler ๋ฅผ ํ†ตํ•ด ์‘๋‹ต์„ ๋ฐ›๊ณ ,

๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ตฌํ˜„ ๋ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๊ณต ๋ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌํ•˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๊ณ„์ธต ์„œ๋น„์Šค๋‹จ์„ ์ž‘์„ฑํ•œ๋‹ค.

@Service
@Slf4j
@RequiredArgsConstructor
public class RequestService {

    private final RequestHandler requestHandler; // ์š”์ฒญ ํ•ธ๋“ค๋ง ์„œ๋น„์Šค
    private final WebClientFactory webClientFactory; // WebClient ๋ฐ˜ํ™˜ ํŒฉํ† ๋ฆฌ
    private final UtilService utilService; // ์˜ˆ์‹œ์šฉ ๊ฐ€์ƒ ์„œ๋น„์Šค

    /**
     * ๋ธ”๋กœํ‚น POST ์š”์ฒญ
     *
     * @param url String
     * @param params Map<String, String>
     * @return String
     */
    public String blockRequest(String url, Map<String, String> params) {
        return requestHandler.sendPostRequest(webClientFactory.getWebClient(), url, params);
    }

    /**
     * ๋…ผ ๋ธ”๋กœํ‚น Mono POST ์š”์ฒญ
     *
     * @param url String
     * @param params Map<String, String>
     * @return Mono<String>
     */
    public Mono<String> monoRequest(String url, Map<String, String> params) {
        return requestHandler.sendNonBlockPostRequest(webClientFactory.getWebClient(), url, params)
                .flatMap(this::parseResponse) // ๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹ ๊ฐ€๋Šฅ
                .doOnSuccess(resp -> log.info("process completed: {}", resp))
                .doOnError(error -> log.info("error occurred: {}", error.getMessage()));
    }

    /**
     * ๋…ผ ๋ธ”๋กœํ‚น Subscribe POST ์š”์ฒญ
     *
     * @param url String
     * @param params Map<String, String>
     */
    public void subscribeRequest(String url, Map<String, String> params) {
        requestHandler.sendNonBlockPostRequest(webClientFactory.getWebClient(), url, params)
                .subscribe(
                        resp -> log.info("process completed: {}", resp),
                        error -> log.error("error occurred: {}", error.getMessage())
                );
    }

    /**
     * ์‘๋‹ต ์ฒ˜๋ฆฌ ๋ณด์กฐ ๋ฉ”์†Œ๋“œ
     *
     * @param resp String
     * @return Mono<String>
     */
    private Mono<String> parseResponse(String resp) {
        return utilService.parseResponse(resp) // ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๋Š” Mono.just(String) ๋กœ ๋ฐ˜ํ™˜
                .doOnSuccess(result -> log.info("data parsing completed: {}", result))
                .doOnError(error -> log.error("data parsing failed: {}", error.getMessage()));
    }

}

 

์œ„ ํด๋ž˜์Šค์˜ ๋ฉ”์†Œ๋“œ์ค‘ ๋…ผ ๋ธ”๋กœํ‚น ๋ฐฉ์‹ ๋ฉ”์†Œ๋“œ์˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์œผ๋กœ Mono ์™€ subscribe() ๋‘ ์ข…๋ฅ˜๋ฅผ ๋ช…์‹œํ•˜์˜€๋‹ค.

๊ฐ ๋ฐ˜ํ™˜ ํƒ€์ž…์˜ ์ฐจ์ด์ ์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

 

๐Ÿ“Œ Mono(Flux) ๋ฐ˜ํ™˜

  • ์ง€์—ฐ ์‹คํ–‰(lazy execution)
  • ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์„ฑ ๊ฐ€๋Šฅ
  • ๋‹ค๋ฅธ ๋ฆฌ์•กํ‹ฐ๋ธŒ ์—ฐ์‚ฐ์ž์™€ ์กฐํ•ฉ ๊ฐ€๋Šฅ
  • ์Šคํ”„๋ง WebFlux ์™€ ํ†ตํ•ฉ ์šฉ์ด

๐Ÿ“Œ subscribe()

  • ์ฆ‰์‹œ ์‹คํ–‰๋จ
  • void ๋ฐ˜ํ™˜
  • ๋ฉ”์†Œ๋“œ ๋‚ด์—์„œ ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋จ
  • ์ฃผ๋กœ ์ตœ์ข… ์†Œ๋น„์ž์—์„œ ์‚ฌ์šฉ

๐Ÿ“ƒ RequestController

๋์œผ๋กœ ํด๋ผ์ด์–ธํŠธ์˜ HTTP ์š”์ฒญ์„ ๋ฐ›์•„๋“ค์ด๋Š” ์•ค๋“œํฌ์ธํŠธ๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ‘œํ˜„ ๊ณ„์ธต ์ปจํŠธ๋กค๋Ÿฌ๋‹จ์„ ์ž‘์„ฑํ•œ๋‹ค.

@RequestMapping("/api")
@RestController
@RequiredArgsConstructor
public class RequestController {

    private final RequestService requestService;

    /**
     * ๋ธ”๋กœํ‚น POST ์š”์ฒญ
     *
     * @param req RequestDTO
     * @return String
     */
    @PostMapping("/post/block")
    public String blockRequest(@RequestBody RequestDTO req) {
        return requestService.blockRequest(req.getUrl(), req.getParams());
    }

    /**
     * ๋…ผ ๋ธ”๋กœํ‚น Mono POST ์š”์ฒญ
     *
     * @param req RequestBody
     * @return Mono<String>
     */
    @PostMapping("/post/mono")
    public Mono<String> monoRequest(@RequestBody RequestDTO req) {
        return requestService.monoRequest(req.getUrl(), req.getParams());
    }

    /**
     * ๋…ผ ๋ธ”๋กœํ‚น Subscribe POST ์š”์ฒญ
     *
     * @param req RequestBody
     * @return Mono<String>
     */
    @PostMapping("/post/subscribe")
    public ResponseEntity<Void> subscribeRequest(@RequestBody RequestDTO req) {
        requestService.subscribeRequest(req.getUrl(), req.getParams());
        return ResponseEntity.accepted().build(); // 202 Accepted, ์š”์ฒญ์€ ์ ‘์ˆ˜๋˜์—ˆ์œผ๋‚˜, ์ฒ˜๋ฆฌ๋Š” ์ถ”ํ›„์— ์™„๋ฃŒ
    }

}

 

๋งˆ์น˜๋ฉฐ

์ด์ฒ˜๋Ÿผ WebClient ๋Š” ๋น„๋™๊ธฐ ๋ฐฉ์‹์˜ HTTP ์š”์ฒญ์„ ๋ณด๋‹ค ์œ ์—ฐํ•˜๊ณ  ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค€๋‹ค.

์ƒ์ˆ ํ•œ ๊ธฐ๋Šฅ์™ธ์—๋„ retry, filter, ๋” ๊ตฌ์ฒด์ ์ธ ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ธฐ๋Šฅ๋“ค์„ ํ™œ์šฉํ•˜๋ฉด,
๋” ๋†’์€ ํ™•์žฅ์„ฑ๊ณผ ์•ˆ์ •์„ฑ์„ ๊ฐ–์ถ˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋Œ“๊ธ€