본문 바로가기

Android/Basic

Retrofit2: 기본 사용

최신화: 2023.11.29~30

‣ Intro

Retrofit은 RESTful API를 사용 중인 안드로이드 개발자에게 매우 도움이 되는 라이브러리다.

이번 글에서는 Retrofit의 개념에 대해 공부해보자.


‣ Retrofit2?

Retrofit2는 Android 및 Java 어플리케이션에서 사용되는 Type-Safety HTTP 클라이언트 라이브러리다.

Square에서 개발되었으며, RESTful API 통신을 단순화하여 앱의 성능을 개선하는데 중점을 두고 있다.

정말 필요해서 Ktor같은 다른 웹 서비스 라이브러리를 사용하는 경우를 제외하면, 안드로이드의 통신분야에서 가장 많이 사용되는 라이브러리다.

그럼 왜 이렇게 Retrofit이 안드로이드 개발자들에게 각광을 받을까??

다음과 같은 Retrofit의 주요 특징들 덕분이다.

  1. 타입 안전
    • Retrofit은 인터페이스와 어노테이션을 사용하여 HTTP API를 설명한다.
    • 이를 통해 컴파일 시간에 오류를 감지할 수 있다.
  2. 데이터 변환기
    • Retrofit은 다양한 데이터 컨버터를 지원한다.
    • Gson, Moshi, Jackson 등의 라이브러리와 함께 사용하여 JSON 응답을 객체로 자동 변환할 수 있다.
  3. 동기 및 비동기 요청
    • Retrofit은 동기 및 비동기 방식으로 네트워크 요청을 처리할 수 있다.
    • 비동기 요청은 Call 객체의 enqueue 메서드를 사용하여 처리된다.
  4. 어노테이션 기반의 API 정의
    • Retrofit은 HTTP 메서드(@GET, @POST, @PUT 등)와 요청 매개변수(@Query, @Path, @Body 등)를 지정하기 위한 다양한 어노테이션을 제공한다.
  5. OkHttp 통합
    • Retrofit는 내부적으로 OkHttp 라이브러리를 사용하여 네트워크 요청을 처리한다.
    • 이는 효율적인 연결 재사용, 요청/응답 인터셉터, 캐싱 등의 기능을 제공한다.
  6. 커스터마이징
    • Retrofit은 인터셉터, 컨버터, 콜 어댑터 등을 사용하여 동작을 쉽게 커스터마이징 할 수 있다.

‣ Retrofit2 기본 사용법

1. Retrofit 인스턴스 생성

Retrofit을 사용하기 위해서는 먼저 인스턴스를 생성해야한다.

Retrofit 인스턴스는 내부적으로 내부적으로 OkHttp를 사용한다.

OkHttp 클라이언트는 커넥션 풀, 캐시, Reqeust/ Response/ Intercepter 등을 관리하는데, 이런 리소스들을 효율적으로 관리하기 위해서는 초기화 되지 않고 재사용되어야한다.

Retrofit이 재사용되면 OkHttp도 재사용되기 때문에, 특별한 이유가 없다면 전역적으로 한번만 생성해서 재사용하는 싱글톤 패턴으로 작성하는 것이 좋다.

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(GsonConverterFactory.create()) // 컨버터.. 글의 하단에서 설명
    .build()

2. API 인터페이스 정의

API 호출을 정의하기 위해 인터페이스를 사용한다.

각 메서드에는 HTTP 동작을 나타내는 어노테이션(@GET, @POST 등)이 포함되어야 한다.

interface ApiService {
    @GET("users/{user}")
    fun getUser(@Path("user") user: String): Call<User>
}

3. 어노테이션을 사용한 HTTP 메서드와 요청 매개변수 저장

Retrofit에서는 다양한 어노테이션을 사용하여 요청의 종류와 매개변수를 지정할 수 있다.

이를 통해 개발자는 복잡한 코드 없이 API의 endpoint와 파라미터를 정의할 수 있다.

  • HTTP 메서드:
    • @GET - GET 요청, 주로 데이터를 검색할 때 사용한다.
    • @POST - POST 요청, 주로 데이터를 서버에 전송할 때 사용한다.
    • @PUT - PUT 요청, 서버의 데이터를 갱신할 때 사용한다.
    • @DELETE - DELETE 요청, 서버의 데이터를 삭제할 때 사용한다.
  • URL 매개변수
    • @Path - URL 일부를 동적으로 변환하는데 사용된다.
      예를 들어, @GET("users/{id}")와 같이 경로에 변수를 지정하고, 메서드 파리미터에서
      @Path("id") int id로 값을 전달 할 수 있다.
  • 쿼리 매개변수
    • @Qury - URL에 쿼리 파라미터를 추가하는데 사용된다
      ex) @Query("sort") string order?sort=orderValue 형태로 URL에 추가된다.
    • @QueryMap - 여러 쿼리 파라미터를 한 번에 전달하기 위해 사용된다. Map 객체를 사용해 key-value 쌍의 파라미터를 전달 가능하다.
  • 헤더
    • @Headers - 요청에 정적 헤더를 추가한다. 여러 헤더를 배열 형태로 전달할 수 있다.
    • @Header - 요청에 동적 헤더를 추가한다. 메서드의 파라미터로 전달된 값을 헤더에 추가한다.
  • 요청 본문
    • @Body - POST나 PUT 요청에서 요청 본문을 전송할 때 사용한다. 주로 객체를 JSON 형식으로 변환함여 서버에 전송하는 데 사용한다.

4. 응답 처리 및 데이터 변환

4-1) Call 를 사용해 동기 및 비동기 방식으로 응답 처리하기

해당 방법은 최근에는 찾아보기 힘든 방식이다. 그냥 이런것도 있구나 하고 읽고 넘어가면 될 듯하다.

  • 동기 호출(Synchronous call):
    *
    Call.execute() 를 사용하여 동기적으로 API 호출.
    이 방식은 메서드가 호출되었을 때 해당 메서드의 실행이 완료될 때까지 호출한 쪽에서 대기하는 방식이다.
    즉, 완료될 때까지 코드가 다음으로 안넘어간다.
    때문에 UI스레드(= Main 스레드)에서 호출하면 안되고 백그라운드 스레드(I/O를 권장)에서 실행되어야 한다.
  • 비동기 호출(Asynchronous call):
    Call.enqueue() 메서드를 사용하여 비동기적으로 API를 호출한다.
    이 방식은 메서드를 호출하고 바로 다음 코드로 넘어간다. API 호출 결과는 Callback 메커니즘을 통해 전달받는다.
interface ApiService {
    @GET("users/{id}")
    fun getUser(@Path("id") userId: Int): Call<User>
}

이렇게 정의된 인터페이스를 사용하여 API를 호출할 때는 다음과 같이 처리한다.

val apiService = retrofit.create(ApiService::class.java)
val call = apiService.getUser(1)
call.enqueue(object : Callback<User> {
    override fun onResponse(call: Call<User>, response: Response<User>) {
        if (response.isSuccessful) {
            val user = response.body()
            // TODO: 사용자 정보 처리
        } else {
            // TODO: 에러 처리
        }
    }

    override fun onFailure(call: Call<User>, t: Throwable) {
        // TODO: 네트워크 오류 또는 요청 실패 처리
    }
})

Call<T>를 사용하면 콜백 메커니즘을 통해 비동기적으로 API응답을 처리해야한다.

4-2) Coroutine을 사용해 응답 처리하기

코루틴은 동시성을 쉽게 처리하기 위한 기능이다.

Retrofit과 코루틴을 함께 사용하면 동기적인 방식의 코드임에도 비동기적인 효과를 얻을 수 있다.

필자가 가장 애용하는 방식이자, 가장 추천하는 응답 처리 방식이다.

코루틴을 Retrofit과 사용할 때의 장점은 다음과 같다.

  • 간결한 코드: Callback을 사용하지 않고 직관적인 순차적 코드로 비동기 로직을 작성할 수 있다. 때문에 가독성이 좋다.
  • 비블로킹: 코루틴은 비블로킹이기 때문에, UI 스레드에서도 네트워크 요청과 같은 시간이 오래 소요되는 작업을 수행할 수 있다.
  • 코루틴 빌더: launch, async, withContext 등의 코루틴 빌더를 사용하여 다양한 동시성 패턴을 적용할 수 있다.
  • 예외 처리: try-catch를 사용하여 예외 처리도 쉽게 가능하다.

Retrofit 2.6.0 버전 이후로 코루틴을 직접 지원하기 때문에, API 인터페이스 메서드에서 suspend키워드로 코루틴 함수를 정의 가능하다.

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") userId: Int): Response<User>
}

위처럼 Thread가 포함된 복잡한 코드나 별도의 동기/비동기 처리 없이 간결하게 API요청과 응답을 처리할 수 있다.

저 Response 뒤의 홑화살괄호('<>')에 감싸진 User는 Api호출 결과로 반환되는 데이터를 표현하기 위해 사용하는 클래스이다.

위 경우, getUser()는 User 클래스의 인스턴스를 담고 있는 Response 객체를 반환한다.

4-3) Response 객체를 통해 HTTP 응답 코드, 헤더, 본문 등의 정보에 엑세스 하는 방법

아래는 카카오 책 검색 API의 Response 데이터다.

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
  "meta": {
    "is_end": true,
    "pageable_count": 9,
    "total_count": 10
  },
  "documents": [
    {
      "authors": [
        "기시미 이치로",
        "고가 후미타케"
      ],
      "contents": "인간은 변할 수 있고, 누구나 행복해 질 수 있다. 단 그러기 위해서는 ‘용기’가 필요하다고 말한 철학자가 있다. 바로 프로이트, 융과 함께 ‘심리학의 3대 거장’으로 일컬어지고 있는 알프레드 아들러다. 『미움받을 용기』는 아들러 심리학에 관한 일본의 1인자 철학자 기시미 이치로와 베스트셀러 작가인 고가 후미타케의 저서로, 아들러의 심리학을 ‘대화체’로 쉽고 맛깔나게 정리하고 있다. 아들러 심리학을 공부한 철학자와 세상에 부정적이고 열등감 많은",
      "datetime": "2014-11-17T00:00:00.000+09:00",
      "isbn": "8996991341 9788996991342",
      "price": 14900,
      "publisher": "인플루엔셜",
      "sale_price": 13410,
      "status": "정상판매",
      "thumbnail": "https://search1.kakaocdn.net/thumb/R120x174.q85/?fname=http%3A%2F%2Ft1.daumcdn.net%2Flbook%2Fimage%2F1467038",
      "title": "미움받을 용기",
      "translators": [
        "전경아"
      ],
      "url": "https://search.daum.net/search?w=bookpage&bookId=1467038&q=%EB%AF%B8%EC%9B%80%EB%B0%9B%EC%9D%84+%EC%9A%A9%EA%B8%B0"
    },
    ...
  ]
}

Response 객체에는 서버에서 받은 응답 코드가 있다. 보통 이런 응답 코드를 통해 요청이 성공했는지 실패 했는지를 파악한다.

보통 200(성공), 400(잘못된 접근), 404(페이지를 못찾음) 등으로 반환해 준다. 하지만 이건 회사나 API를 제공하는 단체마다 다를 수 있다. 본인이 근무 중인 개발팀은 0~20의 숫자를 응답 코드로 사용 중이다.
response.code()를 통해 확인 가능하다.

헤더(Header) 정보는 수신한 정보에 대한 추가적인 설명이다.

예를 들면, 그 정보가 언제 만들어 졌는지, 어떤 타입인지 등의 정보를 포함한다.
response.headers()를 통해 헤더 정보를 볼 수 있다.

본문(Body)는 우리가 필요한 진짜 정보다.

위 예시를 보면, 특정 책에 대한 저자, 가격, 출판사 등의 내용을 포함하고 있다.

보통 response.body()를 사용해서 본문의 내용을 볼 수 있다.

**4-4) Converter를 사용하여 JSON 응답을 객체로 변환하는 방법**

위의 카카오 책검색 API 결과처럼, 대부분의 API 결과는 JSON 응답으로 내려온다.

그럼 왜 JSON 응답을 객체로 변환해서 사용해야 할까?

이유는 크게 세 가지로 요약할 수 있다.

1. 데이터 사용의 용이성

  • JSON은 텍스트 기반의 데이터 포맷으로, 프로그래밍 언어와 독립적이다. 그래서 JSON 형식의 데이터를 직접 파싱하고 관리하기는 복잡하고 시간이 많이 소요되는 비효율적인 일이다.
  • 객체로 변환하면, 개발할 때 쉽게 데이터에 접근하고 필요한 정보를 추출하거나 수정할 수 있게 된다.
  • 예를 들면 Kotlin에서는 클래스의 필드와 메소드를 사용해서 데이터를 쉽게 다룰 수 있다.

2. 타입 안정성과 오류 방지

  • 객체 변환을 통해 데이터의 타입을 명확히 할 수 있고, 이는 잘못된 타입의 데이터 사용으로 발생하는 오류를 방지할 수 있다.

3. 유지보수가 쉽다

  • 객체로 변환하면, 데이터 구조가 클래스나 구조체의 형태로 명확하게 정의된다. 이는 코드를 이해하기 쉽게 해주어 협업에 있어서도 별도의 코드 설명 없이 데이터 구조를 빠르게 파악 할 수 있도록 한다.
  • 또한, 데이터 구조에 변화가 있으면, 해당 객체의 클래스나 구조체만 수정하면 되므로 편하다.

그럼 어떻게 JSON을 객체로 변환할 수 있는가?

보통 안드로이드 개발 시에는, Gson ConverterMoshi Converter를 사용한다.

각 라이브러리에 대한 설명은 다른 글에서 다루도록 하고, 이번 글에서는 Converter를 추가하는 방법만 파악하겠다.

여기서는 가장 많이 쓰이는 Gson을 예시로 설명한다.

우선 Retrofit 인스턴스에 GsonConverterFactory를 추가해야한다.

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .build()

이게 가장 기본적인 Retrofit 빌더의 모습이다.

우리는 여기에 .addConverterFactory(GsonConverterFatory.create())를 추가한다.

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(GsonConverterFactory.create()) // 이렇게 추가한다.
    .build()

다음으로는 JSON 응답을 받을 클래스(데이터 모델)을 정의한다.

이 클래스의 필드는 JSON 응답의 키와 일치해야 한다.

일반 클래스도 상관 없지만, 필자는 Data class를 권장한다.

hashCode, toString, copy등의 몇 가지 이유가 있지만, 데이터 클래스가 일반 클래스 보다 데이터를 저장하고 관리하는데에 특화되어 있다는 점만 알고 일단 넘어가자.

위 책검색 API 호출 결과 중 documents 처리하는 데이터 클래스를 만들면 다음과 같다.

data class Book(
    val authors: List<String>,
    val contents: String,
    val datetime: String,
    val isbn: String,
    val price: Int,
    val publisher: String,
    val sale_price: Int,
    val status: String,
    val thumbnail: String,
    val title: String,
    val translators: List<String>,
    val url: String
)

마지막으로 API 인터페이스 메소드의 반환 타입을 Call로 설정하면, Retrofit과 Gson이 자동으로 JSON응답을 지정한 DataModelClass 객체로 변환한다.

interface BookSearchApi {
    @Headers("Authorization: KakaoAK $API_KEY")
    @GET("search/book")
    suspend fun searchBooks(
        @Query("query") query: String,
        @Query("sort") sort: String,
        @Query("page") page: Int,
        @Query("size") size: Int,
    ): Call<Book>
}

대충 이런 느낌이다.

설명을 위해 작성한 코드라 실제 동작 여부는 보장 못한다.


‣ Outro

정신이 없는 상황에서 새로 작성해서 뭔가 글이 중구난방으로 써진 느낌이다. 때문에 중간중간 내용이 수정될 수 있다.

다음 글에서는 필자가 추천하는 사용방법으로 돌아오겠다.