Java/Spring
(24.12.17) WebClient 정리
Job_E
2024. 12. 16. 23:44
- WebClient란?
- 웹으로 API를 호출하기 위한 HTTP Client 모델
- WebClient의 특징
- 싱글 스레드 & 비동기(Non-Blocking)
- 각 요청은 Event Loop 내에 Job로써 등록됨
- Event Loop는 각 Job을 제공자(Worker)에게 요청한 후 결과를 기다리지 않고 다른 Job를 처리
- 이후 제공자로부터 Callback으로 응답(Response)이 오면 해당 결과를 요청자에게 제공
- WebClient의 사용 배경
- 같은 Http Client 모듈인 RestTemplate와의 비교
- RestTemplate : 멀티 스레드 & 동기(Blocking) (※ WebClient : 싱글 스레드 & 비동기)
- Client가 요청 시 큐에 쌓이고, 가용한 스레드가 있으면 이에 할당되어 요청 처리
- 각 스레드는 동기 방식이기 때문에 해당 요청의 응답이 오기 전까진 다른 요청을 처리할 수 없음
- 문제 발생 등의 사유로 사용 불가능한 스레드가 많아지면 서비스가 현저히 느려질 수 있음
- WebClient와의 성능 비교
- Boot 1 : WebClient // Boot 2 : RestTemplate
- 1000명까진 비슷하지만 동시사용자가 늘수록 Boot 2(RestTemplate)은 급격하게 느려짐
- RestTemplate : 멀티 스레드 & 동기(Blocking) (※ WebClient : 싱글 스레드 & 비동기)
- 같은 Http Client 모듈인 RestTemplate와의 비교
- WebClient의 사용
- Dependency 추가
//build.gradle //webClient 사용을 위한 Webflux 추가 implementation 'org.springframework.boot:spring-boot-starter-webflux'
- Method
- create / build
- WebClient를 생성하는 메소드
WebClient.create(); // WebClient.create("http://https://nid.naver.com"); // 요청할 uri와 함께 생성 가능 // 또는 WebClient.Build();
- Create : 기본 옵션으로 WebClient 생성
- Build : 모든 설정을 커스터마이징할 수 있도록 *DefaultWebClientBuilder* 클래스를 사용하여 WebClient 생성
- 옵션 목록
- UriBuilderFactory : BaseUri를 커스텀한 UriBuilderFactory
- DefaultUriVariables : URI 매핑 시 사용할 기본 값 정의
- defaultHeader : 모든 요청에 사용할 헤더
- defaultCookie : 모든 요청에 사용할 쿠키
- defaultRequest : 모든 요청의 Request를 커스터마이징 할 수 있는 설정
- filter : 모든 요청에 사용할 필터 옵션
- exchangeStrategies : HTTP 메시지 처리 관련 전략 지정 시 사용
- ClientConnecter : Http Client의 라이브러리 세팅
- 이외에도 observationRegistry와 observationConvention이 있지만 생략
- 세팅 예시
import org.springframework.web.reactive.function.client.WebClient; import java.util.Collections; WebClient client = WebClient .builder() .baseUrl("http://localhost:8080") .defaultCookie("def_cookie", "val") // 쿠키 이름, 쿠키 값 .defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"); .build();
- mutate()
- 한번 WebClient를 빌드한 뒤부턴 WebClient는 immutable(수정 불가능)
- 이 때 mutate()를 사용하여 기존 WebClient 인스턴스는 그대로 두고, 대상 WebClient 인스턴스를 복제해와서 설정을 변경하여 사용할 수 있음.
import org.springframework.web.reactive.function.client.WebClient; import java.util.Collections; WebClient client = WebClient .baseUrl("http://localhost:8080") .defaultCookie("def_cookie", "val") // 쿠키 이름, 쿠키 값 .defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"); .build(); WebClient client_2 = client.mutate() .defaultCookie("def_cookie_2", "val_2") // 쿠키 이름, 쿠키 값 .build();
- 옵션 목록
- WebClient를 생성하는 메소드
- create / build
- Dependency 추가
5. WebClient 사용 예시
- 요청 매핑
// 현재 프로젝트 목적으로 개발중인 카카오 로그인 시 콜백 메서드 private final KakaoLoginService kakaoLoginService; @GetMapping("/callback_kakao") public ResponseEntity<?> callback_kakao(@RequestParam("code") String code) { System.out.println("Authorize Code : "+code); String accessToken = kakaoLoginService.getAccessTokenFromKakao(code); System.out.println("AccessToken : " + accessToken); KakaoUserInfoResponseDto userInfoResponseDto = kakaoLoginService.getUserInfo(accessToken); System.out.println(userInfoResponseDto.getId()); return new ResponseEntity<>(HttpStatus.OK); }
- 현재 프로젝트 목적으로 제작중인 Service의 getAccessTokenFromKakao 메서드를 보자.
private String clientId; private final String KAUTH_TOKEN_URL_HOST; private final String KAUTH_USER_URL_HOST; @Autowired public KakaoLoginService(@Value("${spring.security.oauth2.client.registration.kakao.client-id}") String clientId) { this.clientId = clientId; KAUTH_TOKEN_URL_HOST = "https://kauth.kakao.com"; // KAUTH_USER_URL_HOST = "https://kapi.kakao.com"; } public String getAccessTokenFromKakao(String code) { // 여기서 code는 카카오에서 발급받은 인가코드를 의미한다. WebClient 설명에는 상관없으므로 설명 생략 TokenResponseDto tokenResponseDto = WebClient.create(KAUTH_TOKEN_URL_HOST).post() // 1. post 메서드 선언 .uri(uriBuilder -> uriBuilder .scheme("https") .path("/oauth/token") .queryParam("grant_type", "authorization_code") .queryParam("client_id", clientId) // clientId : Kakao Developer를 통해 발급받은 클라이언트ID .queryParam("code", code) .build(true)) // https://kauth.kakao.com/oauth/token?grant_type=authorization_code&클라이언트ID=clientId&code=인가코드 .header(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString()) .retrieve() //Response 세팅 : 위에서 작성된 요청의 결과를 추출할지 명시 // 각 서비스마다 Custom Exception이 있다면 onStatus 메서드에 지정하면 된다. .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> Mono.error(new RuntimeException("Invalid Parameter"))) .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("Internal Server Error"))) .bodyToMono(TokenResponseDto.class) .block(); // BodyToMomo를 통해 반환받은 결과를 동기적으로 처리 return tokenResponseDto.getAccessToken(); }
- retrieve에 대한 추가 설명
- retrieve() 이후에 toEntity(클래스명) 또는 bodyToMomo(클래스명.class)로 사용 가능
- toEntity : status, headers, body를 포함한 ResponseEntity 타입으로 받을 수 있음
- tobodyToMomo : 응답의 body만 받고 싶을 때 사용
- 실행 결과
HTTP POST https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id=(생략)&code=(생략) Response 200 OK Decoded [(생략).backend.auth.dto.TokenResponseDto@656a96c0] Access Token ------> (생략) Refresh Token ------> (생략)
- 참조 사이트
(※ 노션에다 작성하고 붙여넣는 방법을 사용해봤는데... 두번다신 안해야겠다. 내용에 제멋대로 이동하네) - retrieve에 대한 추가 설명