spring boot/기술 적용

webClient 와 feignClient

ballde 2022. 10. 10. 13:03

Deprecated 된 rest template를 대체하는 HTTP 클라이언트

블로그에 쓰는 글들은 여기에 있습니다.(정리가 좀 필요할 것 같지만)

https://github.com/YuSunjo/spring-boot-example

feignClient

  • interface를 작성하고 기존의 spring boot 처럼 사용해서 복잡도가 낮고 은근 친숙함
  • netflix에서 만들어졌다고 하네요.
  • 요즘 MSA에서 많이 사용한다고 합니다. (제대로 해보지 않아서 ㅠ)

import

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:2021.0.0"
    }
}

dependencies {
		...
    // feign client
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
    implementation group: 'io.github.openfeign', name: 'feign-gson', version: '11.0'
		...
}

config

@EnableFeignClients(basePackageClasses = PlatformApplication.class)
@Configuration
public class FeignClientConfig {
}
  • EnableFeignClients 저거를 @SpringBootApplication 여기에 해도 상관 없습니다.

controller

@RestController
@RequiredArgsConstructor
public class FeignController {

    private final FeignService feignService;

    @GetMapping("/feign/ping")
    public String pong() {
        return feignService.ping();
    }

}

service

@Service
@RequiredArgsConstructor
public class FeignService {

    private final TestFeignClient testFeignClient;

    public String ping() {
        return testFeignClient.ping();
    }

}

TestFeignClient

@FeignClient(
        name = "testFeignClient",
        url = "${test.blog_url}",
        configuration = FeignClientConfig.class)
public interface TestFeignClient {

    @GetMapping(value = "ping", produces = "application/json")
    String ping();

}
  • test.blog_url은 resources에 application.yml에 설정해주면 됨

나머지 예시들

// 파라미터
@GetMapping(value = "{url}/lecture/{lectureId}", produces = "application/json")
TestResponse test(@PathVariable String url, @PathVariable String lectureId);

// 헤더
@GetMapping(value = "...", produces = "application/json")
TestResponse test(@RequestHeader(HttpHeaders.AUTHORIZATION) String accessToken);

// 쿼리 스트링
@GetMapping(value = "...", produces = "application/json")
TestResponse test(@RequestParam String access_token);

// 바디
@GetMapping(value = "...", produces = "application/json")
TestResponse test(@RequestBody AppleAccessTokenRequest accessTokenRequest);

지금은 간단한 ping 만 가져오는 것을 해봤습니다. (나중에 소셜 로그인 정리할 때 feign client 사용해서 해보겠습니다.)

그 외에 쿼리 스트링, 헤더, 바디 등 넣을 수 있다!!

webClient

  • 논블로킹 및 비동기 접근 방식 지원
  • 빌더 방식의 인터페이스를 지원하고 리액티브 타입(Mono, Flux)의 전송과 수신을 합니다.

config

@Configuration
public class WebClientConfig {

    ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder() .codecs(
            configurer -> configurer.defaultCodecs().maxInMemorySize(20 * 1024 * 1024)
    ).build();

    @Bean
    public WebClient webClient() {
        return WebClient.builder()
                .exchangeStrategies(exchangeStrategies)
                .clientConnector(new ReactorClientHttpConnector(
                        HttpClient.create()
                                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 20000)
                                .doOnConnected(conn ->
                                        conn.addHandler(new ReadTimeoutHandler(20000, TimeUnit.MILLISECONDS))
                                )

                )).build();
    }

}
  • 메모리, 타임아웃 등의 처리도 가능

import

// webclient
implementation 'org.springframework.boot:spring-boot-starter-webflux'

controller

@RestController
@RequiredArgsConstructor
public class WebClientController {

    private final WebClientService webClientService;

    @GetMapping("/webclient/ping")
    public String ping() {
        return webClientService.ping();
    }

}

service

@Service
@RequiredArgsConstructor
public class WebClientService {

    private final WebClientApiCaller webClientApiCaller;

    public String ping() {
        return webClientApiCaller.ping();
    }

}

WebClientApiCaller

public interface WebClientApiCaller {

    String ping();

}

WebClientApiCallerImpl

@Component
@RequiredArgsConstructor
public class WebClientApiCallerImpl implements WebClientApiCaller {

    private final WebClient webClient;
    private final Duration timeout = Duration.ofMillis(20000);

    @Override
    public String ping() {
        return webClient
                .get()
                .uri("<https://api-lovga.site/ping>")
                .retrieve()
                .onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new BusinessException(ErrorCode.API_CALLER)))
                .onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new BusinessException(ErrorCode.API_CALLER)))
                .bodyToMono(String.class)
                .timeout(timeout, Mono.error(new BusinessException(ErrorCode.TIMEOUT_EXCEPTION)))
                .block();
    }
}
  • timeout이 일어났을 경우 커스텀 예외처리 해주고 싶어서 위처럼 처리

이 외에

// 헤더
.headers(headers -> {
                    headers.add("auth", token);
                })

// 바디
.body(BodyInserters.fromFormData(data))

// 쿼리 스트링
.uri(
            uriBuilder -> uriBuilder
                    .queryParam("limit", limit)
                    .queryParam("page", page)
                    .build()
        )
  • 빌더 패턴이여서 체이닝으로 끌고 가면 됩니다.~

근데 webClient를 사용하면서 webClient 빈 생성하지 않고 WebClient.bulder()도 가능하긴 하다. ⇒ 근데 요청을 보낼때마다 생성해줘서 약간 리소스 낭비(?)라고 생각해서 빈주입해서 했습니다.