뭔지는 알고 쓰자 WebClient!

    지난 코드스쿼드 프로젝트에서 JWT + Oauth로 로그인 구현하기를 시도했다. 클라이언트 단에서 보낸 Github auth 코드를 받아서, 그 코드를 다시 1) Github authorization 서버에 보내서 access token을 받고, 2) 그렇게 받은 access token을 다시 Github resource 서버에 보내면 유저의 정보를 얻어올 수 있다. 근데 이렇게 하려면 서버 어플리케이션에서 깃헙 서버에 HTTP 요청을 보내고 그 응답을 받아서 처리할 수 있어야 한다. 그럴 때 뭘 써야 하지?

     

    하고 검색해 보니까 나온 게 WebClient였다. WebClient 이전에는 RestTemplate라는 게 있었다고 한다. 그런데 WebClient가 뭔가 개선된 버전이래! 그럼 당연히 새로 나온 거 써야지 하고 WebClient를 썼다. 살펴보니까 비동기 처리를 하기 위한 여러 개념들이 등장했는데... 우선 이번 프로젝트에서는 WebClient의 동작 방식을 이해하는 게 핵심은 아니라고 생각했다. 지금까지 코드스쿼드 과정을 진행하면서 제대로 도전하지 못했던 로그인 구현에 도전해 보고 그 흐름을 파악하는 게 목적이었기 때문에 우선 인터넷 자료를 뒤져서 HTTP 요청을 보내고 + 그 응답을 받아올 수 있는 코드를 어쩌저찌 만들어 냈다. 

     

    private Map<String, String> getUserInfo(String accessToken) {
            WebClient getUserInfoClient = WebClient.builder()
                    .baseUrl("https://api.github.com")
                    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                    .build();
    
            WebClient.ResponseSpec userInfoResponseSpec = getUserInfoClient.get()
                    .uri("/user")
                    .header("authorization", String.format("token %s", accessToken))
                    .acceptCharset(StandardCharsets.UTF_8)
                    .retrieve();
    
            Mono<String> mono = userInfoResponseSpec.bodyToMono(String.class);
             
            String userInfoResponse = mono.block();
    
            return new Gson().fromJson(userInfoResponse, Map.class);

     

    예시로 든 getUserInfo() 메서드는 위에서 말한 2가지 요청 중 요청 2)에 해당한다. 요청 1)에서 받은 accessToken을 매개변수로 넣어서, 그 토큰을 헤더의 Authorization 필드에 "token [엑세스 토큰 값]" 형태로 넣어서 GET 요청을 보내는 코드다.

     

    크게

    1) WebClient getUserInfoClient: base uri와 기본 헤더를 명시해서 WebClient 객체를 만든다.

    2) ResponseSpec userInfoResponseSpec:  앞에서 만든 getUserInfoClient 객체에, 요청에 쓸 HTTP 메서드(GET)와 헤더 정보(여기에 authorization 헤더와 토큰 정보를 추가한다), 응답을 받을 문자열 인코딩(UTF-8) 등을 명시해 요청을 보낸 다음, retrieve()를 통해 response body를 디코딩한다.

    3) String userInfoResponse: 앞에서 만든 ResponseSpec 객체의 응답 body를 bodyToMono().block()을 통해 String으로 만든다...? 왜 이렇게 하는지 이해 못하고... 그냥 일단 block을 하면 응답을 받을 수 있다길래 일단 썼다. 일단은 잘 돌아갔다!

     

    3) 과정에서 어떤 일이 일어나는 건지 전혀 이해하지 못하고, "일단 로그인을 구현해보자!"하는 일념으로 구글 주도 개발을 한 모습. Body를 mono로 바꾼다는 게 뭐지? 그리고 block()을 하면 최종적으로 String으로 변환된다는데... block()이 하는 역할은 뭐지? 생각하면서 우선 코드를 짰더니, 아니나 다를까 리뷰어가 바로 이런 댓글을 달아 주셨다.

     

    그게... 이제부터 공부해 보겠습니다(?)

    크크 역시 알고 써야 한다! 그래서 정리하는 RestTemplate과 WebClient의 차이.

     

     

    RestTemplate은 동기식, WebClient는 비동기식

    앞에서 WebClient가 RestTemplate를 개선한 버전이라는 점을 언급했다. 다시 말해 RestTemplate가 가지는 단점이 있었다는 이야기인데, 바로 RestTemplate가 동기식으로 작동한다는 점이다. RestTemplate는 요청 하나당 스레드 하나를 생성해서 보낸다. 그리고 보낸 요청이 완료되어 응답을 받을 때까지 스레드가 block 상태에서 대기하게 된다. 요청이 하나라면 그럭저럭 괜찮겠지만, 만약 여러 개의 요청을 보내야 하는 경우라면 어떨까? 요청 개수만큼의 스레드가 생성되어서 응답을 기다리는 동안 계속 돌아가기 때문에 자원을 계속 점유하고 있는 것은 물론이고, 스레드 간의 switching에도 비용이 든다. 그러면 당연히 어플리케이션에 부하가 증가하고 성능은 저하된다.

     

    그래서 등장한 게 WebClient다. 얘는 비동기식으로 동작한다! 그래서 일단 HTTP 요청을 보내고, 그 요청에 대한 응답은 비동기적으로 처리한다. 이런 비동기적 처리를 수행하기 위해 도입된 자바의 리액티브 프로그래밍 패러다임을 스프링 5 버전부터 지원하기 시작했는데, 이 중 웹 클라이언트와 서버에서 리액티브 스타일의 프로그래밍을 하기 위한 모듈이 WebFlux이다. WebClient의 비동기 처리는 이 WebFlux를 기반으로 만들어져 있다.

     

     

    Reactive Stream, Flux와 Mono

    자, WebClient는 요청을 보낸 다음에 응답을 비동기로 처리한다. 응답이 올 때까지 블록된 상태에서 대기하는 게 아니라, 리턴은 먼저 받아 놓고 서버로부터 응답이 제대로 돌아오면 그 응답을 사용하는 것이다. 이렇게 처리하려면 응답을 어떤 형태로 받아야 할까? 그렇다. 스트림이다! 자바에서 이런 비동기 스트림 처리의 표준이 되는 것이 reactive stream이다. reactive stream은 데이터를 생성하는 Publisher와 그 데이터를 소비하는 Subscriber로 구성된다.

     

    WebFlux에서 reactive stream의 구현체는 Reactor라는 칭구고, 그 Reactor에서 데이터를 생성하는 Publisher 역할을 하는 객체는 크게 Flux와 Mono로 나눌 수 있다. Flux 여러 개의 데이터를 발생시킬 수 있고, Mono는 1개의 데이터를 발생시킨다. 

     

    이제 위 코드에서 bodyToMono()가 어떤 역할을 하는지 이제 알 수 있다. WebClient의 get() 메서드로 리퀘스트를 만들기 시작해서, retrieve() 메서드를 통해 리퀘스트를 보내고, 그 응답 body를 Mono로 바꿔주는 게 bodyToMono()라고 볼 수 있다. 내가 받아야 하는 응답은 단일 값이니까 Mono를 쓴 것이고, bodyToMono의 인자로 String.class를 넣어주었기 때문에 Mono<String>을 반환하게 된다. String 데이터를 보내는 Stream이라고 볼 수 있다.

     

     

    block(): Spring MVC에서 WebFlux를 사용하는 것의 의미

    자, 그럼 Mono 형태로 데이터를 받은 것까지는 좋다. 근데 그럼 그걸 어떻게 처리해야 돼...? 그게 객체(위 코드에서는 String) 타입이 되어야 그걸 받아서 리턴해줄 거 아냐. 여기서 문제가 발생한다. 위 코드는 기본적으로 Spring Web MVC를 사용하는 코드다. 스프링 MVC는 기본적인 패러다임이 블로킹 기반의 동기적 프로그래밍이다. 그래서 비동기적으로 응답을 Mono로 처리해도 거기서 응답을 받아 와야 다음 코드를 처리할 수가 있다. 그래서 위 코드에서 block() 메서드를 사용하고 있는 것이다. block() 메서드를 사용하면 Mono에서 응답이 만들어질 때까지 기다리고, 그걸 받아와서 객체 형태로 리턴받는다.

     

    사실 이렇게 처리하면 WebFlux가 가지고 있는 논블로킹 방식의 장점이 없어진다. 즉, WebClient가 제공하는 논블로킹 방식의 장점을 완전히 사용하기 위해서는 코드 전체가 비동기적 방식으로 돌아가야 의미가 있다. 리뷰어가 얘기하신 게 바로 이 부분이다. 어차피 block()을 쓰면 WebClient를 쓰는 의미가 크게 없다는 것이다.

     

    그러면 RestTemplate를 쓰는 게 맞나? 싶었는데, Spring에서는 RestTemplate는 점차 deprecate될 예정이니 WebClient를 사용하라고 권고하고 있다. 그럼 어떻게 해야 하나... 하고 자료를 찾아보니, Flux의 경우에는 toSteam()을 통해 stream으로 변환하고, Mono의 경우에는 flux() 메서드를 통해 Flux 형태로 변환한 다음 findFirst() 메서드를 통해 Optional로 처리하는 방법이 있다고.(출처) 어차피 Flux로 바꾸고 stream 처리하는 거라, 사실상 이런 상황에서는 Mono로 받을 이유가 크게 없겠구나 생각이 들었다.

     


    어떻게 쓰는지 모르고 막 쓰면 안 된다는 걸 다시 한 번 깨달은 경험이었다. 이 글에서는 Reactive 프로그래밍과 WebFlux 자체에 대한 내용은 개략적으로만 다루고 있는데, 기회가 되면 이후 글에서 조금씩 더 다루어 보겠다!

     

     

    참고자료

    - https://spring.getdocs.org/en-US/spring-framework-docs/docs/spring-web-reactive/webflux-client/webflux-client-retrieve.html

    -https://www.baeldung.com/spring-webclient-resttemplate#:~:text=RestTemplate%20uses%20Java%20Servlet%20API,RestTemplate%20will%20still%20be%20used.

    - https://tech.kakao.com/2018/05/29/reactor-programming/

    - https://sjh836.tistory.com/141

    - https://devuna.tistory.com/120

    - https://happycloud-lee.tistory.com/220

    - https://medium.com/@odysseymoon/spring-webclient-%EC%82%AC%EC%9A%A9%EB%B2%95-5f92d295edc0

    댓글