[스프링 REST 클라이언트]_01_RestTemplate과 WebClient

2 minute read

스프링 REST 클라이언트 1부: RestTemplate과 WebClient

RestTemplate

  • Blocking I/O 기반의 Synchronous API
  • RestTemplateAutoConfiguration
  • 프로젝트에 spring-web 모듈이 있다면 RestTemplateBuilder를 빈으로 등록해 줍니다.
  • https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#rest-client-access

WebClient

  • Non-Blocking I/O 기반의 Asynchronous API
  • WebClientAutoConfiguration
  • 프로젝트에 spring-webflux 모듈이 있다면 WebClient.Builder를 빈으로 등록해 줍니다.
  • https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-client

※ 스프링이 제공하는 기능으로 스프링부트는 빈으로 등록해주기만 한다. RestTemplate과 WebClient를 빈으로 등록해주는 것이 아니라 Builder를 등록해주는 것. 따라서 빌더를 주입 받아서 필요할 때마다 빌드를 해서 인스턴스를 생성해서 사용해야한다.

  • RestTemplate과 WebClient 둘을 동시에 사용해도 된다.

RestTemplate

  • Blocking I/O 기반의 Synchronous API

(예제)

/hello 요청은 5초가 걸리고 /world 요청은 3초가 걸릴 때, /hello 요청과 /world 요청을 순서대로 호출해서 걸리는 시간을 확인

@RestController
public class SampleController {

    @GetMapping("/hello")
    private String hello() throws InterruptedException {
        Thread.sleep(5000l); //5초 슬립
        return "hello";
    }

    @GetMapping("/world")
    private String world() throws InterruptedException {
        Thread.sleep(3000l); //3초 슬립
        return "world";
    }
}
@Component
public class RestRunner implements ApplicationRunner {

    @Autowired
    RestTemplateBuilder restTemplateBuilder;


    @Override
    public void run(ApplicationArguments args) throws Exception {
        RestTemplate restTemplate = restTemplateBuilder.build();

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        String helloResult = restTemplate.getForObject("http://localhost:8080/hello", String.class); //이 요청이 끝날 때까지 다음 라인으로 넘어가지 않는다. // 5초 슬립이 걸려있다.
        System.out.println(helloResult);

        String worldResult = restTemplate.getForObject("http://localhost:8080/world", String.class); //이 요청이 끝날 때까지 다음 라인으로 넘어가지 않는다. // 3초 슬립이 걸려있다.
        System.out.println(worldResult);

        stopWatch.stop();
        System.out.println(stopWatch.prettyPrint());
    }
}

→ 한 라인의 명령이 끝날 때까지 기다리면서 다음라인을 순차적으로 진행한다.

결과 : 총 8초의 시간이 걸림

2021-01-06 11:42:21.907  INFO 13385 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
hello
world
StopWatch '': running time = 8103483462 ns //<-- 총 8초의 시간이 걸림
---------------------------------------------
ns         %     Task name
---------------------------------------------
8103483462  100%  

WebClient

  • Non-Blocking I/O 기반의 Asynchronous API
Mono<String> helloMono = webClient.get().uri("http://localhost:8080/hello") //요청
        .retrieve() //결과값을 가져옴
        .bodyToMono(String.class); //Body값을 Mono 타입으로 변경

MonoFlux에 대한 공부 필요

이건 Streaming API인데, Steam을 Subscribe하기 전까지 흐르지 않는다. 위의 코드는 물이 흐르지 않고, 물을 어딘가에 담아서 둔 것 뿐이다.

subscribe()를 해야 실제 물이 흐르게 되어있다.

실제 요청을 보내는 subscribe() 코드도 Non-Blocking이다. s → {} 코드에서 {}안의 코드가 Asynchronous하게 응답이 오면 그 제서야 코드가 실행된다. 응답처리를 기다리지 않음

// 실제 요청을 보내는 코드 - 이 코드도 Non-Blocking이다.
helloMono.subscribe(s -> {
	  //subscribe()는 결과값이 String으로 나온다. 결과값을 출력하고 stropwatch를 잠시 멈추고 출력 후 재시작
    System.out.println(s);

    if (stopWatch.isRunning()) {
        stopWatch.stop();
    }

    System.out.println(stopWatch.prettyPrint());
    stopWatch.start();
});

(예제)

/hello 요청은 5초가 걸리고 /world 요청은 3초가 걸릴 때, /hello 요청과 /world 요청을 순서대로 호출해서 걸리는 시간을 확인

@Component
public class RestRunner implements ApplicationRunner {

    @Autowired
    WebClient.Builder builder;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        WebClient webClient = builder.build();

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        Mono<String> helloMono = webClient.get().uri("http://localhost:8080/hello")
                .retrieve()
                .bodyToMono(String.class);
        helloMono.subscribe(s -> {
            System.out.println(s);

            if (stopWatch.isRunning()) {
                stopWatch.stop();
            }

            System.out.println(stopWatch.shortSummary());
            stopWatch.start();
        });

        Mono<String> worldMono = webClient.get().uri("http://localhost:8080/world")
                .retrieve()
                .bodyToMono(String.class);
        worldMono.subscribe(s -> {
            System.out.println(s);

            if (stopWatch.isRunning()) {
                stopWatch.stop();
            }

            System.out.println(stopWatch.shortSummary());
            stopWatch.start();
        });
    }
}
world
StopWatch '': running time = 3344120450 ns // word는 3초 슬립이기 때문에 먼저 처리돼서 출력된다.
hello
StopWatch '': running time = 5314634377 ns // 그 후, 5초 슬립걸린 hello 출력

⇒ WebClient를 사용하면,

다양하게 여러가지 API를 호출할 때, API 호출을 조합할 수 있다.

ex) A api, B api, C api 를 모두 호출하고, 모든 응답이 다 왔을 경우에 다른 로직을 처리한다.

​ 또는 A, B 응답이 오면 그 때 A, B 응답을 가지고 요청을 만들어서 C api로 요청을 보내서 처리한다.

유연하게 API Client를 만들어서 쓸 수 있다. 따라서 WebClient를 추천!! (하지만 공부 필요)

다양한 호출이 없는 경우, RestTemplate을 쓰는 것도 지장이 없으므로 요구사항에 맞게 선택해서 사용하자


질문

이전에는 비동기로 처리하기위해 @async 어노테이션으로 새로운 함수를 만들고 그 함수 내에서 client로 요청했는데, 이렇게 하는 것과 webclient 를 사용하는 것에는 어떤 차이가 있나요?

@Async와 RestTemplate을 사용하면 별도의 쓰레드 풀로 Blocking I/O를 처리하기 때문에 결과적으로 WebClient를 사용하는 것과 같은 효과를 낼 수 있습니다. 차이는 @Async용 쓰레드풀 설정이 필요하다는 점과 코딩 스타일이 다를 뿐입니다.