본문 바로가기

IT/Spring

[Retrofit] Retrofit을 이용해서 다양한 방식으로 API 요청을 해보자

 

1. Retrofit 이란?

OKHttp 라이브러리를 기반으로 http 통신을 할 수 있게 도와주는 자바 라이브러리입니다.
보통 안드로이드 또는 스프링 웹 어플리케이션 서버에서 외부 서버와 API 통신을 하기 위해 사용합니다.

 

2. 테스트 API

테스트 진행 시 https://jsonplaceholder.typicode.com/guide/ 에서 제공해주는 API를 이용했습니다.
통신하는 API는 다음과 같습니다.

도메인
https://jsonplaceholder.typicode.com

포스트 생성1 (application/json)
POST /posts

포스트 생성2 (application/x-www-form-urlencoded)
POST /posts

단일 포스트 조회
GET /posts/{userId}

모든 포스트 조회
GET /posts?userId=1

 

2. build.gradle 의존성 정의

본 테스트는 스프링 부트를 이용해서 진행했습니다.
Spring Boot Version '2.6.3'
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    //retrofit2
    implementation 'com.squareup.retrofit2:retrofit:2.7.2'
    //요청,응답 객체를 파싱해주는 라이브러리(gson을 이용함)
    implementation 'com.squareup.retrofit2:converter-gson:2.7.2'
    //선택사항 : http 통신 로그를 출력한다
    implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0'
}

 

3. 포스트 요청, 응답 객체 정의

  1) PostsRequestDto.java

import com.google.gson.annotations.SerializedName;

import lombok.Builder;
import lombok.Getter;

public class PostsRequestDto {

    @Getter
    public static class Create {

        @Builder
        public Create(Long userId, String title, String body) {
            this.userId = userId;
            this.title = title;
            this.body = body;
        }

        @SerializedName("userId")
        private Long userId;

        @SerializedName("title")
        private String title;

        @SerializedName("body")
        private String body;
    }
}
PostsRequestDto.Create 는 포스트 생성 시 사용되는 클래스입니다.

ContentType이 application/json일 경우 json으로 파싱해주어야 합니다.
@SerializedName("userId")는 파싱할 경우 key을 의미합니다.

여기서 주의할 사항은 ContentType이 application/x-www-form-urlencoded 일 경우 위와 같은 방법으로 요청할 수 없습니다. 그래서 단일 필드로 파라미터를 전달하거나, Map객체로 매핑하여 전달해야합니다.

 

 2) PostsResponseDto.java

import com.google.gson.annotations.SerializedName;

import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

public class PostsResponseDto {

    @ToString
    @Getter
    public static class Posts {

        @Builder
        public Posts(Long userId, Long id, String title, String body) {
            this.userId = userId;
            this.id = id;
            this.title = title;
            this.body = body;
        }

        @SerializedName("userId")
        private Long userId;

        @SerializedName("id")
        private Long id;

        @SerializedName("title")
        private String title;

        @SerializedName("body")
        private String body;
    }

    @ToString
    @Getter
    public static class Create {

        @Builder
        public Create(Long userId, Long id, String title, String body) {
            this.userId = userId;
            this.id = id;
            this.title = title;
            this.body = body;
        }

        @SerializedName("userId")
        private Long userId;

        @SerializedName("id")
        private Long id;

        @SerializedName("title")
        private String title;

        @SerializedName("body")
        private String body;
    }
}
PostsResponseDto.Posts 클래스는 포스트 조회 시 사용되며, PostsResponseDto.Create는 포스트 생성의 응답 값입니다.
API 요청 시 응답값이 JSON일 경우 Retrofit은 별도의 파싱 작업 없이 key, value 규칙이 일치하다면 서드파트 라이브러리(Gson 등)를 이용해서 자동으로 파싱을 지원합니다.

 

4. 포스트 API 인터페이스 정의

import java.util.List;

import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.Query;

public interface PostsAPI {

    @GET("/posts/{userId}")
    Call<PostsResponseDto.Posts> getPosts(@Path("userId") Long userId);

    @GET("/posts")
    Call<List<PostsResponseDto.Posts>> getAllPosts(@Query("userId") Long userId);

    @POST("/posts")
    Call<PostsResponseDto.Create> createPosts(@Body PostsRequestDto.Create create);

    @FormUrlEncoded
    @POST("/posts")
    Call<PostsResponseDto.Create> createPostsByForm(
        @Field("userId") Long userId,
        @Field("title") String title,
        @Field("body") String body
    );
}
Retrofit은 요청할 API를 미리 인터페이스로 정의하며 Call 객체를 반환합니다.

1) @Path
Path 변수를 사용합니다.

2) @Query
QueryString을 사용합니다. (?userId=1)

3) @Body
@Body을 파라미터에 정의하면 파라미터 객체를 JSON으로 파싱하며, ContentType이 application/json으로 정의됩니다.

4) @FormUrlEncoded
@FormUrlEncoded 어노테이션을 정의하면, ContentType이 application/x-www-form-urlencoded으로 정의됩니다. 그리고 각 파라미터에 @Field 어노테이션을 정의해주어야합니다. 객체 형식으로 전달하고 싶으면 @FieldMap을 선언한 Map 객체를 파라미터로 넘겨야하며, 커스텀 객체를 사용할 수 없습니다. (찾는다면 댓글로 적어주세요..)

 

5. RetrofitRegistry.java

import java.util.concurrent.TimeUnit;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

@Configuration
public class RetrofitRegistry {

    private final String baseUrl = "https://jsonplaceholder.typicode.com";

    private static final HttpLoggingInterceptor loggingInterceptor
        = new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY);

    private final Gson gson = new GsonBuilder()
        .setLenient()
        .create();

    @Bean
    PostsAPI getJsonPlaceHolderAPI() {
        //retrofit 상세 설정
        OkHttpClient client = new OkHttpClient.Builder()
            //서버로 요청하는데 걸리는 시간을 제한 (15초 이내에 서버에 요청이 성공해야한다. (handshake))
            .connectTimeout(15, TimeUnit.SECONDS)
            //서버로 요청이 성공했고, 응답데이터를 받는데 시간을 제한한다. (15초 이내에 응답 데이터를 전달받아야한다)
            .addInterceptor(loggingInterceptor)
            .build();

        return new Retrofit.Builder()
            .baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create(gson))
            .client(client)
            .build()
            .create(PostsAPI.class);
    }

}
정의한 API를 Retrofit 설정과 함께 Bean으로 등록하는 컴퍼넌트입니다.

 

6. RetrofitUtils.java

import java.io.IOException;

import org.springframework.stereotype.Component;

import retrofit2.Call;
import retrofit2.Response;

@Component
public class RetrofitUtils {

    public <T> T execute(Call<T> call) {
        try {
            Response<T> response = call.execute();

            if (response.isSuccessful()) {
                return response.body();
            } else { //서버 통신은 성공, 하지만 2xx가 아닌 http status가 반환되었다.
                throw new RuntimeException(response.raw().toString());
            }
        } catch (IOException e) { // 서버 통신 실패
            throw new RuntimeException(e.getMessage());
        }
    }
}
API를 실제 요청해주는 Retrofit Util 클래스입니다. 해당 클래스의 사용은 필수가 아니지만, 사용하지 않으면 중복되는 로직이 발생합니다.

 

7. PostCaller.java, PostCaller.java

import java.util.List;

public interface PostsCaller {

    //포스트 단일 조회
    PostsResponseDto.Posts getPosts(Long userId);
    
    //모든 포스트 조회
    List<PostsResponseDto.Posts> getAllPosts(Long userId);

    //포스트 저장 (application/json)
    PostsResponseDto.Create createPosts(PostsRequestDto.Create create);

    //포스트 저장 (application/x-www-form-urlencoded)
    PostsResponseDto.Create createPostsByForm(PostsRequestDto.Create create);
}
import java.util.List;

import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
import retrofit2.Call;

@RequiredArgsConstructor
@Component
public class PostsCallerImpl implements PostsCaller {

    private final RetrofitUtils retrofitUtils;

    private final PostsAPI postsAPI;

    @Override
    public PostsResponseDto.Posts getPosts(Long userId) {
        Call<PostsResponseDto.Posts> call = postsAPI.getPosts(userId);
        return retrofitUtils.execute(call);
    }

    @Override
    public List<PostsResponseDto.Posts> getAllPosts(Long userId) {
        Call<List<PostsResponseDto.Posts>> call = postsAPI.getAllPosts(userId);
        return retrofitUtils.execute(call);
    }

    @Override
    public PostsResponseDto.Create createPosts(PostsRequestDto.Create create) {
        Call<PostsResponseDto.Create> call = postsAPI.createPosts(create);
        return retrofitUtils.execute(call);
    }

    @Override
    public PostsResponseDto.Create createPostsByForm(PostsRequestDto.Create create) {
        Call<PostsResponseDto.Create> call = postsAPI.createPostsByForm(
            create.getUserId(),
            create.getTitle(),
            create.getBody()
        );

        return retrofitUtils.execute(call);
    }
}
RetrofitUtils 객체와 PostsAPI 객체를 주입받습니다.
그리고 postsAPI를 이용해서 원하는 메소드를 실행하면 Call 객체가 반환되는데, 아직은 API 통신을 시작하지 않은 상태이고, retrofitUtils.execute(call); 실행 시 API 통신을 시작합니다.

 

비동기로 호출하고 싶을 경우

    public void createPostsByAsync(PostsRequestDto.Create create) {
        Call<PostsResponseDto.Create> call = postsAPI.createPosts(create);

        call.enqueue(new Callback<>() {
            @Override
            public void onResponse(Call<PostsResponseDto.Create> call, Response<PostsResponseDto.Create> response) {
                if(response.isSuccessful()){
                    PostsResponseDto.Create create = response.body();
                    //처리 로직
                } else {
                    throw new RuntimeException(response.raw().toString());
                }
            }

            @Override
            public void onFailure(Call<PostsResponseDto.Create> call, Throwable t) {
                throw new RuntimeException(t.getMessage());
            }
        });
    }
비동기로 호출하고 싶은 경우 위와같이 call의 enqueue 메소드를 이용하고 Callback 익명 클래스를 정의합니다.

 

8. 테스트 (실행만)

import java.util.List;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class RetrofitTest {

    @Autowired
    private PostsCallerImpl postsCaller;

    @Test
    @DisplayName("단일 포스트를 조회한다")
    public void 단일_포스트를_조회한다() {
        PostsResponseDto.Posts posts = postsCaller.getPosts(50L);
    }

    @Test
    @DisplayName("포스트 리스트를 조회한다")
    public void 포스트_리스트를_조회한다() {
        List<PostsResponseDto.Posts> posts = postsCaller.getAllPosts(1L);
    }

    @Test
    @DisplayName("포스트를 생성한다 (application/json)")
    public void 포스트를_생성한다() {
        PostsRequestDto.Create request = PostsRequestDto.Create.builder()
            .userId(30L)
            .title("안녕하세요?")
            .body("반갑습니다.")
            .build();

        PostsResponseDto.Create createResponse = postsCaller.createPosts(request);

    }
    @Test
    @DisplayName("포스트를 생성한다 (application/x-www-form-urlencoded)")
    public void 포스트를_생성한다2() {
        PostsRequestDto.Create request = PostsRequestDto.Create.builder()
            .userId(30L)
            .title("안녕하세요?")
            .body("반갑습니다.")
            .build();

        PostsResponseDto.Create createResponse = postsCaller.createPostsByForm(request);

    }
}
'com.squareup.okhttp3:logging-interceptor:3.9.0' 라이브러리 의존성을 등록했으면, 아래와 같이 API 요청에 대한 자세한 로그가 출력됩니다.


 

도움이 되셨다면 공감버튼 한번씩 눌러주세요!

 

github : Bamdule/retrofit-example: retrofit example (github.com)