이제 Velog 통계 서비스 구상기에서 살펴본 Velog API로 request를 보내야 한다. 추후에 WebClient
로 변경하긴 하지만 RestTemplate
로 개발을 시작하였다.
(Spring Http Request 검색했을 때 대부분이 RestTemplate 글이었기 떄문에..)
WebClient
로 변경기는 다음 글에서 작성하도록 하고..
스프링 3.0에서부터 지원하는 RestTemplate은 HTTP 통신에 유용하게 쓸 수 있는 템플릿이다. REST 서비스를 호출하도록 설계되어 HTTP 프로토콜의 메서드 (GET, POST, DELETE, PUT)에 맞게 여러 메서드를 제공한다.
- 통신을 단순화하고 RESTful 원칙을 지킨다
- 멀티쓰레드 방식을 사용
- Blocking 방식을 사용
따로 의존성을 추가할 필요 없고 사용도 쉽다.
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(5))
.additionalInterceptors(clientHttpRequestInterceptor())
.build();
}
private ClientHttpRequestInterceptor clientHttpRequestInterceptor() {
return (request, body, execution) -> {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(3));
try {
return retryTemplate.execute(context -> execution.execute(request, body));
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
};
}
}
RestTemplate는 Thread Safe
하기 때문에 @Bean
에 등록하여 싱글톤으로 사용할 수 있다.
new RestTemplate()
기본생성자를 사용할 수 있지만 몇가지 설정을 추가해 주었다.
스프링부트에서 제공하는 RestTemplateBuilder
을 사용하여 타임아웃과 인터셉터 설정을 통해 retry 설정을 해주었다.
@RequiredArgsConstructor
@Service
public class RestTemplateService {
private final RestTemplate restTemplate;
private static final String REQUEST_URL="https://v2cdn.velog.io/graphql";
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 restTemplate.postForObject(REQUEST_URL, body, Posts.class);
}
RequestBody
를 build하여 POST
요청을 보낸다.
body만 필요해서 postForObject
를 사용했다. 응답 받을 클래스들과 각각에 대한deserializer
들을 작성해 주었다.
public List<Stat> getStats(List<Post> posts, String accessToken) {
HttpHeaders headers = new HttpHeaders();
headers.set("cookie", "access_token=" + accessToken);
List<Stat> stats = new ArrayList<>();
for (Post post : posts) {
StatsVariables 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();
HttpEntity<RequestBody> entity = new HttpEntity<>(body, headers);
Stat stat = restTemplate.postForObject(REQUEST_URL, entity, Stat.class);
stat.setId(post.getId());
stats.add(stat);
}
return stats;
}
}
헤더가 필요한 경우에는 HttpHeaders
객체를 생성해 HttpEntity
에 body와 담아 보낸다.
해당 요청에 필요한 인증정보인 accesstoken
을 함께 전송하도록 했다.
아쉽게도 API가 postId
별로 요청을 하도록 되어있어서,,, 전체 조회수를 구하기 위해서는 해당 요청을 전체 게시글 수 만큼 보내야 했다.
RestTemplate는 Blocking
방식이기 때문에 요청후 응답을 기다린다.
내 게시글이 대략 65개 정도였는데 반복문 수행에 대략 6초정도의 시간이 소요되었다..
(페이지 로딩에 6초는 참을수가 없단말이다.)
추후에 Non-Blocking
방식의 WebClient
로 변경하고 속도가 아주 개선되었다.
{
"operationName": "Posts",
"variables": {
"username": "yevini118",
"tag": null,
"cursor": "ebf3d8fe-ddc1-495b-8763-54e902c1094a"
},
"query": "query Posts($cursor: ID, $username: String, $temp_only: Boolean, $tag: String, $limit: Int) {\n posts(cursor: $cursor, username: $username, temp_only: $temp_only, tag: $tag, limit: $limit) {\n id\n title\n short_description\n thumbnail\n user {\n id\n username\n profile {\n id\n thumbnail\n __typename\n }\n __typename\n }\n url_slug\n released_at\n updated_at\n comments_count\n tags\n is_private\n likes\n __typename\n }\n}\n"
}
사용할 모든 request의 payload
는 다음과 같은 구조를 갖고 있다.
@Builder
@Getter
public class RequestBody {
private String operationName;
private Variables variables;
private String query;
}
payload
로 사용될 클래스를 작성해주었다.
public interface Variables {
}
variables 의 요소는 operation별로 다르기때문에 interface
를 작성해주고
각각의 operation에 맞게 구현체들을 작성해주었다.
@Builder
@Getter
public class PostsVariables implements Variables {
private String username;
private String tag;
private int limit;
}