WebClient

smallcherry's techlogยท2022๋…„ 12์›” 5์ผ
0

๐Ÿ’ก ์ฑ—๋ด‡2 ์ŠคํŠœ๋””์˜ค, ์—”์ง„ ํŒŒ์ผ์„œ๋ฒ„ ์—ฐ๋™ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์ค‘ ์™ธ๋ถ€ ์„œ๋ฒ„๋กœ API ๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ ์œ„ํ•ด ๊ณต๋ถ€

WebClient

๊ฐœ์š”

  • ์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์ œ๊ณตํ•˜๋Š” HTTP ํ†ต์‹ ์„ ์œ„ํ•œ ๋น„๋™๊ธฐ ํด๋ผ์ด์–ธํŠธ
  • HTTP ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์‘๋‹ต์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์คŒ.

๋™์ž‘ ํŠน์ง•

  • Non-Blocking ๋ฐฉ์‹: ์š”์ฒญ ๋ณด๋‚ธ ์ดํ›„์—๋„ ์ œ์–ด๊ถŒ์„ ๋นผ์•—๊ธฐ์ง€ ์•Š๊ณ  ์ดํ›„ ๋กœ์ง์„ ์ˆ˜ํ–‰.
  • ๋น„๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘: ๋„คํŠธ์›Œํฌ ์š”์ฒญ์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ํ”„๋กœ๊ทธ๋žจ์˜ ์‹คํ–‰์ด ์ค‘๋‹จ๋˜์ง€ ์•Š๋„๋ก ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Œ.
  • ์Šค๋ ˆ๋“œ๋‚˜ ๋™์‹œ์„ฑ๊ณผ ๊ด€๋ จ๋œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ํ•„์š” ์—†์ด ๋น„๋™๊ธฐ ๋กœ์ง ๊ตฌ์„ฑ์ด ๊ฐ€๋Šฅํ•จ.
  • Reactor์— ๊ธฐ๋ฐ˜ํ•œ API ์ œ๊ณตํ•˜๋ฉฐ,
  • ์™ธ๋ถ€ API๋กœ ์š”์ฒญ์„ ํ•  ๋•Œ ์‘๋‹ต ๊ฒฐ๊ณผ๋ฅผ Mono์™€ Flux๋กœ ๋ฐ˜ํ™˜ํ•จ

Mono, Flux

  • WebFlux API์—์„œ ์ฃผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๋ฐ์ดํ„ฐ return ํƒ€์ž…
  • Reactor (Reactive Streams library)์—์„œ ์ œ๊ณตํ•˜๋Š” ํด๋ž˜์Šค
  • ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๊ฑด์ˆ˜์— ๋”ฐ๋ผ ๋‚˜๋‰จ
    • 0~1๊ฐœ์˜ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ โ‡’ Mono
    • 0~N๊ฐœ์˜ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ โ‡’ Flux
  • ๋‘˜์„ ์™œ ๋‚˜๋ˆ ๋†จ์„๊นŒ? ๋ฐ์ดํ„ฐ ์„ค๊ณ„๋ฅผ ํ• ๋•Œ ๊ฒฐ๊ณผ๊ฐ€ ์—†๊ฑฐ๋‚˜ ํ•˜๋‚˜์˜ ๊ฒฐ๊ณผ๊ฐ’๋งŒ ๋ฐ›๋Š” ๊ฒƒ์ด ๋ช…๋ฐฑํ•œ ๊ฒฝ์šฐ, List๋‚˜ ๋ฐฐ์—ด์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ฒ˜๋Ÿผ, Multi Result๊ฐ€ ์•„๋‹Œ ํ•˜๋‚˜์˜ ๊ฒฐ๊ณผ์…‹๋งŒ ๋ฐ›๊ฒŒ ๋  ๊ฒฝ์šฐ์—๋Š” ๋ถˆํ•„์š”ํ•˜๊ฒŒ Flux๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  Mono๋ฅผ ์‚ฌ์šฉ.
  • ์ž์„ธํ•œ ๋™์ž‘ ๋ฐฉ์‹

Spring WebFlux

  • WebClient๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ
  • ์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ Reactive-stack ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ œ์ž‘์„ ์œ„ํ•ด ์ œ๊ณตํ•˜๋Š” ์›น ํ”„๋ ˆ์ž„์›Œํฌ
    • Reactive-stack ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜: Reactive Streams API์— ๊ธฐ๋ฐ˜ํ•œ Non-Blocking ์„œ๋ฒ„์—์„œ ๊ตฌ๋™๋˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
      • Reactive Streams API
        • ์š”์ฒญ ์ธก์—์„œ ์š”์ฒญ ๋ฐ›์€ ์ธก์˜ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์†๋„๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•จ
        • blocking ํ˜•ํƒœ์—์„œ๋Š” waiting์„ ๊ฐ•์ œํ•˜๋Š” ๊ฒƒ์ด ์ž์—ฐ์Šค๋Ÿฌ์šด ๊ฒƒ์ฒ˜๋Ÿผ, non-blocking ํ˜•ํƒœ์—์„œ๋Š” producer๊ฐ€ consumer๋ฅผ ์•ž์ง€๋ฅด์ง€ ์•Š๋„๋ก ์ด๋ฒคํŠธ์˜ ์†๋„๋ฅผ ์ œ์–ดํ•˜๋Š” ๊ฒƒ (Back Pressure)์ด ์ค‘์š”ํ•จ
          • consumer๊ฐ€ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ์˜ ์–‘ ๋งŒํผ๋งŒ producer์—๊ฒŒ ์š”์ฒญ
  • ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜ ๋น„๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•  ๋•Œ WebFlux๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ ์ ˆ
    • ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ์€ ๋ญ”๋ฐ?
      • ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ ์ŠคํŠธ๋ฆผ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋ฐฉ์‹
        • ๋ฐ์ดํ„ฐ๋ฅผ ์ŠคํŠธ๋ฆผ์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค๋Š” ๊ฒƒ์€?
          • ๋ฐ์ดํ„ฐ๋ฅผ ์š”์†Œ๋ณ„๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ณ , ์ด๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ๊ณ , ์š”์†Œ๋“ค์„ ๋ณ‘๋ ฌ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธ
      • ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ (๋น„๋™๊ธฐ ๋ฐฉ์‹์˜ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ)
  • ๋น„๋™๊ธฐ ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ œ์ž‘์— ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” FW!!

์‚ฌ์šฉ๋ฒ•

์˜์กด์„ฑ ์ถ”๊ฐ€

  • Gradle Dependency
    dependencies {
        compile 'org.springframework.boot:spring-boot-starter-webflux'
    }

Create: ์ƒ์„ฑ

  • create()
    • default ์„ธํŒ…/์š”์ฒญํ•  uri๋กœ ํด๋ผ์ด์–ธํŠธ๋ฅผ ๋ฐ”๋กœ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ

    • WebClient์˜ default ์„ธํŒ…์œผ๋กœ ์•„๋ž˜์™€ ๊ฐ™์ด ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜, ์š”์ฒญํ•  uri๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„ฃ์–ด์„œ ์ƒ์„ฑ

      WebClient.create();
      WebClient.create("http://localhost:8080");
  • build()
    • ๋ชจ๋“  ์„ค์ •์„ ์ปค์Šคํ…€ํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ

    • Customization Options
      - uriBuilderFactory: base url์„ ์ปค์Šคํ…€ํ•œ UriBuilderFactory
      - defaultUriVariables: uri ํ…œํ”Œ๋ฆฟ์„ ๋ฐฐํฌํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋””ํดํŠธ ๊ฐ’
      - defaultCookie: ๋ชจ๋“  ์š”์ฒญ์— ์‚ฌ์šฉํ•  ์ฟ ํ‚ค
      - defaultHeader: ๋ชจ๋“  ์š”์ฒญ์— ์‚ฌ์šฉํ•  ํ—ค๋”
      - defaultRequest: ๋ชจ๋“  ์š”์ฒญ์„ ์ปค์Šคํ…€ํ•  Consumer
      - filter: ๋ชจ๋“  ์š”์ฒญ์— ์‚ฌ์šฉํ•  ํด๋ผ์ด์–ธํŠธ ํ•„ํ„ฐ
      - exchangeStrategies: HTTP ๋ฉ”์‹œ์ง€ reader, writer ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
      - clietConnector: HTTP ํด๋ผ์ด์–ธํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ธํŒ…

      WebClient client = WebClient.builder()
        .baseUrl("http://localhost:8080")
        .defaultCookie("cookieKey", "cookieValue")
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 
        .defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
        .build();

Configuration: ์ •๋ณด ์„ค์ •

  • Timeout
    • Timeout๊ณผ ๊ด€๋ จ๋œ ์„ค์ • ์ •๋ณด๋ฅผ ์„ค์ •ํ•œ HttpClient ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด WebClient์— ์ฃผ์ž…

      HttpClient httpClient = HttpClient.create()
      	.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
      	.responseTimeout(Duration.ofMillis(5000))
      	.doOnConnected(conn -> 
      		conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
      		.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));
      
      WebClient client = WebClient.builder()
      	.clientConnector(new ReactorClientHttpConnector(httpClient))
      	.build();
    • ์ฝ”๋“œ ์„ค๋ช…: Connect Timeout๊ณผ ReadTimeout, WriteTimeout์„ ๋ชจ๋‘ 5000ms๋กœ ์ง€์ •ํ•œ HttpClient๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด WebClient์— ์ฃผ์ž… ํ•œ ์˜ˆ์‹œ.

  • mutate()
    • WebClient๋Š” ํ•œ ๋ฒˆ ๋นŒ๋“œํ•œ ๋’ค ๋ถ€ํ„ฐ๋Š” immutable ํ•œ๋ฐ, WebClient๋ฅผ ์‹ฑ๊ธ€ํ„ด์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ์ƒํ™ฉ์—์„œ default setting๊ณผ ๋‹ค๋ฅด๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ mutate๋ฅผ ํ†ตํ•ด ๋‹ค๋ฅธ ์„ค์ •๊ฐ’์„ ๊ฐ€์ง€๋Š” ์š”์ฒญ์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

      mutable: ๊ฐ’์ด ๋ณ€ํ•  ์ˆ˜ ์žˆ๋Š”
      immutable: ๊ฐ’์ด ๋ณ€ํ•˜์ง€ ์•Š๋Š”

    • client1๊ณผ client2๊ฐ€ ๊ฐ€๋ฆฌํ‚ค๋Š” ๊ฐ์ฒด๋Š” ๊ฐ™์€ WebClient ์ธ์Šคํ„ด์Šค์ง€๋งŒ, mutate()๋ฅผ ์ด์šฉํ•ด ์„œ๋กœ ๋‹ค๋ฅธ ์„ค์ •๊ฐ’์„ ๊ฐ€์ง€๋Š” ์š”์ฒญ์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

    • ๊ฐ™์€ ๊ฐ์ฒด์— client1๊ณผ client2์— ๋‹ค๋ฅธ ์˜ต์…˜๊ฐ’์„ ํ”„๋ฆฌ์…‹ ํ•ด๋‘๋Š” ๋Š๋‚Œ

      WebClient client1 = WebClient.builder()
              .filter(filterA).filter(filterB).build();
      
      WebClient client2 = client1.mutate()
              .filter(filterC).filter(filterD).build();
      
      // client1 has filterA, filterB
      // client2 has filterA, filterB, filterC, filterD

Request: ์š”์ฒญ ๋ณด๋‚ด๊ธฐ

  • GET
    • Flux [GET] /employees Request: collection of employees as Flux
      @Autowired
      WebClient webClient;
      
      public Flux<Employee> findAll() {
      	return webClient.get()
      		.uri("/employees")
      		.retrieve() // body๋ฅผ ๋ฐ›์•„ ๋””์ฝ”๋”ฉํ•˜๋Š” ๋ฉ”์„œ๋“œ
      		.bodyToFlux(Employee.class);
      }
    • Mono [GET] /employees/{id} Request: single employee by id as Mono
      @Autowired
      WebClient webClient;
      
      public Mono<Employee> findById(Integer id) {
      	return webClient.get()
      		.uri("/employees/" + id)
      		.retrieve()
      		.bodyToMono(Employee.class);
      }
  • POST
    • [POST] /employees

    • Request: creates a new employee from request body

    • Response: returns the created employee

      @Autowired
      WebClient webClient;
      
      public Mono<Employee> create(Employee empl) {
      	return webClient.post()
      		.uri("/employees")
      		.body(Mono.just(empl), Employee.class) // body์— ๋‹ด์•„ ์š”์ฒญํ•  ๋ฐ์ดํ„ฐ์™€ ๊ทธ ํƒ€์ž…์„ ๋ช…์‹œ
      		.retrieve()
      		.bodyToMono(Employee.class); // POST ํ•˜๊ณ  ๋ฐ›์„ body๊ฐ€ ์—†๋‹ค๋ฉด ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ Void.class๋ฅผ ์ž…๋ ฅ
      }

Response: ์‘๋‹ต ๋ฐ›๊ธฐ

  • retrieve()
    • toEntity()
      • status, headers, body๋ฅผ ํฌํ•จํ•˜๋Š” ResponseEntity ํƒ€์ž…์œผ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ

        Mono<ResponseEntity<Employee>> entityMon = client.get();
        	.uri("/employee/1")
        	.accept(MediaType.APPLICATION_JSON)
        	.retreive()
        	.toEntity(Employee.class);
    • toMono(), toFlux()
      • body์˜ ๋ฐ์ดํ„ฐ๋กœ๋งŒ ๋ฐ›๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉ

        Mono<Employee> entityMono = client.get()
        	.uri("/employee/1")
        	.accept(MediaType.APPLICATION_JSON)
        	.retreive()
        	.bodyToMono(Employee.class);
  • exchangeToMono(), exchangeToFlux()
    Mono<Employee> entityMono = client.get()
    	.uri("/employee/1")
    	.accept(MediaType.APPLICATION_JSON)
    	.exchangeToMono(resonse -> {
    		if (response.statusCode().equals(HttpStatus.OK)) {
    			return response.bodyToMono(Employee.class);
    		}	else {
    				return response.createException().flatMap(Mono::error);
    		}
    	})
  • Mono<>, Flux<> ์‚ฌ์šฉ
    • Blocking์œผ๋กœ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ•˜๋Š” ๊ฒฝ์šฐ
      • block()์œผ๋กœ blocking ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌ

        Mono<Employee> employeeMono = webClient.get()
        	/* ์ค‘๋žต */
        
        employeeMono.block();
    • Non-Blocking์œผ๋กœ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ•˜๋Š” ๊ฒฝ์šฐ
      • .subscribe() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ฝœ๋ฐฑํ•จ์ˆ˜ ์ง€์ •
        - ์ฝœ๋ฐฑํ•จ์ˆ˜: ์–ด๋–ค ์ด๋ฒคํŠธ์— ์˜ํ•ด ํ˜ธ์ถœ๋˜์–ด์ง€๋Š” ํ•จ์ˆ˜

        Mono<Employee> employssFlux = webClient.get()
        	/* ์ค‘๋žต */
        
        employeeFlux.subscribe(employee -> { ... });	

ErrorHandling

์—๋Ÿฌ ํ•ธ๋“ค๋ง์€ ๊ฒฐ๊ณผ ๊ฐ’์„ ๋ฐ˜ํ™˜๋ฐ›์„ ๋•Œ์˜ ์ƒํ™ฉ์— ๋”ฐ๋ผ ์ ์ ˆํžˆ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

  • retreive()
    • .onStatus() ์ด์šฉํ•ด์„œ StatusCode ๋ณ„๋กœ ํ•ธ๋“ค๋ง

      Mono<Employee> result = client.get()
      	.uri("/employee/{id}", id)
      	.accept(MediaType.APPLICATION_JSON)
      	.retreive()
      	.onStatus(HttpStatus::is4xxClientError, response -> ...)
      	.onStatus(HttpStatus::is5xxServerError, response -> ...)
      	.bodyToMono(Employee.class);
  • exchangeToMono(), exchangeToFlux()
    • exchange๋ฅผ ํ†ตํ•ด ๋ฐ›์€์‘๋‹ต์— ๋Œ€ํ•œ statusCode๋ฅผ ์ด์šฉํ•ด ๋ถ„๊ธฐ์ฒ˜๋ฆฌํ•˜์—ฌ ํ•ธ๋“ค๋ง

      Mono<Object> enriryMono = client.get()
      	.uri("/persons/1")
      	.accept(MediaType.APPLICATION_JSON)
      	.exchangeToMono(response -> {
      		if (response.statusCode().equals(HttpStatus.OK)) {
      			return response.bodyToMono(Employee.class);
      		}	else if (response.statusCode().is4xxClientError()){
      				return response.bodyToMono(ErrorContainer.class);
      		} else {
      				return Mono.error(response.createException());
      		}
      	})

์‹ฌํ™” ํ•™์Šต ์ž๋ฃŒ

Reactive Stack

Web on Reactive Stack

WebClient์ด์šฉํ•˜์—ฌ Non-Blocking์œผ๋กœ ๊ฐœ์„ ํ•˜๊ธฐ

๋น„ํšจ์œจ์ ์ธ Blocking ์ฝ”๋“œ๋ฅผ WebClient๋ฅผ ํ†ตํ•ด ๊ฐœ์„ ํ•˜๊ธฐ

Reference

Web on Reactive Stack

[Spring] WebFlux์˜ ๊ฐœ๋… / Spring MVC์™€ ๊ฐ„๋‹จ๋น„๊ต

[WebFlux] Mono์™€ Flux ๊ฐœ๋…๊ณผ ๊ตฌํ˜„

๋น„ํšจ์œจ์ ์ธ Blocking ์ฝ”๋“œ๋ฅผ WebClient๋ฅผ ํ†ตํ•ด ๊ฐœ์„ ํ•˜๊ธฐ

profile
Java Developer

0๊ฐœ์˜ ๋Œ“๊ธ€