티스토리 뷰

개요

  • Java 진영의 Spring 생태계에서 가장 널리 쓰이는 REST 클라이언트 라이브러리로는 Spring RestTemplate, Netflix Feign, Square Retrofit이 있다. 이 중에서도 Square Retrofit은 마치 @RestController의 클라이언트 버전을 보는 것과 같은 우아하게 구조화된 인터페이스를 제공하여 독보적이라 할 만하다.
  • Retrofit은 트위터의 최고 경영자인 잭 도시가 소유한 또 다른 미국 소재의 PG 결제 서비스 전문 회사인 Square에서 만든 라이브러리이다. Retrofit은 내부적인 로우 레벨 통신을 OkHttp가 담당하는데 이 또한 같은 회사에서 만든 라이브러리이다. 안드로이드 진영에서는 둘다 킬러 라이브러리로 폭넓게 쓰이고 있다.
  • Retrofit은 요청과 응답 데이터에 있어 타입을 강제한다.(Type-safe라고도 한다.) 따라서 개발자는 IDE 레벨에서 안전하고 실수 없는 충분히 예측 가능한 소스 코드를 작성할 수 있다.
  • 본 글에서는 Spring Boot 프로젝트에 Retrofit을 적용하는 방법을 설명하고자 한다. REST 요청의 대상이 되는 서비스는 REST 테스트용 더미 서비스를 제공하는 JSONPlaceholder로 하였다.

프로젝트 생성

  • Spring Initializr에 접속하여 기본 골격 프로젝트를 생성한다. Dependencies 항목에서 Web을 추가하고 Generate Project를 클릭하면 압축된 프로젝트 파일을 다운로드할 수 있다. 압축을 해제하고 build.gradle 파일을 IDE에서 불러오면 된다.

build.gradle 추가

  • 생성된 기본 /build.gradle 파일에 아래 내용을 추가한다.
dependencies {
    compile group: 'com.squareup.retrofit2', name: 'retrofit', version: '2.3.0'
    compile group: 'com.squareup.retrofit2', name: 'converter-jackson', version: '2.3.0'
    compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.9.3'
    provided group: 'org.projectlombok', name: 'lombok', version: '1.16.20'
    testCompile('org.springframework.boot:spring-boot-starter-test')
}
  • 만약 RxJava 기반의 비동기 HTTP 요청을 구현하고자 한다면 아래와 같이 라이브러리 종속성을 추가한다.
dependencies {
    compile group: 'io.reactivex.rxjava2', name: 'rxjava', version: '2.1.9'
    compile group: 'com.squareup.retrofit2', name: 'adapter-rxjava2', version: '2.3.0'
}
  • RxJavaRetrofit RxJava Adaptor 아티팩트를 추가하면 Observable 모델을 이용한 유연하고 직관적인 요청 로직을 구현할 수 있다.

POJO 작성

  • 대상 서비스에 대한 요청, 응답 데이터를 담을 POJO 클래스를 아래와 같이 작성한다.
package com.example.demo;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

@Data
public class Post {

    @JsonProperty("id")
    private Long postId;

    @JsonProperty("userId")
    private Long userId;

    @JsonProperty("title")
    private String title;

    @JsonProperty("body")
    private String body;
}
package com.example.demo;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

@Data
public class Comment {

    @JsonProperty("id")
    private Long commentId;

    @JsonProperty("postId")
    private Long postId;

    @JsonProperty("name")
    private String name;

    @JsonProperty("body")
    private String body;
}
  • POJO 클래스를 통해 개발자는 로우 레벨의 HTTP 인터페이스를 잊고 소스 코딩 하듯이 REST 요청을 수행할 수 있다.

Interceptor 작성

  • 인터셉터 역할을 수행할 클래스를 아래와 같이 작성한다.
package com.example.demo; class="language-java"

import okhttp3.Interceptor;
import okhttp3.Response;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class JsonPlaceholderInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {

        return chain.proceed(

                chain.request().newBuilder()
                        .addHeader("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE)
                        .addHeader("Cache-Control", "no-cache")
                        .addHeader("Cache-Control", "no-store")
                        .build()
        );
    }
}
  • 인터셉터 구현체의 구현은 필수가 아닌 옵션이다. 전체 REST 요청에 있어 반복되게 나타나는 동일 파라메터를 한 번에 처리하기 위한 용도로 주로 사용된다. 위 예제에서는 모든 요청에 첨부될 공통 헤더 파라메터를 추가한 것이다.

Service 작성

  • Retrofit를 빛내주는 핵심 기능인 Service 클래스를 아래와 같이 작성한다.
package com.example.demo;

import retrofit2.Call;
import retrofit2.http.*;

import java.util.List;

public interface JsonPlaceholderService {

    @GET("/posts/{postId}")
    Call<Post> getPost(@Path("postId") long postId);

    @GET("/posts")
    Call<List<Post>> getPosts();

    @POST("/posts")
    Call<Post> createPost(@Body Post post);

    @PUT("/posts/{postId}")
    Call<Post> updatePost(@Path("postId") long postId, @Body Post post);

    @DELETE("/posts/{postId}")
    Call deletePost(@Path("postId") long postId);

    @GET("/posts/{postId}/comments")
    Call<List<Comment>> getCommentsByPostId(@Path("postId") long postId);

    @GET("/comments")
    Call<List<Comment>> getComments(@Query("postId") long postId);
}
  • 위 예제는 JSONPlaceholder에서 제공하는 REST 서비스 엔드포인트를 추상화한 것이다. 한 눈에 봐도 @RestController와 같이 직관적인 인터페이스를 제공한다는 것을 알 수 있다. 앞서 작성된 POJO 클래스가 요청 바디 및 응답 바디를 담을 목적으로 사용되었다.
  • 리턴 타입은 기본적으로 Call<T> 타입이 요구된다. 요청의 대한 응답을 한 번 더 감싸 성공 여부 및 상태 코드를 제공한다. 만약 Java8CallAdapterFactory를 적용할 경우 CompletableFuture<T>로 대체할 수 있다. 이 경우 Java 8이 제공하는 레퍼런스 비동기 코드를 작성할 수 있다.
  • 아래와 같이 RxJavaObservable<T>타입으로 리턴 타입을 교체하면 RxJava의 모든 특징을 이용하여 비동기 요청 처리 로직을 구현할 수 있다.
@GET("/posts/{postId}")
Observable<Post> getPost(@Path("postId") long postId);

Config 클래스 작성

  • 앞서의 기능을 작동하게 해주는 빈을 등록할 차례이다.
package com.example.demo;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;

@Configuration
public class JsonPlaceholderConfig {

    @Autowired
    private Interceptor jsonPlaceholderInterceptor;

    @Bean("jsonPlaceholderOkHttpClient")
    public OkHttpClient jsonPlaceholderOkHttpClient() {

        return new OkHttpClient.Builder()

                .addInterceptor(jsonPlaceholderInterceptor)
                .build();
    }

    @Bean("jsonPlaceholderObjectMapper")
    public ObjectMapper jsonPlaceholderObjectMapper() {

        return Jackson2ObjectMapperBuilder.json()

                .featuresToDisable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
                .modules(new JavaTimeModule())
                .build();
    }

    @Bean("jsonPlaceholderRetrofit")
    public Retrofit jsonPlaceholderRetrofit(

            @Qualifier("jsonPlaceholderObjectMapper") ObjectMapper jsonPlaceholderObjectMapper,
            @Qualifier("jsonPlaceholderOkHttpClient") OkHttpClient jsonPlaceholderOkHttpClient
    ) {

        return new Retrofit.Builder()

                .baseUrl("https://jsonplaceholder.typicode.com")
                .addConverterFactory(JacksonConverterFactory.create(jsonPlaceholderObjectMapper))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .client(jsonPlaceholderOkHttpClient)
                .build();
    }

    @Bean("jsonPlaceholderService")
    public JsonPlaceholderService jsonPlaceholderService(

            @Qualifier("jsonPlaceholderRetrofit") Retrofit jsonPlaceHolderRetrofit
    ) {
        return jsonPlaceHolderRetrofit.create(JsonPlaceholderService.class);
    }
}
  • 앞서 Retrofit은 내부적으로 OkHttp를 사용한다고 언급했었다. 위 예제와 같이 커스터마이징된 OkHttpClient 빈을 등록해둔다. 이 과정에서 먼저 작성한 인터셉터 클래스를 등록할 수 있다.
  • REST 추상화의 핵심은 실제 RAW 데이터와 POJO 오브젝트 간의 매끄러운 상호변환이다. Retrofit은 다양한 오픈 소스 JSON 라이브러리에 대한 컨버터를 제공한다. 위 예제는 Spring Boot의 기본 JSON 라이브러리인 ObjectMapper 빈을 등록하였다.
  • Retofit 빈은 핵심 코어이다. 앞서 작성한 빈을 이용하여 빈을 등록한다. 이 과정에서 대상 REST 서비스에 대한 baseUrl을 설정할 수 있다.
  • 마지막으로 Service 빈을 등록한다.

사용 예

  • 앞서의 작업으로 모든 설정이 끝났다. 이제 작성된 기능을 사용할 차례이다.
@Autowired
JsonPlaceholderService jsonPlaceholderService;

...

Response<List<Post>> getPostsResponse = jsonPlaceholderService.getPosts().execute();

Post newPost = new Post();
newPost.setTitle("newTitle");
newPost.setBody("newBody");
newPost.setUserId(1L);

Response<Post> createPostResponse = jsonPlaceholderService.createPost(newPost).execute();
Response<Post> getPostResponse = jsonPlaceholderService.getPost(100L).execute();
Response<Comment> getCommentsByPostIdResponse = jsonPlaceholderService.getCommentsByPostId(1L).execute();
  • RxJavaObservable 리턴 타입으로 구현했을 경우 아래와 같이 작성할 수 있다.
jsonPlaceholderService.getPost(1L)
        .subscribeOn(Schedulers.io())
        .repeat() // 해당 요청을 반복하고 싶을 경우 실행, 생략하면 1회 요청
        .doOnError(error -> {
            if (error instanceof UnknownHostException) {
                // UnknownHostException 예외 발생시 처리
            }
            if (error instanceof HttpException) {
                // 상태 코드 2XX 외의 응답시 예외 처리
            }
        })
        .subscribe(post -> {
            log.info("postId: {}", post.getPostId()); // 실제 로직의 실행
        });
  • .subscribeOn() 메써드에 스케쥴러 타입을 명시할 수 있다. 명시하지 않을 경우 HTTP 요청은 현재 쓰레드에서 동기로 처리된다. Schedulers.io()를 명시하면 CPU 또는 메모리를 많이 소모하지 않는 파일, 네트워크 관련 작업에 적합한 쓰레드 풀을 이용하여 요청을 비동기로 처리한다. HTTP 요청은 전형적인 네트워크 Non-Blocking 작업이므로 적합하다.

참고 글

댓글
댓글쓰기 폼