개인 App을 만들거나 현업에서 App을 만들다보면, 단순한 화면이동 등의 기능으로는 사용자의 요구사항을 충당하기 힘이 듭니다.

일반적인 App은 서버와 클라이언트가 네트워크 통신을 통해 데이터를 주고 받으며 실질적으로 화면에 정보를 표출하고, 입력된 정보를 저장하는 등의 여러가지 기능들이 구현되어 있습니다.

App에서의 네트워킹 기능은 필수라고 해도 과언이 아닐 정도이죠.

그리고 요즘은 일반적으로 서버와 클라이언트가 데이터를 주고 받는 과정에서, REST API라는 방식을 이용하여 네트워크 통신을 하게 됩니다.

Android Framework에 기본적으로 내장되어 있는 AsyncTaskHttpURLConnection 객체를 이용하면,
충분히 네트워크 통신에 대한 기능을 구현할 수 있지만,
직접 네트워크 통신을 구현하면 필수적으로 처리해주어야 할 몇가지의 작업으로 인해 여간 귀찮은 것이 아닙니다.

예를들어, 네트워크 연결 / 해제 / 데이터 파싱 / 파싱된 데이터를 데이터 클래스에 저장 / 에러처리 등이 있겠네요.

하지만, 이렇게 귀찮은 작업을 굉장히 편리하게 해주는 도구가 있습니다.

이번 포스팅에서는,
그 중에서도, REST API 통신에 특화되어 있는 Retrofit이라는 App 개발 시 필수적인 Library라고 해도 과언이 아닌 녀석의 기본 사용법에 대해 정리를 해볼까 합니다.

우선 들어가기에 앞서, Retrofit의 공식페이지에서 소개하고 있는 내용을 ctrl + c / v만 잘 이용해도 바로 사용할 수 있을 정도로 자료를 제공해주고 있음에도 불구하고, 이렇게 따로 정리를 하는 이유는 영어문서에 대한 거부감이 있으신 분이나 Android 프로그래밍을 시작한지 얼마 되지 않아 Library에 대한 사용이 익숙하지 않으신 분들에게 조금이나마 도움을 드리고자 글을 작성하게 되었습니다.

그리고 이번 포스팅을 시작으로 Retrofit에 사용에 대한 감을 익히고,

실질적으로 Retrofit을 이용한 REST API 통신과 통신 후, View에 표출하는 과정까지에 대한 방법을 몇번의 포스팅으로 나눠서 작성해보고자 합니다.

구상중인 포스팅 목록

현재 이 글을 작성하는 시점을 기준으로 4단계의 포스팅을 구상하고 있습니다.

  • Retrofit Library 추가와 기본 사용법 (현재글)
  • FutureStudio에서 소개하고 있는 ServiceGenerator Class 작성.
  • MVC Pattern을 적용한 Retrofit과 RecyclerView를 사용한 기능 구현.
  • 완성된 MVC Pattern의 코드를 MVP Pattern의 코드로 적용.

추가적으로, 현재의 샘플 프로젝트는 Retrofit의 사용법에 대한 감을 익히기 위한 프로젝트 이므로, GET 요청만 사용하지만,
기회가 된다면 POST / PUT / DELETE 요청과 파일 전송을 위한 MULTIPART까지 한번 내용을 추가해 보는 것이 목표입니다.

포스팅을 하나하나 추가해가며, 완성된 포스팅에 대한 글은 추가해나가도록 하겠습니다.

본격적인 시작에 앞서, 최종 결과물만을 원하시는 분은 Github SampleRepository를 확인해보세요!

참고 Repository

이번 포스팅과 관련된 샘플 프로젝트의 전체 코드는 링크에서 확인하세요!
Github Repository

Gradle Settings.

모든 Library의 사용을 위한 첫번째 관문인 Gradle을 세팅해봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
// Project Level Gradle.
ext {
retrofitVersion = '2.3.0'
okhttpVersion = '3.9.0'
}
// App Level Gradle.
dependencies {
compile "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"
compile "com.squareup.retrofit2:converter-gson:$rootProject.retrofitVersion"
compile "com.squareup.okhttp3:okhttp:$rootProject.okhttpVersion"
}

Gradle 설정작업에 있어서는 개인 성향에 따라 다르겠지만,
저의 경우 2번의 Gradle 작업을 진행하였습니다.

사용하는 의존성들에 대한 버전 관리를 조금 더 편리하게 하기 위해, Project Level의 Gradleext를 추가하여, Retrofit과 OkHttp의 버전을 명시해 주었습니다.

두번째로, 실제 Library의 의존성 주입을 위해, App Level의 Gradle에 3가지의 Library를 추가해 주었습니다.

  • Retrofit
  • Retrofit-Conveter-Gson
  • OkHttp

포스팅의 주제인 RetrofitGson-Converter, 그리고 OkHttp에 대한 의존성을 추가해주었습니다.

네트워크 통신 시, XML / JSON 등 서버개발자와 협의된 방식의 데이터 포맷으로 데이터를 주고 받게 되고,
결과값으로 받은 데이터 타입을 실제 프로그램에서 사용할 수 있도록 데이터 파싱이라는 과정이 발생합니다.

이때, 일반적으로 Java에서 DTO 또는 VO등으로 불리는 데이터 클래스를 선언하여,
데이터를 파싱하고 파싱된 데이터를 구조에 맞게 작성된 해당 데이터 클래스에 저장해두곤 합니다.

이러한 일괄의 작업들을 Retrofit은 조금 더 편리하게, 그리고 자동으로 해줄 수 있도록 몇가지의 Converter를 함께 제공하고 있습니다.

그 중, 우리는 JSON 포맷을 이용할 예정이며, JSON과 관련된 몇가지의 Converter 중 Gson-Converter을 이용하도록 할 예정입니다.

그렇다면, OkHttp 모듈은 왜 함께 추가되었는지에 대한 의문이 있을 수도 있겠네요.

실질적으로 샘플 프로젝트에서는 사용하고 있지 않은 방식이지만,

요즘들어 사용자 인증과 관련된 방법 중 많이 이야기되고 있는 JSON Web Token등의 방식을 통해,

사용자에 대한 인증정보를 매 요청마다 서버로 함께 전달해 주어야 하는 경우가 발생하거나, 개발 중 실질적인 요청과 응답에 대한 로깅을 해야되는 경우가 발생합니다.

비효율적인 방법으로 Token이 필요하거나 로깅이 필요한 통신에 대해서 개발자 스스로 수작업을 통한 ctrl + c / v를 이용하여 요청 시 마다 넣어주는 방법도 있지만,

Retrofit은 개발자들에게 이러한 불편함을 없애주기라도 하겠다는 듯, OkHttpClient를 매개변수로 받는 client() method를 제공해주고 있습니다.

우리는 위와 같은 비효율적인 작업 대신에, OkHttpClient객체를 생성하여, Header정보에 Token정보를 설정해주거나, 로깅과 관련된 추가적인 Library를 프로젝트에 추가 및 설정하여 입맞에 맞게 OkHttpClient객체를 작성하고, 이렇게 작성된 객체를 Retrofit의 client() method에 설정을 해주면, ctrl + c / v를 해오던 불필요한 수작업을 손쉽게 해결할 수 있도록 도와줍니다.

이번 샘플 프로젝트와 포스팅에서는 해당 범주와는 조금 맞지 않은 주제이기에, 혹여 기회가 된다면 Token 정보를 Header에 설정하거나, 로깅을 위한 추가적인 Logger Library를 설정하는 방법도 포스팅으로 다뤄보겠습니다.

그럼 이제 각설하고, 본격적으로 Retrofit을 추가하고, 어떻게 사용하는지 살펴보도록 합시다!

Retrofit Builder 설정과 Retrofit 객체 생성.

실질적인 통신에 앞서, Retrofit을 이용한 REST API 통신을 위해서는 2가지의 선행 작업이 필요합니다.

  • Retrofit 객체 생성.
  • REST API 명세에 맞는 Interface 선언.

해당 섹션에서는 Retrofit을 사용하기 위한 Retrofit에서 제공하는 Builder객체를 이용한 몇가지 설정과 해당 Builder객체를 이용한 실질적인 Retrofit 객체를 생성하는 방법을 다뤄보겠습니다.

1
2
3
4
Retrofit retrofit = new Retrofit.Builder()
.baseURL("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();

위의 코드를 하나하나 살펴보도록 합시다.

우선 Retrofit 객체를 저장할 Retrofit Type의 변수를 하나 선언해주었고,

Retrofit에서 제공하고 있는 Builder객체를 이용하여 몇가지 설정을 해주었습니다.

baseURL() method를 이용하여, 어떤 서버로 네트워크 통신을 요청할 것인지에 대한 설정과, addConverterFactory() method를 이용하여,

통신이 완료된 후, 어떤 Converter를 이용하여 데이터를 파싱할 것인지에 대한 설정입니다.

우리는 위에서 Gson-Converter를 이용하도록 할 것이라고 언급하였고, 관련 Library를 추가해주었으니, Gson-Converter를 설정하였습니다.

추가적인 설정을 더 지정할 수 있지만, 우선은 이것만으로도 통신이 가능하므로 추가적인 설정에 대한 내용은 skip 하였습니다.

마지막으로, build() method를 통해, Retrofit.Builder객체에 설정한 정보를 이용하여 실질적으로 Retrofit 객체를 만들어 반환해주도록 하였습니다

아! 여기서 한가지 주의하셔야 할 점이 있는데요.

공식문서상에는 baseURL()에 설정된 서버 URL에 /가 없습니다.

하지만 실질적으로 여러가지 통신을 하면서 로깅을 해보면 baseURL()/가 없을 경우, 요청 되어야 할 URL의 일부가 잘려나가는 현상이 발생하여 잘못된 경로로 요청하는 현상이 발생할 수 있습니다.

그러니 꼭 baseURL()에 설정되는 URL의 마지막 경로에는 /를 함께 포함해 주도록 해주세요!

API 규격에 맞는 Interface 선언.

위에서 언급했던 대로, 실질적인 통신 전에 선행되어야 할 두번째 작업입니다.

우리는 네트워킹과 관련된 코드를 작성하기에 앞서, 서버 개발자와 사전에 협의된 REST API 명세에 맞게 Interface를 선언해주어야 합니다.

1
2
3
4
public interface GithubService {
@GET("users/{user}/repos")
Call<JsonArray> getUserRepositories(@Path("user") String userName);
}

간단하죠? 그럼 해당 코드도 위에서부터 차근차근 한번 살펴보도록 하죠.

코드를 살펴보기에 앞서, Retrofit을 처음 사용하는 분에게는 조금은 생소한 키워드도 있을 법 합니다.

우선 처음보는 키워드를 언급하고 코드를 살펴보면 조금 더 쉬운 이해를 도울 수 있을 것 같습니다.

  • @GET / @Path Annotation
  • Call<JsonArray>

@GET / @Path와 같이 @가 붙은 키워드는 흔히 Java 언어에서 사용되는 Annotation 키워드 입니다.

Retrofit은 REST API 통신에 필요한 요소들에 대해서 Annotation을 만들어 두었습니다.

@GET Annotation의 경우, HTTP 통신에 사용되는 method 중 GET 요청을 하겠다라는 것을 명시해주는 Annotation이며,

@Path Annotation의 경우, URL부분 중 일부의 경로가 필요에 따라 동적으로 바인딩 되어야 하는 경우 @Path Annotation을 지정하여, 문자열을 정의 해두면 해당 문자열을 URL부분 일부에 바인딩하여 동적으로 URL을 만들 수 있도록 도와주는 Annotation 입니다.

Call<JsonArray>의 경우는 해당 코드를 추적하여 들여다 보면 Retrofit이 Call<T> 형태의 Generic Type을 매개변수로 받는 Callback Interface인 것을 알 수 있습니다.

조금은 생소했던 키워드들이 눈에 들어오기 시작하였나요? 그럼 다시 코드를 한번 살펴보죠.

우선 method를 선언하는 상단부에, @GET Annotation을 이용하여 해당 method를 구현하는 요청은 HTTP GET 요청을 할 것이라고 명시를 해주었고,
@GET Annotation 바로 뒤에 ()를 이용하여 실질적으로 요청할 API의 URL을 선언해주었습니다.

요청 URL의 경로를 https:// 형태로 정의하지 않은 이유는, 우리는 위에서 선행작업을 진행하며 Retrofit객체에 이미 어떤 서버로 요청을 할 것인지 명시를 해두었기 때문에,

해당 Retrofit의 객체와 해당 Interface의 method를 이용하여 통신을 구현하면 https://api.github.com/users/{user}/repos URL이 합쳐진 완성된 URL인 https://api.github.com/users/{user}/repos URL로 요청을 할 수 있게 됩니다. 그렇기에 Interface에 정의되는 URL은 서버 URL을 제외한 나머지 URL만 작성하면 됩니다.

하단부에는, String 데이터 하나를 매개변수로 전달 받는 Call<JsonArray> 타입을 리턴하는 메서드를 선언해주었습니다.

위에서 언급했던 대로 Call<JsonArray>는 Retrofit이 미리 선언해둔 Call<T>형태의 Callback Interface 인데, 추후에는 실질적으로는 구조에 맞게 작성된 데이터 클래스를 지정하도록 하겠지만, 현재는 빠르게 Retrofit을 사용해보기 위해 JsonArray 타입을 지정하여 Callback으로 JsonArray 타입의 데이터를 전달 받을 수 있도록 그대로 지정하였습니다.

그리고, method의 매개변수에 드디어 위에서 언급했던 @Path Annotation이 있군요.

위에서 잠시 언급하였지만, @Path Annotation은 URL의 특정부분에 문자열을 바인딩해준다고 했습니다.

우리는 해당 method를 구현할 때, String 데이터 하나를 매개변수로 전달하게 되며, 이렇게 전달된 문자열은 @GET Annotation에 정의되어 있는 URL 중, {}로 묶인 {user} 부분에 해당 문자열을 매칭하여 바인딩해주게 됩니다.

그렇다면 우리는 동적인 URL을 이용하여 통신하기 위해서는 @GET / @POST Annotation의 URL 선언부에 {}로 동적으로 변경되어야 할 URL의 일부를 묶어주면 된다는 것을 알게 되었습니다.

Interface의 선언은 이것으로 끝입니다!

이렇게 잠깐 살펴보았지만, 굉장히 직관적이다라는 생각이 들지 않나요?

Retrofit은 HTTP 통신에 필요한 요소들에 대해 많은 Annotation을 제공하고 있으며,

이러한 Annotation들을 조합하여, 조금 더 네트워크 통신에 대한 코드를 직관적이고 편리하게 사용할 수 있도록 제공해주고 있습니다.

이번 포스팅에서는 Retrofit이 제공하는 Annotation 모두를 다루기에는 너무 방대하므로,

포스팅을 작성해나가며 새롭게 등장하는 Annotation이 있다면 그때 그때 필요에 따라 해당 Annotation에 대한 설명을 붙여나가도록 하고,

조금 더 자세히 Retrofit이 제공하는 Annotation에 대해 알고싶으신 분들에게는 아쉽지만, 개인의 몫으로 남겨두도록 하겠습니다.

클라이언트 객체 생성 및 요청.

Retrofit 객체 생성과 API명세에 맞는 Interface를 선언하였다면 통신을 위한 모든 준비는 완료되었습니다.

이제 남은 것은 해당 Retrofit 객체와, 선언된 Interface를 이용하여 실제 HTTP 통신을 요청하고 결과를 받는 작업이겠지요?

1
2
3
4
5
6
7
8
9
10
11
12
13
GithubService service = retrofit.create(GithubService.class);
Call<JsonArray> request = service.getUserRepositories("dev-juyoung");
request.enqueue(new Callback<JsonArray>() {
@Override
public void onResponse(Call<JsonArray> call, Response<JsonArray> response) {
// Code...
}
@Override
public void onFailure(Call<JsonArray> call, Throwable t) {
// Code...
}
});

Retrofit객체의 create() method와 미리 정의해둔 Interface를 이용하여 실질적으로 사용할 클라이언트 객체를 생성합니다.

이렇게 생성된 클라이언트 객체를 이용하여 요청을 보내고 받아온 응답을 가지고 특정 작업을 하고나면 해당 Retrofit객체의 사용은 끝이나게 되지요.

위의 예제코드에서는 생성된 클라이언트 객체가 제공하는 enqueue() method를 이용하여 요청을 처리하였고, 해당 method에 익명의 Callback Interface를 구현하여 주었습니다.

해당 Callback Interface는 onResponseonFailure method를 구현해주어야 하며, method의 명에서도 이미 알 수 있듯,

onResponse()에서는 요청이 성공된 경우의 처리를, onFailure()에서는 요청이 실패된 경우의 처리를 해주면 됩니다.

한가지 주의하여야 할 점이라면,

제 경험상 onFailure()의 경우 정말 요청이 실패된 경우에 한해서만 호출이 됩니다.

즉, 여기서 말하는 정말 요청이 실패된 경우라 하면,

요청한 API 서버의 다운 / DB Query 중 오류 등와 같은 서버의 비정상적인 동작으로 인해 요청에 대한 응답을 받지 못하는 경우를 말합니다.

또한 onResponse()를 호출하였다 하더라도, 무조건적인 성공이 보장되지는 않는다는 것 입니다.

기획 또는 서버 개발자의 의도에 따라 서버에서 40x등의 응답코드를 전송할 경우라 할지라도, 실질적으로는 onResponse()를 호출합니다.

따라서, onResponse() method 에서는 두번째 매개변수로 전달되는 response를 잘 활용하여,

해당 응답이 20x의 응답코드를 가진 정상적인 응답인지, 40x등의 응답코드를 가진 요청은 정상적이였으나 실패된 응답인지 판단하는 추가적인 코드들은 필요할 수 있습니다.

위에서 언급한 40x등의 응답코드를 가졌을 경우, 추가적으로 필요한 Error 핸들링은 추후 자체적인 테스트용 API 서버를 구축한 뒤 포스팅으로 한번 다뤄보겠습니다.

동기 vs 비동기.

현재 이 글을 보고 계신 개발자 분이라면, 정확히 어떤 뜻을 가지고 있는지는 모를 수 있지만

동기식과 비동기식 방식에 대한 용어는 익히 들어 보셨을 것 같습니다.

추가적으로 설명하기 위해 추가한 해당 섹션에서는 Retrofit을 이용하는 경우, 동기식 방식의 통신과 비동기식 방식의 통신을 어떻게 처리하는지에 대해 언급하고자 추가하였습니다.

우선 자주 사용 될 법한 비동기식 방식의 통신은 위에서 이미 사용해 본 enqueue() method를 이용하는 것 입니다.

이미 코드를 작성하며 느끼셨을 수도 있지만, 매개변수로 Callback Interface를 구현하는 익명객체를 넘겨준다는 것만 보셔도 비동기 통신이구나라는 느낌이 오시지 않았었나요?

그렇다면 반대로 동기식 방식의 통신을 하려면 어떻게?

고민할 필요도 없이 간단합니다. 생성된 클라이언트 객체가 제공하고 있는 execute() method를 enqueue() method 대신 이용하면 됩니다. 간단하죠?

하지만 제가 진행했던 프로젝트의 특성일 수도 있지만, 개인적인 경험상 execute() method를 몇번이나 쓸지는 의문이긴 합니다.

위의 예제 코드에서도 동기식 방식의 통신인 execute() method의 사용법을 소개하지 않았던 이유는,

제 개인적인 경험상 동기식 통신을 할 상황이 별로 없었다는 첫번째 이유와,

Android의 철학(?) 상,
UI Thread가 일시적으로 멈추는 현상을 사용자에게 제공하는 것은 좋지 않은 사용자 경험을 제공하는 것이므로,
시간이 오래 걸리는 작업의 경우 새로운 Thread를 생성하여 UI Thread가 멈추는 현상을 방지하여야 한다라는 것인데요.

네트워크 통신의 특징 상, 사용자의 네트워크 환경에 따라 시간이 얼마나 걸리는 작업인지 알 수 없습니다.

제가 생각했을 때, 네트워크 통신의 경우 시간이 오래 걸리는 작업이라는 판단과, 대부분의 App은 아마 비동기식 방식의 통신을 하고 있을 것이다!라는 개인적인 추측에 의해 굳이 언급하지 않았습니다.

그리고 Android의 네트워크 통신을 검색해 보시면, 많은 블로그에서 이 포스팅의 서두에서 언급했던 Android Framework의 내장 객체인 AsyncTaskHttpURLConnection를 이용한 통신에 대한 정보가 수두룩하게 나올 정도로, 대다수의 개발자분들이 비동기식 방식의 통신을 하고 있다라는 판단도 있었구요!

하지만 정말 필요에 의해 동기식 방식의 통신이 이뤄질 수 있을 수도 있으므로, 단순히 method명이라도 소개해드리고자 해당 섹션을 추가하였습니다!

마무리

자주 포스팅을 작성하진 않는 편이지만.

역시, 글을 쓰다보면 언제나 조금만 더…, 조금만 더 쉽게…라는 욕심이 생기나 봅니다.

단순히 Retrofit을 설정하고 간단하게 사용해보는 방법을 언급하려던 글이,

개인적으로 조금 더 좋은 포스팅을 전달해 드리고자 몇번이고 포스팅을 Review하다보니 굉장히 길어졌네요…ㅠㅠ

여튼! 이번 포스팅에서는 Retrofit을 설정하고, 간단한 기본 사용법과 처음 사용하시는 분들에게는 조금은 생소할 수 있는 키워드들에 대해 하나씩 코드를 살펴가며 정리를 해보았습니다.

이미 느끼셨겠지만,

Retrofit은 개발자가 처리하여야 할 필수적인 작업을 대신 처리하여 개발자의 생산성을 높여주고, HTTP 통신에 필요한 요소들을 Annotation으로 제공해주기에

이를 잘 활용하면 서버 개발자가 미리 작성해둔 여러가지의 API를 굉장히 손쉽게 접근하고 구현할 수 있도록 해주는 편리한 도구 같지 않나요?

개인적인 견해이나, Retrofit이 이렇게 해질 수 있었던 이유는 불필요한 코드를 제거할 수 있도록 하여 개발자의 생산성을 높여주는 이점도 있지만,

Annotation을 이용하여 통신에 필요한 코드를 간결하게 작성할 수 있도록 제공하여, 코드를 처음보는 개발자도 코드를 쉽게 이해하도록 하고,

단순히 Interface의 선언부만 보는 것 만으로도 해당 코드가 어떤 통신의 역할을 하는지 알 수 있도록 해주는 것이 한 몫하지 않았나? 라는 생각이 듭니다.

아무래도, 글을 작성하다보니 개인적인 의견이 많이 들어가게 되었고, 제가 사용하는 용어들이 다른 개발자분들에게는 조금 생소한 용어일지도 모르겠습니다.

혹여 포스팅을 읽으시다가 잘못된 용어와 설명을 발견하신다면 하단의 댓글에 질타와 조언을 부탁드립니다!

다음 포스팅에서는 Retrofit을 조금 더 편리하게 관리하고 사용할 수 있도록, 저도 모르게 은연중에 생성 했던 Class와 유사한 방법으로 작성된 FutureStudio Blog에서 소개하고 있는 ServiceGenerator Class를 작성해보는 포스팅으로 찾아뵙도록 하겠습니다.