Velog 통계 API로 HTTP Request를 보내는데 RestTemplate
를 사용했었다.
문제는 Request를 게시글 수 만큼 보내야 한다는 것인데 이 부분이 상당히 오래걸렸다.
RestTemplate
는 Blocking
방식이기 때문에 하나의 요청을 보내고 대기를 하게 되기 때문이었다.
메인페이지 로딩에 6초가 걸릴 순 없다
이 문제를 해결하기 위해 알아보던중 WebClient
에 대해 알게 되었다.
RestTemplate를 대체하는 HTTP Client이다.
- 싱글쓰레드 방식을 사용
- Non-Blocking 방식을 사용
- Reactor 기반으로 동작
향후 RestTemplate는 deprecated될 예정이라고 한다. 논블로킹을 사용한다는 점에서 큰 차이를 갖는다.
Non-Blocking
방식이기 때문에 호출한 시스템의 동작을 기다리지 않고 동시에 다른 작업을 진행 할 수 있어 성능이 향상 될 수 있다.
@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()는 default 설정으로 생성되고 builder()는 설정 커스텀이 가능하다.
기본으로는 create()를 사용하여 싱글톤 객체를 생성 후 필요한 경우에만 mutable()
를 사용해 설정을 커스텀 해주었다.
기존의 RestTemplate
를 사용하던 방식에서 WebClient
를 사용하도록 변경했다.
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방식으로 구현해 보았다.
속도개선이 필요했던 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초도 걸리지 않는다..!!!
예외 처리에 대한 부분이 없기 때문에 추가해주어야겠다.