부트 개념과 활용-32.RestTemplate과 WebClient, 커스터마이징(스프링 REST 클라이언트)


스프링 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

구현 코드(RestTemplate)

@RestController
public class SampleController {

    @GetMapping("/hello")
    public String hello() throws InterruptedException {
        Thread.sleep(5000l);
        return "hello";
    }

    @GetMapping("/world")
    public String world() throws InterruptedException {
        Thread.sleep(3000l);
        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();

        // TODO /hello
        String helloResult = restTemplate.getForObject("http://localhost:8080/hello", String.class);
        System.out.println(helloResult);

        // TODO /world
        String worldResult = restTemplate.getForObject("http://localhost:8080/world", String.class);
        System.out.println(worldResult);

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

실행결과

hello
world
StopWatch '': running time = 8135775176 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
8135775176  100%  

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

실습 코드(WebClient)

의존성 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
@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();

        // TODO /hello
        Mono<String> helloMono = webClient.get().uri("http://localhost:8080/hello")
                .retrieve()
                .bodyToMono(String.class);
        // 이 상태까지는 진행되지 않음.(Non-Blocking)

        // subscribe 해야 진행 (Asynchronous 처리
        helloMono.subscribe(s -> {
            System.out.println("helloMono : " + s);

            if(stopWatch.isRunning()) {
                stopWatch.stop();
            }
            System.out.println(stopWatch.prettyPrint());
            stopWatch.start();
        });

       // TODO /world
        Mono<String> worldMono = webClient.get().uri("http://localhost:8080/world")
                .retrieve()
                .bodyToMono(String.class);

        worldMono.subscribe(s -> {
            System.out.println("worldMono : " + s);

            if(stopWatch.isRunning()) {
                stopWatch.stop();
            }
            System.out.println(stopWatch.prettyPrint());
            stopWatch.start();
        });

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

실행결과

StopWatch '': running time = 151808503 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
151808503  100%  

2019-12-25 19:55:54.658  INFO 34203 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2019-12-25 19:55:54.659  INFO 34203 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2019-12-25 19:55:54.662  INFO 34203 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 3 ms
worldMono : world
StopWatch '': running time = 151808503 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
151808503  100%  

helloMono : hello
StopWatch '': running time = 2113159529 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
151808503  007%  
1961351026  093%  

world가 먼저 찍힌 것을 볼 수 있다.

앞에서 thread sleep을 준 게 world 3초 / hello 5초
world 는 3초만 기다리면 되니까 먼저 찍힌걸 확인할 수 있다.

즉, 코드는 쭉 흘러가고(Non-Blocking 기반),

3초가 지난 후에 worldMono 응답이 도착해서, 스탑워치가 정지되고 찍힌 후, 다시 스타트.

그 이후 2초가 지난 후에 응답이 도착해서(helloMono) hello가 찍히고 스탑워치가 출력되는 걸 볼 수 있다.

RestTemplate vs WebClient

RestTemplate보다는 WebClient를 사용하는 것을 더 추천.
여러가지 API 호출 후 조합하는 것도 가능하고, 유연하게 활용이 가능하다. (+ 시간적인 측면)
a,b,c 호출 및 a,b를 호출해서 받은 값을 c에 활용한다던가.. 등

만약 호출이 단순하거나, 사용하는 호출이 적다면 RestTemplate을 사용해도 큰 차이는 없다.

스프링 REST 클라이언트 2부: 커스터마이징

RestTemplate

  • 기본으로 java.net.HttpURLConnection 사용.
  • 커스터마이징
    • 로컬 커스터마이징
    • 글로벌 커스터마이징
      • RestTemplateCustomizer
      • 빈 재정의
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>
    @Bean
    public RestTemplateCustomizer restTemplateCustomizer(){
        return restTemplate -> {
            restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
            // java.net 의 http 커넥션이 아닌 아파치 http 클라이언트를 사용
        };
    }

WebClient

  • 기본으로 Reactor Netty의 HTTP 클라이언트 사용.
  • 커스터마이징
    • 로컬 커스터마이징
    • 글로벌 커스터마이징
      • WebClientCustomizer
      • 빈 재정의

지역적인 커스터마이징

    @Override
    public void run(ApplicationArguments args) throws Exception {
        WebClient webClient = builder
                .baseUrl("http://localhost:8080") // base url 설정 (지역적인 커스터마이징)
                .build();

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

        // TODO /hello
        Mono<String> helloMono = webClient.get().uri("/hello") // base url +
                .retrieve()
                .bodyToMono(String.class);
        // 이 상태까지는 진행되지 않음.(Non-Blocking)

        // subscribe 해야 진행 (Asynchronous 처리
        helloMono.subscribe(s -> {
            System.out.println("helloMono : " + s);

            if(stopWatch.isRunning()) {
                stopWatch.stop();
            }
            System.out.println(stopWatch.prettyPrint());
            stopWatch.start();
        });

       // TODO /world
        Mono<String> worldMono = webClient.get().uri("/world") // base url +
                .retrieve()
                .bodyToMono(String.class);

        worldMono.subscribe(s -> {
            System.out.println("worldMono : " + s);

            if(stopWatch.isRunning()) {
                stopWatch.stop();
            }
            System.out.println(stopWatch.prettyPrint());
            stopWatch.start();
        });

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

전역적인 커스터마이징

모든 빌더는 기본적으로 base url이 세팅된 상태로 다른 빈들에 주입이 된다.

@SpringBootApplication
public class RestApplication {

    public static void main(String[] args) {
        SpringApplication.run(RestApplication.class, args);
    }

    @Bean
    public WebClientCustomizer webClientCustomizer(){
        return new WebClientCustomizer() {
            @Override
            public void customize(WebClient.Builder webClientBuilder) {
                webClientBuilder.baseUrl("http://localhost:8080");
            }
        };
    }
}

//-> 람다 변환(동일)

    @Bean
    public WebClientCustomizer webClientCustomizer(){
        return webClientBuilder -> webClientBuilder.baseUrl("http://localhost:8080");
    }





© 2019. by jaeuk