Retrofit이란?
Retrofit은 스퀘어에서 만든 HTTP 통신을 간편하게 만들어주는 라이브러리
Retrofit은 1 버전과 2 버전이 있지만 Retrofit2가 2015년에 나왔으므로 지금 Retrofit이라고 하면 대부분 Retrofit2라고 봐도 무방
따라서 이 책에서 소개하는 Retrofit은 Retrofit2를 의미
retrofit 소개
Retrofit
A type-safe HTTP client for Android and Java
square.github.io
Retrofit을 이용하려면 먼저 프로그램의 구조를 이해해야 한다

Retrofit은 네트워크 통신 정보만 주면 그대로 네트워크 프로그래밍을 대신 구현해 줌
위의 그림에서 interface는 코틀린의 interface 키워드로 직접 만들어야 함
그리고 인터페이스의 함수는 통신할 때 필요
즉, a(), b() 함수를 호출해서 통신하겠다는 의미
그런데 인터페이스에는 함수를 선언만 하며 통신할 때 필요한 어떤 코드도 담지 않음
이렇게 만든 인터페이스를 Retrofit에 알려주면 인터페이스 정보를 보고 실제 통신할 때 필요한 코드를 담은 Service 객체를 만들어줌
* Service: 안드로이드 컴포넌트가 아니라 통신을 하게 해준다는 의미의 서비스
Retrofit은 우리가 알려준 인터페이스를 바탕으로 서비스를 만드므로 인터페이스에 선언한 함수를 그대로 포함
이 서비스의 함수를 호출하면 Call 객체를 반환하는데, 이 Call 객체의 enqueue() 함수를 호출하는 순간 통신을 수행
지금까지 설명한 Retrofit 동작 방식 정리
- 통신용 함수를 선언한 인터페이스 작성
- Retrofit에 인터페이스 전달
- Retrofit이 통신용 Service 객체 반환
- Service의 통신용 함수를 호출한 후 Call 객체 반환
- Call 객체의 enqueue() 함수를 호출하여 네트워크 통신 수행
구현
권한 추가
앱에서 네트워크 통신을 구현하려면 우선 매니페스트 파일에 다음처럼 퍼미션을 선언해야 함
<uses-permission android:name="android.permission.INTERNET"/>

라이브러리 선언
Retrofit을 이용하려면 빌드 그래들의 dependencies 항목에 다음처럼 라이브러리를 등록해야 함
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.google.code.gson:2.8.6")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
첫 번째 라이브러리는 Retrofit을 이용할 때 필수이며 나머지 2개는 다른 라이브러리를 이용할 수도 있음
Retrofit은 JSON이나 XML 데이터를 모델(VO 클래스) 객체로 변환해 주는데 이때 JSON, XML을 파싱하는 라이브러리가 필요
따라서 구글에서 만든 gson 라이브러리를 사용하고자 com.google.code.gson:gson을 등록했으며,
Retrofit에서 gson을 이용해 모델 객체로 변환해 주는 com.squareup.retrofit2:converter-gson을 함께 등록
만약 파싱 라이브러리가 바뀌면 converter 라이브러리도 그에 맞게 바꿔야 함
Retrofit은 다음과 같은 converter 라이브러리를 제공
- Gson : com.squareup.retrofit2:converter-gson
- Jackson : com.squareup.retrofit2:converter-jackson
- Moshi : com.squareup.retrofit2:converter-moshi
- Protobuf : com.squareup.retrofit2:converter-protobuf
- Wire : com.squareup.retrofit2:converter-wire
- Simple XML : com.squareup.retrofit2:converter-simplexml
- JAXB : com.squareup.retrofit2:converter-jaxb
- Scalars(primitives, boxed, String) : com.squareup.retrofit2:converter-scalars
모델 클래스 선언
모델 클래스란?
모델 클래스란 서버와 주고받는 데이터를 표현하는 클래스
흔히 VO(value-object) 클래스라고도 하며 JSON, XML 데이터를 파싱해 모델 클래스 객체에 담는 것을 자동화해 줌
만약 서버에서 넘어오는 JSON 데이터가 다음과 같다고 가정해 보자
{
"id": 7,
"email": "michael.lawson@reqres.in",
"first_name": "Michael",
"last_name": "Lawson",
"avatar": "https://reqres.in/img/faces/7-image.jpg"
}
원래는 JSON 데이터를 코드에서 직접 파싱해서 이용해야 하는데, 데이터를 담을 모델 클래스를 선언하고 클래스 정보만 알려 주면 모델 클래스의 객체를 알아서 생성하고 그 객체에 데이터를 담아줌
모델 클래스는 상위 타입에 제약이 없으므로 어떤 클래스를 상속받거나 인터페이스를 구현할 필요는 없음. 순전히 개발자가 작성하는 클래스임
앞의 JSON 정보를 담을 모델 클래스는 다음처럼 작성 가능
- 모델 클래스
data class UserModel (
var id: String,
@SerializedName("first_name")
var firstName: String,
// @SerializedName("last_name")
var lastName: String,
var avatar: String,
var avatarBitmap: Bitmap
)
모델 클래스의 프로퍼티에 데이터가 자동으로 저장되는 기본 규칙은 데이터의 키와 프로퍼티 이름을 매칭하는 것
예를 들어 id라는 키값은 id 프로퍼티에 저장됨
만약 키와 프로퍼티 이름이 다를 때는 @SerializedName이라는 애너테이션으로 명시
코드에서 주석으로 지정한 @SerializedName("first_name")의 의미는 first_name이라는 키의 데이터가 firstName 프로퍼티에 저장된다는 의미
그런데 키와 프로퍼티 이름이 다르더라도 밑줄 다음에 오는 단어의 첫 글자를 대문자로 바꾼 프로퍼티명이 있을 때는 @SerializedName 애너테이션을 사용하지 않아도 됨
예를 들어 키가 last_name이면 자동으로 lastName 프로퍼티에 저장됨
모델 클래스를 만들 때 서버의 데이터와 상관없는 프로퍼티를 선언해도 됨
위의 코드를 보면 avatarBitmap이라는 프로퍼티를 선언했는데 서버로부터 넘어오는 JSON에는 이와 관련된 데이터가 없음
이처럼 모델에 서버 연동과 상관없는 데이터를 담는 프로퍼티를 선언해도 됨
서버의 데이터가 복잡할 때는 모든 데이터를 하나의 모델 클래스로 표현하지 않고 여러 클래스로 분리한 후 조합해서 사용 가능
예를 들어 목록 화면을 구성하는 서버 데이터가 다음처럼 전달된다고 가정
{
"page": 2,
"per_page": 6,
"total": 12,
"total_pages": 2,
"data": [
{
"id": 7,
"email": "michael.lawson@reqres.in",
"first_name": "Michael",
"last_name": "Lawson",
"avatar": "https://reqres.in/img/faces/7-images.jpg"
},
{
"id": 8,
"email": "lindsay.ferguson@reqres.in",
"first_name": "Lindsay",
"last_name": "Ferguson",
"avatar": "https://reqres.in/img/faces/8-image.jpg"
}
],
}
이 데이터를 하나의 모델 클래스에 담아도 되지만 data 키값을 저장하는 UserModel 클래스와 전체 페이지 정보를 저장하는 UserListModel 클래스로 분리해 작성하고 UserListModel에서 UserModel을 이용하면 됨
- 모델 클래스 분리 이용
data class UserListModel (
var page: String,
@SerializedName("per_page")
var perPage: String,
var total: String,
@SerializedName("total_pages")
var totalPages: String,
var data: List<UserModel>?
)
Retrofit을 이용할 때 UserListModel을 알려 주면 JSON 데이터를 파싱해 프로퍼티에 저장하며 data 키값은 data 프로퍼티에 선언된 UserModel 클래스의 객체에 담아 줌
서비스 인터페이스 정의
Retrofit을 이용할 때 가장 중요한 부분 : 네트워크 통신이 필요한 순간에 호출할 함수를 포함하는 서비스 인터페이스를 작성하는 것
- 서비스 인터페이스 정의
//class가 아닌 interface!!
interface INetworkService {
@GET("api/users")
fun doGetUserList(@Query("page") page: String): Call<UserListModel>
@GET
fun getAvatarImage(@Url url:String): Call<ResponseBody>
}
INetworkService라는 이름의 인터페이스를 선언하고 그 안에 doGetUserList(), getAvatarImage() 라는 이름의 함수를 정의
그런데 이 인터페이스명과 함수명은 개발자가 지은 이름일 뿐임
이 인터페이스를 구현해 실제로 통신하는 클래스는 Retrofit이 자동으로 만들어 주는데 이때 애너테이션을 참조
즉, 함수에 선언한 에너테이션을 보고 그 정보대로 네트워크 통신을 할 수 있는 코드를 자동 생성
함수에 선언한 에너테이션
- @GET : 서버와 연동할 때 GET 방식으로 해달라는 의미
- @Query : 서버에 전달되는 데이터
- @Url : 요청 URL
결국 Retrofit을 이용할 때는 인터페이스의 애너테이션이 중요하며 이 내용은 잠시 후에 자세히 정리
Retrofit 객체 생성
Retrofit을 사용할 때 가장 먼저 Retrofit 객체를 생성하는 코드를 실행해야 함(MainActivity)
val retrofit: Retrofit
// get()은 속성에 대한 커스텀 Getter 메서드
get() = Retrofit.Builder()
.baseUrl("https://reqres.in/")
.addConverterFactory(GsonConverterFactory.create())
.build()

Retrofit 객체를 생성하는 코드는 초기 설정을 하므로 한 번만 생성하면 됨
baseUrl()함수로 URL을 설정하면 이후에 이 URL 뒤에 올 경로만 지정해서 서버와 연동 가능
예를 들어 baseUrl을 앞의 코드처럼 선언하고 어디선가 @GET("api/users")처럼 경로를 지정했다면 서버 요청 URL은 https://reqres.in/api/users 가 됨
물론 baseUrl을 선언했더라도 전혀 다른 URL로 요청할 수도 있음
그리고 addConverterFactory() 함수로 데이터를 파싱해 모델 객체에 담는 역할자를 지정해 줌
앞에서는 GsonConverterFactory.create()로 작성했으므로 GsonConverter를 이용하겠다는 의미
인터페이스 타입의 서비스 객체 얻기
Retrofit 객체를 생성한 다음에는 이 객체로 서비스 인터페이스를 구현한 클래스의 객체를 얻음
- 서비스 객체 얻기(MainActivity에 선언)
// INetworkService::class.java는 INetworkService 인터페이스에 해당하는 Java Class 객체를 가져옴
// Retrofit의 create 메서드는 Java Class 객체를 필요로 하기 때문에 이런 형태로 사용
var networkService: INetworkService = retrofit.create(INetworkService::class.java)
Retrofit의 create() 함수에 앞에서 만든 서비스 인터페이스 타입을 전달
그러면 이 인터페이스를 구현한 클래스의 객체를 반환해 줌
실제 네트워크가 필요할 때 이 객체의 함수를 호출하면 된다
네트워크 통신 시도
이제 모든 준비가 끝났으므로 네트워크 통신이 필요한 순간에 Retrofit 객체로 얻은 서비스 객체의 함수를 호출만 해주면 됨
서비스 클래스와 객체는 Retrofit이 만들어 주지만 우리가 만든 인터페이스를 구현한 클래스이므로 인터페이스의 함수를 호출하면 네트워크 통신을 시도함
- Call 객체 얻기
var userListCall = networkService.doGetUserList("1")
인터페이스에 선언한 함수를 호출하면 위 코드에서 userListCall처럼 Call 객체가 반환됨
실제 통신은 이 Call 객체의 enqueue() 함수를 호출하는 순간 이루어짐
- 네트워크 통신 수행
userListCall.enqueue(object: Callback<UserListModel> {
override fun onResponse(call: Call<UserListModel>, response: Response<UserListModel>) {
val userList = response.body()
// (... 생략 ...)
}
override fun onFailure(call: Call<UserListModel>, t: Throwable) {
call.cancel()
}
})
Call 객체의 enqueue() 함수를 호출하면 비로소 통신이 수행됨
그리고 enqueue() 함수의 매개변수로 지정한 Callback 객체의 onResponse(), onFailure() 함수가 자동으로 호출됨
- onResponse() 함수 : 통신 성공 시 호출
- onFailure() 함수 : 통신 실패 시 호출
통신에 성공하면 서버에서 넘어온 데이터가 onResponse() 함수의 매개변수인 Response 객체로 전달되며 이 데이터를 response.body() 함수로 얻을 수 있음
위 코드를 보면 Response<UserListModel>로 선언했으므로 response.body() 함수가 반환하는 값은 UserListModel 객체
즉, 제네릭으로 선언한 클래스의 객체에 담아서 전달해 줌
Retrofit 애너테이션
지금까지 Retrofit의 기본 구조를 이해했으므로 이제 서비스 인터페이스를 만들 때 통신 개요를 설정하는 애너테이션을 알아보자.
Retrofit은 우리가 작성한 서비스 인터페이스에 따라 통신을 수행하므로 결국 어떤 에너테이션을 작성할 것인지가 핵심
@GET, @POST, @PUT, @DELETE, @HEAD
HTTP 메서드를 정의하는 애너테이션
@GET처럼 메서드명만 지정하거나 @GET("users/list")처럼 URL 경로를 지정해도 됨
또한 @GET("users/list?sort=desc")처럼 ?로 URL 뒤에 데이터 추가할 수도 있음
이처럼 경로를 지정하면 baseURL 뒤에 추가되어 최종 서버 요청 URL이 됨
- HTTP 메서드 애너테이션
// 인터페이스에 선언한 함수(INetworkService에 선언)
@GET("users/list?sort=desc")
fun test1(): Call<UserModel>
// Call 객체를 얻는 구문(MainActivity에 선언)
val call: Call<UserModel> = networkService.test1()
// 최종 서버 요청 URL
// https://reqres.in/users/list?sort=dec
@Path
URL의 경로를 동적으로 지정해야 할 때도 있음
예를 들어 group/1/users나 group/2/users처럼 1,2가 들어가는 부분을 동적으로 처리하려면 중괄호{}로 감싸야 함
group/{id}/users라고 지정하면 {id} 영역은 동적 데이터가 들어갈 자리이며 id는 개발자가 임의로 작성하면 됨
이 id 영역에 들어갈 데이터를 함수의 매개변수로 받으려면 그 매개변수에 @Path 애너테이션을 추가해야 함
- 동적인 경로 애너테이션
// 인터페이스에 선언한 함수(INetworkService에 선언)
@GET("group/{id}/users/{name}")
fun test2(
@Path("id") userId: String,
@Path("name") arg2: String
): Call<UserModel>
// Call 객체를 얻는 구문(MainActivity에 선언)
val call:Call<UserModel> = networkService.test2("10", "kkang")
// 최종 서버 요청 URL
// https://reqres.in/group/10/users/kkang
test2() 함수의 첫 번째 매개변수에 @Path("id") userId: String이라고 작성
이렇게 하면 첫 번째 매개변숫값이 경로에서 {id} 영역에 대입됨
@ Query
경로에 ?를 이용해 서버에 전달할 데이터를 지정할 수도 있지만, 함수의 매개변수값을 서버에 전달하고 싶다면 @Query 애너테이션을 사용
- 질의 애너테이션 예
// 인터페이스에 선언한 함수(INetworkService에 선언)
@GET("group/users")
fun test3(
@Query("sort") arg1: String,
@Query("name") arg2: String
): Call<UserModel>
// Call 객체를 얻는 구문(MainActivity에 선언)
val call: Call<UserModel> = networkService.test3("age", "kkang")
// 최종 서버 요청 URL
// https://reqres.in/group/10/users?sort=age&name=kkang
함수의 매개변수에 @Query("name")이라고 선언하면 서버에 요청할 때 name을 키로, 매개변숫값을 값으로 해서 서버에 데이터를 전달
@ QueryMap
만약 서버에 전송할 데이터가 많다면 함수의 매개변수를 여러 개 선언해야 하는 부담이 있음
이때에는 @QueryMap을 이용해 서버에 전송할 데이터를 Map 타입의 매개변수로 받으면 됨
- 질의 맵 애너테이션 예
// 인터페이스에 선언한 함수(INetworkService에 선언)
@GET("group/users")
fun test4(
@QueryMap options: Map<String, String>,
@Query("name") name: String
): Call<UserModel>
// Call 객체를 얻는 구문(MainActivity에 선언)
// ("one" to "hello")에서 첫 번째 요소가 Key이고 두 번째 요소가 Value
val call: Call<UserModel> = networkService.test4(mapOf<String, String>("one" to "hello", "two" to "world"), "kkang")
// 최종 서버 요청 URL
// https://reqres.in/group/users?one=hello&two=world&name=kkang
@ Body
서버에 전송할 데이터를 모델 객체로 지정하고 싶다면 @Body 애너테이션을 사용
@Body로 선언한 매개변수는 모델 객체 타입이며 이 객체의 프로퍼티명을 키로, 프로퍼티의 데이터를 값으로 해서 JSON 문자열을 만들어 서버에 전송
이때 JSON 문자열은 데이터 스트림으로 전송하므로 @Body는 @GET에서는 사용할 수 없으며 @POST와 함께 사용해야 함
// 인터페이스에 선언한 함수(INetworkService에 선언)
@POST("group/users")
fun test5(
@Body user:UserModel,
@Query("name") name:String
):Call<UserModel>
// Call 객체를 얻는 구문(MainActivity에 선언)
val call: Call<UserModel> = networkService.test5(
UserModel(id="1", firstName = "gildong", lastName = "hong", avatar = "someurl"),
"kkang"
)
// 최종 서버 요청 URL
// https://reqres.in/group/users?name=kkang
// 서버에 스트림으로 전송되는 데이터
// {"id":"1", "first_name":"gildong", "last_name":"hong", "avatar":"someurl"}
@Body 애너테이션을 사용하면 서버 요청 URL은 바뀌지 않음
@Body로 지정한 모델의 데이터는 객체의 내용을 JSON 문자열로 만들어 URL이 아닌 데이터 스트림으로 서버에 전송됨
@ FormUrlEncoded와 @Field
@FormUrlEncoded 애너테이션은 데이터를 URL 인코딩 형태로 만들어 전송할 때 사용
앞에서 살펴본 @Body는 데이터를 JSON으로 만들어 전송하지만 @FormUrlEncoded는 서버 전송 데이터를 '키=값' 형태의 URL 인코딩으로 전송
@Field 애너테이션이 추가된 데이터를 인코딩해서 전송하며 @FormUrlEncoded 애너테이션을 사용할 때만 적용 가능
그리고 @FormUrlEncoded 애너테이션은 POST 방식에서만 사용 가능
- URL 인코딩 애너테이션 예
// 인터페이스에 선언한 함수(INetworkService에 선언)
@FormUrlEncoded
@POST("user/edit")
fun test6(
@Field("first_name") first: String?,
@Field("last_name") last: String?,
@Query("name") name: String?
): Call<UserModel>
// Call 객체를 얻는 구문(MainActivity에 선언)
val call: Call<UserModel> = networkService.test6(
"gildong 길동",
"hong 홍",
"kkang"
)
// 최종 서버 요청 URL
// https://reqres.in/user/edit?name=kkang
// 서버에 스트림으로 전송되는 데이터
// first_name=gildong%20%EA%B8%B8%EB%8F%99&last_name=hong%20%ED%99%8D
@Field 애너테이션은 모델 객체에는 사용할 수 없으며 데이터 여러 건을 한꺼번에 지정하고 싶다면 배열이나 List 객체 이용
배열이나 List 객체에 @Field 애너테이션을 적용하면 데이터 여러 건을 같은 키로 서버에 전달 가능
- 리스트에 필드 애너테이션 사용 예
// 인터페이스에 선언한 함수(INetworkService에 선언)
@FormUrlEncoded
@POST("tasks")
fun test7(@Field("title") titles: List<String>): Call<UserModel>
// Call 객체를 얻는 구문(MainActivity에 선언)
val list: MutableList<String> = ArrayList()
list.add("홍길동")
list.add("류현진")
val call = networkService.test7(list)
// 최종 서버 요청 URL
// https://reqres.in/tasks
// 서버에 스트림으로 전송되는 데이터
// title=%ED%99%8D%EA%B8%EB%8F%99&title=%EB%A5%98%ED%98%84%EC%A7%84
@ Header
서버 요청에서 헤더값을 조정하고 싶다면 @Header 애너테이션 사용
- 헤더 에너테이션 예
// 인터페이스에 선언한 함수(INetworkService에 선언)
@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
fun test8(): Call<UserModel>
@ URL
baseURL을 무시하고 전혀 다른 URL을 지정하고 싶다면 @Url 애너테이션 사용
// 인터페이스에 선언한 함수(INetworkService에 선언)
@GET
fun test9(@Url url: String, @Query("name") name: String): Call<UserModel>
// Call 객체를 얻는 구문(MainActivity에 선언)
val call = networkService.test9("http://www.google.com", "kkang")
// 최종 서버 요청 URL
http://www.google.com/?name=kkang
전체 코드는 아래 깃허브에서 retrofit2 폴더에 들어가면 볼 수 있다
GitHub - rosa2070/AndroidKotlinBook: 깡샘의 안드로이드 스튜디오 코틀린
깡샘의 안드로이드 스튜디오 코틀린. Contribute to rosa2070/AndroidKotlinBook development by creating an account on GitHub.
github.com
enqueue 함수를 호출하는 부분부터 그 밑에 선언한 call 변수들도 모두 오류가 뜨는데 이유를 모르겠다...
'Android, Kotlin 📱 > [kotlin]깡샘의 안드로이드 앱 프로그래밍 with 코틀린' 카테고리의 다른 글
[깡샘코틀린] 1-3 앱 실행하기 (1) | 2023.10.31 |
---|---|
[깡샘코틀린] 1-2 첫 번째 앱 만들기 (0) | 2023.10.31 |
[깡샘코틀린] 1-1 안드로이드 스튜디오 설치하기 (0) | 2023.10.31 |
[깡샘코틀린] 6-4 뷰 바인딩(View binding) (0) | 2023.10.25 |
[깡샘코틀린] 6-2 뷰 클래스 (0) | 2023.10.25 |
Retrofit이란?
Retrofit은 스퀘어에서 만든 HTTP 통신을 간편하게 만들어주는 라이브러리
Retrofit은 1 버전과 2 버전이 있지만 Retrofit2가 2015년에 나왔으므로 지금 Retrofit이라고 하면 대부분 Retrofit2라고 봐도 무방
따라서 이 책에서 소개하는 Retrofit은 Retrofit2를 의미
retrofit 소개
Retrofit
A type-safe HTTP client for Android and Java
square.github.io
Retrofit을 이용하려면 먼저 프로그램의 구조를 이해해야 한다

Retrofit은 네트워크 통신 정보만 주면 그대로 네트워크 프로그래밍을 대신 구현해 줌
위의 그림에서 interface는 코틀린의 interface 키워드로 직접 만들어야 함
그리고 인터페이스의 함수는 통신할 때 필요
즉, a(), b() 함수를 호출해서 통신하겠다는 의미
그런데 인터페이스에는 함수를 선언만 하며 통신할 때 필요한 어떤 코드도 담지 않음
이렇게 만든 인터페이스를 Retrofit에 알려주면 인터페이스 정보를 보고 실제 통신할 때 필요한 코드를 담은 Service 객체를 만들어줌
* Service: 안드로이드 컴포넌트가 아니라 통신을 하게 해준다는 의미의 서비스
Retrofit은 우리가 알려준 인터페이스를 바탕으로 서비스를 만드므로 인터페이스에 선언한 함수를 그대로 포함
이 서비스의 함수를 호출하면 Call 객체를 반환하는데, 이 Call 객체의 enqueue() 함수를 호출하는 순간 통신을 수행
지금까지 설명한 Retrofit 동작 방식 정리
- 통신용 함수를 선언한 인터페이스 작성
- Retrofit에 인터페이스 전달
- Retrofit이 통신용 Service 객체 반환
- Service의 통신용 함수를 호출한 후 Call 객체 반환
- Call 객체의 enqueue() 함수를 호출하여 네트워크 통신 수행
구현
권한 추가
앱에서 네트워크 통신을 구현하려면 우선 매니페스트 파일에 다음처럼 퍼미션을 선언해야 함
<uses-permission android:name="android.permission.INTERNET"/>

라이브러리 선언
Retrofit을 이용하려면 빌드 그래들의 dependencies 항목에 다음처럼 라이브러리를 등록해야 함
implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.google.code.gson:2.8.6") implementation("com.squareup.retrofit2:converter-gson:2.9.0")
첫 번째 라이브러리는 Retrofit을 이용할 때 필수이며 나머지 2개는 다른 라이브러리를 이용할 수도 있음
Retrofit은 JSON이나 XML 데이터를 모델(VO 클래스) 객체로 변환해 주는데 이때 JSON, XML을 파싱하는 라이브러리가 필요
따라서 구글에서 만든 gson 라이브러리를 사용하고자 com.google.code.gson:gson을 등록했으며,
Retrofit에서 gson을 이용해 모델 객체로 변환해 주는 com.squareup.retrofit2:converter-gson을 함께 등록
만약 파싱 라이브러리가 바뀌면 converter 라이브러리도 그에 맞게 바꿔야 함
Retrofit은 다음과 같은 converter 라이브러리를 제공
- Gson : com.squareup.retrofit2:converter-gson
- Jackson : com.squareup.retrofit2:converter-jackson
- Moshi : com.squareup.retrofit2:converter-moshi
- Protobuf : com.squareup.retrofit2:converter-protobuf
- Wire : com.squareup.retrofit2:converter-wire
- Simple XML : com.squareup.retrofit2:converter-simplexml
- JAXB : com.squareup.retrofit2:converter-jaxb
- Scalars(primitives, boxed, String) : com.squareup.retrofit2:converter-scalars
모델 클래스 선언
모델 클래스란?
모델 클래스란 서버와 주고받는 데이터를 표현하는 클래스
흔히 VO(value-object) 클래스라고도 하며 JSON, XML 데이터를 파싱해 모델 클래스 객체에 담는 것을 자동화해 줌
만약 서버에서 넘어오는 JSON 데이터가 다음과 같다고 가정해 보자
{ "id": 7, "email": "michael.lawson@reqres.in", "first_name": "Michael", "last_name": "Lawson", "avatar": "https://reqres.in/img/faces/7-image.jpg" }
원래는 JSON 데이터를 코드에서 직접 파싱해서 이용해야 하는데, 데이터를 담을 모델 클래스를 선언하고 클래스 정보만 알려 주면 모델 클래스의 객체를 알아서 생성하고 그 객체에 데이터를 담아줌
모델 클래스는 상위 타입에 제약이 없으므로 어떤 클래스를 상속받거나 인터페이스를 구현할 필요는 없음. 순전히 개발자가 작성하는 클래스임
앞의 JSON 정보를 담을 모델 클래스는 다음처럼 작성 가능
- 모델 클래스
data class UserModel ( var id: String, @SerializedName("first_name") var firstName: String, // @SerializedName("last_name") var lastName: String, var avatar: String, var avatarBitmap: Bitmap )
모델 클래스의 프로퍼티에 데이터가 자동으로 저장되는 기본 규칙은 데이터의 키와 프로퍼티 이름을 매칭하는 것
예를 들어 id라는 키값은 id 프로퍼티에 저장됨
만약 키와 프로퍼티 이름이 다를 때는 @SerializedName이라는 애너테이션으로 명시
코드에서 주석으로 지정한 @SerializedName("first_name")의 의미는 first_name이라는 키의 데이터가 firstName 프로퍼티에 저장된다는 의미
그런데 키와 프로퍼티 이름이 다르더라도 밑줄 다음에 오는 단어의 첫 글자를 대문자로 바꾼 프로퍼티명이 있을 때는 @SerializedName 애너테이션을 사용하지 않아도 됨
예를 들어 키가 last_name이면 자동으로 lastName 프로퍼티에 저장됨
모델 클래스를 만들 때 서버의 데이터와 상관없는 프로퍼티를 선언해도 됨
위의 코드를 보면 avatarBitmap이라는 프로퍼티를 선언했는데 서버로부터 넘어오는 JSON에는 이와 관련된 데이터가 없음
이처럼 모델에 서버 연동과 상관없는 데이터를 담는 프로퍼티를 선언해도 됨
서버의 데이터가 복잡할 때는 모든 데이터를 하나의 모델 클래스로 표현하지 않고 여러 클래스로 분리한 후 조합해서 사용 가능
예를 들어 목록 화면을 구성하는 서버 데이터가 다음처럼 전달된다고 가정
{ "page": 2, "per_page": 6, "total": 12, "total_pages": 2, "data": [ { "id": 7, "email": "michael.lawson@reqres.in", "first_name": "Michael", "last_name": "Lawson", "avatar": "https://reqres.in/img/faces/7-images.jpg" }, { "id": 8, "email": "lindsay.ferguson@reqres.in", "first_name": "Lindsay", "last_name": "Ferguson", "avatar": "https://reqres.in/img/faces/8-image.jpg" } ], }
이 데이터를 하나의 모델 클래스에 담아도 되지만 data 키값을 저장하는 UserModel 클래스와 전체 페이지 정보를 저장하는 UserListModel 클래스로 분리해 작성하고 UserListModel에서 UserModel을 이용하면 됨
- 모델 클래스 분리 이용
data class UserListModel ( var page: String, @SerializedName("per_page") var perPage: String, var total: String, @SerializedName("total_pages") var totalPages: String, var data: List<UserModel>? )
Retrofit을 이용할 때 UserListModel을 알려 주면 JSON 데이터를 파싱해 프로퍼티에 저장하며 data 키값은 data 프로퍼티에 선언된 UserModel 클래스의 객체에 담아 줌
서비스 인터페이스 정의
Retrofit을 이용할 때 가장 중요한 부분 : 네트워크 통신이 필요한 순간에 호출할 함수를 포함하는 서비스 인터페이스를 작성하는 것
- 서비스 인터페이스 정의
//class가 아닌 interface!! interface INetworkService { @GET("api/users") fun doGetUserList(@Query("page") page: String): Call<UserListModel> @GET fun getAvatarImage(@Url url:String): Call<ResponseBody> }
INetworkService라는 이름의 인터페이스를 선언하고 그 안에 doGetUserList(), getAvatarImage() 라는 이름의 함수를 정의
그런데 이 인터페이스명과 함수명은 개발자가 지은 이름일 뿐임
이 인터페이스를 구현해 실제로 통신하는 클래스는 Retrofit이 자동으로 만들어 주는데 이때 애너테이션을 참조
즉, 함수에 선언한 에너테이션을 보고 그 정보대로 네트워크 통신을 할 수 있는 코드를 자동 생성
함수에 선언한 에너테이션
- @GET : 서버와 연동할 때 GET 방식으로 해달라는 의미
- @Query : 서버에 전달되는 데이터
- @Url : 요청 URL
결국 Retrofit을 이용할 때는 인터페이스의 애너테이션이 중요하며 이 내용은 잠시 후에 자세히 정리
Retrofit 객체 생성
Retrofit을 사용할 때 가장 먼저 Retrofit 객체를 생성하는 코드를 실행해야 함(MainActivity)
val retrofit: Retrofit // get()은 속성에 대한 커스텀 Getter 메서드 get() = Retrofit.Builder() .baseUrl("https://reqres.in/") .addConverterFactory(GsonConverterFactory.create()) .build()

Retrofit 객체를 생성하는 코드는 초기 설정을 하므로 한 번만 생성하면 됨
baseUrl()함수로 URL을 설정하면 이후에 이 URL 뒤에 올 경로만 지정해서 서버와 연동 가능
예를 들어 baseUrl을 앞의 코드처럼 선언하고 어디선가 @GET("api/users")처럼 경로를 지정했다면 서버 요청 URL은 https://reqres.in/api/users 가 됨
물론 baseUrl을 선언했더라도 전혀 다른 URL로 요청할 수도 있음
그리고 addConverterFactory() 함수로 데이터를 파싱해 모델 객체에 담는 역할자를 지정해 줌
앞에서는 GsonConverterFactory.create()로 작성했으므로 GsonConverter를 이용하겠다는 의미
인터페이스 타입의 서비스 객체 얻기
Retrofit 객체를 생성한 다음에는 이 객체로 서비스 인터페이스를 구현한 클래스의 객체를 얻음
- 서비스 객체 얻기(MainActivity에 선언)
// INetworkService::class.java는 INetworkService 인터페이스에 해당하는 Java Class 객체를 가져옴 // Retrofit의 create 메서드는 Java Class 객체를 필요로 하기 때문에 이런 형태로 사용 var networkService: INetworkService = retrofit.create(INetworkService::class.java)
Retrofit의 create() 함수에 앞에서 만든 서비스 인터페이스 타입을 전달
그러면 이 인터페이스를 구현한 클래스의 객체를 반환해 줌
실제 네트워크가 필요할 때 이 객체의 함수를 호출하면 된다
네트워크 통신 시도
이제 모든 준비가 끝났으므로 네트워크 통신이 필요한 순간에 Retrofit 객체로 얻은 서비스 객체의 함수를 호출만 해주면 됨
서비스 클래스와 객체는 Retrofit이 만들어 주지만 우리가 만든 인터페이스를 구현한 클래스이므로 인터페이스의 함수를 호출하면 네트워크 통신을 시도함
- Call 객체 얻기
var userListCall = networkService.doGetUserList("1")
인터페이스에 선언한 함수를 호출하면 위 코드에서 userListCall처럼 Call 객체가 반환됨
실제 통신은 이 Call 객체의 enqueue() 함수를 호출하는 순간 이루어짐
- 네트워크 통신 수행
userListCall.enqueue(object: Callback<UserListModel> { override fun onResponse(call: Call<UserListModel>, response: Response<UserListModel>) { val userList = response.body() // (... 생략 ...) } override fun onFailure(call: Call<UserListModel>, t: Throwable) { call.cancel() } })
Call 객체의 enqueue() 함수를 호출하면 비로소 통신이 수행됨
그리고 enqueue() 함수의 매개변수로 지정한 Callback 객체의 onResponse(), onFailure() 함수가 자동으로 호출됨
- onResponse() 함수 : 통신 성공 시 호출
- onFailure() 함수 : 통신 실패 시 호출
통신에 성공하면 서버에서 넘어온 데이터가 onResponse() 함수의 매개변수인 Response 객체로 전달되며 이 데이터를 response.body() 함수로 얻을 수 있음
위 코드를 보면 Response<UserListModel>로 선언했으므로 response.body() 함수가 반환하는 값은 UserListModel 객체
즉, 제네릭으로 선언한 클래스의 객체에 담아서 전달해 줌
Retrofit 애너테이션
지금까지 Retrofit의 기본 구조를 이해했으므로 이제 서비스 인터페이스를 만들 때 통신 개요를 설정하는 애너테이션을 알아보자.
Retrofit은 우리가 작성한 서비스 인터페이스에 따라 통신을 수행하므로 결국 어떤 에너테이션을 작성할 것인지가 핵심
@GET, @POST, @PUT, @DELETE, @HEAD
HTTP 메서드를 정의하는 애너테이션
@GET처럼 메서드명만 지정하거나 @GET("users/list")처럼 URL 경로를 지정해도 됨
또한 @GET("users/list?sort=desc")처럼 ?로 URL 뒤에 데이터 추가할 수도 있음
이처럼 경로를 지정하면 baseURL 뒤에 추가되어 최종 서버 요청 URL이 됨
- HTTP 메서드 애너테이션
// 인터페이스에 선언한 함수(INetworkService에 선언) @GET("users/list?sort=desc") fun test1(): Call<UserModel> // Call 객체를 얻는 구문(MainActivity에 선언) val call: Call<UserModel> = networkService.test1() // 최종 서버 요청 URL // https://reqres.in/users/list?sort=dec
@Path
URL의 경로를 동적으로 지정해야 할 때도 있음
예를 들어 group/1/users나 group/2/users처럼 1,2가 들어가는 부분을 동적으로 처리하려면 중괄호{}로 감싸야 함
group/{id}/users라고 지정하면 {id} 영역은 동적 데이터가 들어갈 자리이며 id는 개발자가 임의로 작성하면 됨
이 id 영역에 들어갈 데이터를 함수의 매개변수로 받으려면 그 매개변수에 @Path 애너테이션을 추가해야 함
- 동적인 경로 애너테이션
// 인터페이스에 선언한 함수(INetworkService에 선언) @GET("group/{id}/users/{name}") fun test2( @Path("id") userId: String, @Path("name") arg2: String ): Call<UserModel> // Call 객체를 얻는 구문(MainActivity에 선언) val call:Call<UserModel> = networkService.test2("10", "kkang") // 최종 서버 요청 URL // https://reqres.in/group/10/users/kkang
test2() 함수의 첫 번째 매개변수에 @Path("id") userId: String이라고 작성
이렇게 하면 첫 번째 매개변숫값이 경로에서 {id} 영역에 대입됨
@ Query
경로에 ?를 이용해 서버에 전달할 데이터를 지정할 수도 있지만, 함수의 매개변수값을 서버에 전달하고 싶다면 @Query 애너테이션을 사용
- 질의 애너테이션 예
// 인터페이스에 선언한 함수(INetworkService에 선언) @GET("group/users") fun test3( @Query("sort") arg1: String, @Query("name") arg2: String ): Call<UserModel> // Call 객체를 얻는 구문(MainActivity에 선언) val call: Call<UserModel> = networkService.test3("age", "kkang") // 최종 서버 요청 URL // https://reqres.in/group/10/users?sort=age&name=kkang
함수의 매개변수에 @Query("name")이라고 선언하면 서버에 요청할 때 name을 키로, 매개변숫값을 값으로 해서 서버에 데이터를 전달
@ QueryMap
만약 서버에 전송할 데이터가 많다면 함수의 매개변수를 여러 개 선언해야 하는 부담이 있음
이때에는 @QueryMap을 이용해 서버에 전송할 데이터를 Map 타입의 매개변수로 받으면 됨
- 질의 맵 애너테이션 예
// 인터페이스에 선언한 함수(INetworkService에 선언) @GET("group/users") fun test4( @QueryMap options: Map<String, String>, @Query("name") name: String ): Call<UserModel> // Call 객체를 얻는 구문(MainActivity에 선언) // ("one" to "hello")에서 첫 번째 요소가 Key이고 두 번째 요소가 Value val call: Call<UserModel> = networkService.test4(mapOf<String, String>("one" to "hello", "two" to "world"), "kkang") // 최종 서버 요청 URL // https://reqres.in/group/users?one=hello&two=world&name=kkang
@ Body
서버에 전송할 데이터를 모델 객체로 지정하고 싶다면 @Body 애너테이션을 사용
@Body로 선언한 매개변수는 모델 객체 타입이며 이 객체의 프로퍼티명을 키로, 프로퍼티의 데이터를 값으로 해서 JSON 문자열을 만들어 서버에 전송
이때 JSON 문자열은 데이터 스트림으로 전송하므로 @Body는 @GET에서는 사용할 수 없으며 @POST와 함께 사용해야 함
// 인터페이스에 선언한 함수(INetworkService에 선언) @POST("group/users") fun test5( @Body user:UserModel, @Query("name") name:String ):Call<UserModel> // Call 객체를 얻는 구문(MainActivity에 선언) val call: Call<UserModel> = networkService.test5( UserModel(id="1", firstName = "gildong", lastName = "hong", avatar = "someurl"), "kkang" ) // 최종 서버 요청 URL // https://reqres.in/group/users?name=kkang // 서버에 스트림으로 전송되는 데이터 // {"id":"1", "first_name":"gildong", "last_name":"hong", "avatar":"someurl"}
@Body 애너테이션을 사용하면 서버 요청 URL은 바뀌지 않음
@Body로 지정한 모델의 데이터는 객체의 내용을 JSON 문자열로 만들어 URL이 아닌 데이터 스트림으로 서버에 전송됨
@ FormUrlEncoded와 @Field
@FormUrlEncoded 애너테이션은 데이터를 URL 인코딩 형태로 만들어 전송할 때 사용
앞에서 살펴본 @Body는 데이터를 JSON으로 만들어 전송하지만 @FormUrlEncoded는 서버 전송 데이터를 '키=값' 형태의 URL 인코딩으로 전송
@Field 애너테이션이 추가된 데이터를 인코딩해서 전송하며 @FormUrlEncoded 애너테이션을 사용할 때만 적용 가능
그리고 @FormUrlEncoded 애너테이션은 POST 방식에서만 사용 가능
- URL 인코딩 애너테이션 예
// 인터페이스에 선언한 함수(INetworkService에 선언) @FormUrlEncoded @POST("user/edit") fun test6( @Field("first_name") first: String?, @Field("last_name") last: String?, @Query("name") name: String? ): Call<UserModel> // Call 객체를 얻는 구문(MainActivity에 선언) val call: Call<UserModel> = networkService.test6( "gildong 길동", "hong 홍", "kkang" ) // 최종 서버 요청 URL // https://reqres.in/user/edit?name=kkang // 서버에 스트림으로 전송되는 데이터 // first_name=gildong%20%EA%B8%B8%EB%8F%99&last_name=hong%20%ED%99%8D
@Field 애너테이션은 모델 객체에는 사용할 수 없으며 데이터 여러 건을 한꺼번에 지정하고 싶다면 배열이나 List 객체 이용
배열이나 List 객체에 @Field 애너테이션을 적용하면 데이터 여러 건을 같은 키로 서버에 전달 가능
- 리스트에 필드 애너테이션 사용 예
// 인터페이스에 선언한 함수(INetworkService에 선언) @FormUrlEncoded @POST("tasks") fun test7(@Field("title") titles: List<String>): Call<UserModel> // Call 객체를 얻는 구문(MainActivity에 선언) val list: MutableList<String> = ArrayList() list.add("홍길동") list.add("류현진") val call = networkService.test7(list) // 최종 서버 요청 URL // https://reqres.in/tasks // 서버에 스트림으로 전송되는 데이터 // title=%ED%99%8D%EA%B8%EB%8F%99&title=%EB%A5%98%ED%98%84%EC%A7%84
@ Header
서버 요청에서 헤더값을 조정하고 싶다면 @Header 애너테이션 사용
- 헤더 에너테이션 예
// 인터페이스에 선언한 함수(INetworkService에 선언) @Headers("Cache-Control: max-age=640000") @GET("widget/list") fun test8(): Call<UserModel>
@ URL
baseURL을 무시하고 전혀 다른 URL을 지정하고 싶다면 @Url 애너테이션 사용
// 인터페이스에 선언한 함수(INetworkService에 선언) @GET fun test9(@Url url: String, @Query("name") name: String): Call<UserModel> // Call 객체를 얻는 구문(MainActivity에 선언) val call = networkService.test9("http://www.google.com", "kkang") // 최종 서버 요청 URL http://www.google.com/?name=kkang
전체 코드는 아래 깃허브에서 retrofit2 폴더에 들어가면 볼 수 있다
GitHub - rosa2070/AndroidKotlinBook: 깡샘의 안드로이드 스튜디오 코틀린
깡샘의 안드로이드 스튜디오 코틀린. Contribute to rosa2070/AndroidKotlinBook development by creating an account on GitHub.
github.com
enqueue 함수를 호출하는 부분부터 그 밑에 선언한 call 변수들도 모두 오류가 뜨는데 이유를 모르겠다...
'Android, Kotlin 📱 > [kotlin]깡샘의 안드로이드 앱 프로그래밍 with 코틀린' 카테고리의 다른 글
[깡샘코틀린] 1-3 앱 실행하기 (1) | 2023.10.31 |
---|---|
[깡샘코틀린] 1-2 첫 번째 앱 만들기 (0) | 2023.10.31 |
[깡샘코틀린] 1-1 안드로이드 스튜디오 설치하기 (0) | 2023.10.31 |
[깡샘코틀린] 6-4 뷰 바인딩(View binding) (0) | 2023.10.25 |
[깡샘코틀린] 6-2 뷰 클래스 (0) | 2023.10.25 |