[SpringBoot] RestTemplate로 HTTP request 보내기

울상냥·2023년 5월 9일
0

SpringBoot

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

이제 Velog 통계 서비스 구상기에서 살펴본 Velog API로 request를 보내야 한다. 추후에 WebClient로 변경하긴 하지만 RestTemplate로 개발을 시작하였다.
(Spring Http Request 검색했을 때 대부분이 RestTemplate 글이었기 떄문에..)
WebClient로 변경기는 다음 글에서 작성하도록 하고..


RestTemplate

스프링 3.0에서부터 지원하는 RestTemplate은 HTTP 통신에 유용하게 쓸 수 있는 템플릿이다. REST 서비스를 호출하도록 설계되어 HTTP 프로토콜의 메서드 (GET, POST, DELETE, PUT)에 맞게 여러 메서드를 제공한다.

  • 통신을 단순화하고 RESTful 원칙을 지킨다
  • 멀티쓰레드 방식을 사용
  • Blocking 방식을 사용

따로 의존성을 추가할 필요 없고 사용도 쉽다.


Config

@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 설정을 해주었다.


RestTemplateService

@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로 변경하고 속도가 아주 개선되었다.


Payload

{
    "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 는 다음과 같은 구조를 갖고 있다.

RequestBody

@Builder
@Getter
public class RequestBody {

    private String operationName;
    private Variables variables;
    private String query;
}

payload로 사용될 클래스를 작성해주었다.

Variables

public interface Variables {
}

variables 의 요소는 operation별로 다르기때문에 interface를 작성해주고
각각의 operation에 맞게 구현체들을 작성해주었다.

PostsVariables

@Builder
@Getter
public class PostsVariables implements Variables {

    private String username;
    private String tag;
    private int limit;
}

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

0개의 댓글