[SpringBoot] RestTemplate 에서 WebClient로 속도 개선하기

울상냥·2023년 5월 10일
0

SpringBoot

목록 보기
9/11
post-custom-banner

Velog 통계 API로 HTTP Request를 보내는데 RestTemplate를 사용했었다.
문제는 Request를 게시글 수 만큼 보내야 한다는 것인데 이 부분이 상당히 오래걸렸다.
RestTemplateBlocking방식이기 때문에 하나의 요청을 보내고 대기를 하게 되기 때문이었다.
메인페이지 로딩에 6초가 걸릴 순 없다

이 문제를 해결하기 위해 알아보던중 WebClient에 대해 알게 되었다.

WebClient VS RestTemplate

WebClient

RestTemplate를 대체하는 HTTP Client이다.

  • 싱글쓰레드 방식을 사용
  • Non-Blocking 방식을 사용
  • Reactor 기반으로 동작

향후 RestTemplate는 deprecated될 예정이라고 한다. 논블로킹을 사용한다는 점에서 큰 차이를 갖는다.
Non-Blocking 방식이기 때문에 호출한 시스템의 동작을 기다리지 않고 동시에 다른 작업을 진행 할 수 있어 성능이 향상 될 수 있다.


Config

@Configuration
public class WebClientConfig {

    private static final String REQUEST_URL="https://v2cdn.velog.io/graphql";

    @Bean
    public WebClient webClient() {
        return WebClient.create(REQUEST_URL);
    }
}

WebClient 객체를 생성하는 2가지 방법이있는데

  • create()
  • builder()

create()는 default 설정으로 생성되고 builder()는 설정 커스텀이 가능하다.
기본으로는 create()를 사용하여 싱글톤 객체를 생성 후 필요한 경우에만 mutable()를 사용해 설정을 커스텀 해주었다.


WebClientService

기존의 RestTemplate를 사용하던 방식에서 WebClient를 사용하도록 변경했다.

block()

WebClient는 RestTemplate에서 사용하던 Blocking 방식으로도 사용이 가능하다.

@RequiredArgsConstructor
@Service
public class WebClientService {

    private final WebClient webClient;

    public Posts getPosts(String username, int totalPostsCount) {

        Variables variables = PostsVariables.builder()
                .username(username)
                .limit(totalPostsCount)
                .build();

        RequestBody body = RequestBody.builder()
                .operationName("Posts")
                .variables(variables)
                .query("""
                        query Posts($cursor: ID, $username: String, $temp_only: Boolean, $tag: String, $limit: Int) {
                            posts(cursor: $cursor, username: $username, temp_only: $temp_only, tag: $tag, limit: $limit) {
                                id
                                title
                                thumbnail
                                comments_count
                                tags
                                likes
                           }
                        }
                        """)
                .build();

        return webClient.post()
                .bodyValue(body)
                .retrieve()
                .bodyToMono(Posts.class)
                .block();
    }

block() 을 사용하면 RestTemplate와 유사하게 요청을 보내게 된다. getPosts의 경우 요청을 한번만 보내기 때문에 별 차이가 없을 것 같아 blocking방식으로 구현해 보았다.

subscribe()

속도개선이 필요했던 getStats는 논블로킹 방식으로 구현하였다.

public List<Stat> getStats(List<Post> posts, String accessToken){

        CountDownLatch countDownLatch = new CountDownLatch(posts.size());

        WebClient webClientMutated = webClient.mutate()
                .defaultCookie("access_token", accessToken)
                .build();

        List<Stat> stats = Collections.synchronizedList(new ArrayList<>());
        for (Post post : posts) {

            Variables variables = new StatsVariables(post.getId());

            RequestBody body = RequestBody.builder()
                    .operationName("GetStats")
                    .variables(variables)
                    .query("""
                            query GetStats($post_id: ID!) {
                                getStats(post_id: $post_id) {
                                    total
                                    count_by_day {
                                        count
                                        day
                                    }
                                }
                            }
                            """)
                    .build();

            webClientMutated
                .post()
                .bodyValue(body)
                .retrieve()
                .bodyToMono(Stat.class)
                .doOnTerminate(countDownLatch::countDown)
                .subscribe(result -> {
                    result.setId(post.getId());
                    stats.add(result);
                });
        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return stats;
    }

헤더에 accesstoken이 필요하기 때문에 mutate()로 헤더에 값을 설정해주었다.

논블로킹 방식으로 동작하기 때문에 각 request가 완료되기 전에 메인스레드가 종료될 수 있다.
CountDownLatch를 사용하여 각 호출이 완료되면 countDown()를 수행하고 await()를 통해 메인스레드 동작의 완료를 기다리도록 한다.

subscribe() callback 함수에서 list에 add를 해주게 되는데 여기서 문제가 발생했다..
보내는 요청의 수와 결과 list의 수가 맞지 않는것... 몇개의 응답이 증발해 버렸다ㅠ
이는 ArrayList가 thread safe하지 않기 때문이었다. 때문에 비동기 방식인 환경에서 ArrayList의 add가 동시에 일어나게 되면 데이터 손실이 발생하는 것이었다.

(증발..)

동기화된 리스트인 Collections.synchronizedList를 사용하여 문제가 해결되었다.


속도 테스트

 @Test
    public void RestTemplate_속도() {
        
        List<Stat> stats = restTemplateService.getStats(posts, accessToken);

        Assertions.assertEquals(stats.size(), posts.size());
    }

    @Test
    public void WebClient_비동기_속도() {
        
        List<Stat> stats = webClientService.getStats(posts, accessToken);

        Assertions.assertEquals(stats.size(), posts.size());
    }

    @Test
    public void WebClient_동기_속도() {
        
        List<Stat> stats = webClientService.getStatsByBlock(posts, accessToken);

        Assertions.assertEquals(stats.size(), posts.size());
    }

RestTemplate, WebClient Blocking, WebClient Non-Blocking의 세가지 방식으로 구현하여 테스트를 진행해 보았다.

현재 내 게시글이 66개인 상황에서

RestTemplate, WebClient Blocking 방식은 유사하게 5~6초 가량 소요되었고
WebClient Non-Blocking방식은 1초도 걸리지 않는다..!!!


예외 처리에 대한 부분이 없기 때문에 추가해주어야겠다.

profile
안되면 되게하라
post-custom-banner

0개의 댓글