안드로이드 통신에 자주 사용하는 Retrofit 알아보기 (1) - HttpURLConnection, OkHttp, Retrofit 장점을 중심으로
Jwt 토큰 관련해서 프로젝트의 버그 픽스, 간단한 리팩토링(리팩토링이라고 하기도 뭐하지만) 을 진행하면서 이 김에 Retrofit 클래스의 내부 구조에 대해서 조금 이해를 해야 할 필요가 있을 것 같아 정리해보려고 합니다.
안드로이드 레트로핏 api 호출하는 인터페이스에서 @Header 중복을 제거하기
APi 요청 시 Access Token 을 갱신해야 할 때 Refresh Token 으로 토큰 갱신 후 같은 api 재요청
위 글에서 간단히 코드를 수정한 부분들을 정리한 내용입니다.
배달의 민족 기술이사 김영한님의 Http 웹 기본 지식 강의를 듣고 정리한 내용들
위 Http 웹 기본 지식에 대해서도 알면 더 도움이 될 겁니다. 당연히 강의를 결제해서 듣는 것을 추천합니다!!
https://www.inflearn.com/course/http-%EC%9B%B9-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC
개요
일단 retrofit2
패키지의 Retrofit
클래스는 okHttp3
패키지(HttpUrl
, OkHttpClient
클래스) 에 의존성을 가집니다.
정확히 말하면 Retrofit
클래스 내부에 중첩 클래스 Builder
에서 의존성을 가지고 있고 해당 Builder
타입의 메서드를 Retrofit
클래스에서 사용합니다.
OkHttp3 역시 Square 에서 만든 라이브러리로 일종의 Third-Party 라이브러리입니다.
사실 초창기 안드로이드에서는 네트워크 콜을 수행하기 위해서 HttpURLConnection
을 사용했습니다.
HttpURLConnection
HttpURLConnection
은 URLConnection
클래스를 상속받는 abstrct class 입니다.
URLConnection
은 App. 과 URL 간의 통신 연결을 사용하는 모든 클래스의 슈퍼클래스입니다.
URLConnection
클래스의 인스턴스는 URL 에서 참조한 리소스를 읽고 쓰는 데 사용됩니다. openConnection()
과 connect()
메서드를 사용해서 원격 리소스 연결 관련 패러미터를 조작하고, 리소스와 상호작용, 리소스의 헤더와 내용을 쿼리합니다.
위는 Android developer 공식 문서에서 가져온 내용입니다.
HttpUrlConnection
은 그림에서 나온 것처럼 java.net
패키지에 존재합니다.
즉, 자바에서 기본적으로 제공하는 클래스이기 때문에 호환성 문제도 없고 가볍게 사용할 수 있다는 장점이 있습니다.
Android developer 공식 문서에서 HttpURLConnection
을 사용해서 통신을 하는 간단한 가이드라인이 소개되고 있습니다.
URL.openConnection()
호출.
→ 얻은 결과를HttpURLConnection
으로 캐스팅해서HttpURLConnection
객체를 얻기.- http 통신의 request 준비.
(request 의 주요 프로퍼티는 URL 이며, 헤더에 credentials(자격 증명), content type, 쿠키 등이 들어감) - request 바디에 원하는 데이터 담아서 전송.
(request 바디가 있다면 인스턴스를setDoOutput(true)
로 설정한 후,URLConnection.getOutputStream()
으로 얻은 스트림에 데이터를 직접 써서 전송함.) - http 통신의 response 를 읽기.
(response 헤더에는 보통 response 바디의 content type, 길이, 쿠키 등의 메타 데이터가 담김URLConnection.getInputStream()
으로 얻은 스트림에 response 바디가 있음.
response 바디가 없으면 이 메서드는 빈 스트림을 리턴함.) - 연결 끊기
(response 의 바디를 읽고 나서disconnect()
를 호출.HttpURLConnection
을 close 해야 함
연결을 끊으면 리소스가 릴리즈되어서 리소스가 close 되거나 리소스를 재사용할 수 있음.)
HttpURLConnection
을 이용해서 GET 메서드를 호출하고 response 를 읽는 코드는 아래처럼 수행됩니다.
fun run() {
try {
val url = URL("https://publicobject.com/helloworld.txt")
val connection = url.openConnection() as HttpURLConnection
// GET 요청 설정
connection.requestMethod = "GET"
// 응답 코드 확인
val responseCode = connection.responseCode
if (responseCode != HttpURLConnection.HTTP_OK) {
throw IOException("Unexpected code $responseCode")
}
// 응답 헤더 출력
val headers = connection.headerFields
for ((name, valueList) in headers) {
for (value in valueList) {
println("$name: $value")
}
}
// 응답 본문 읽기
val reader = BufferedReader(InputStreamReader(connection.inputStream))
var line: String?
val response = StringBuilder()
while (reader.readLine().also { line = it } != null) {
response.append(line).append('\n')
}
reader.close()
// 응답 내용 출력
println(response.toString())
} catch (e: IOException) {
e.printStackTrace()
}
}
위처럼 사용법이 복잡하고 간단한 api 하나를 부르는데도 boiler code 가 너무 많아집니다.
게다가 위 가이드라인에는 적혀있지 않지만 비동기도 AsyncTask
객체를 만들어서 직접 구현해야 합니다...
OkHttp
OkHttp 는 Square 에서 개발한 HTTP 네트워크 통신 Third-party 라이브러리입니다.
OkHttp 의 장점은 아래와 같습니다.
- 간결하고 직관적인 api 를 제공, Http request 를 생성하고 전송이 쉬워짐.
- 커스텀 헤더 및 인터셉터를 제공. 네트워크 통신 조작, 로깅이 쉬움.
- 비동기 및 동기 요청을 모두 지원.
- 테스팅이 쉬워짐.
- Http/2 지원. http2 의 스트림과 커넥션을 관리하기 위해 사용하는 shared memory pool 을 사용.
- Http/2 를 사용할 수 없는 경우는 Connection pooling 을 지원. 요청 대기 시간이 줄어듬.
- transparent GZIP 으로 다운로드 사이즈를 줄임.
- response caching 으로 반복적인 요청에 대해 효율적 처리 가능.
square 깃허브 사이트에서 간단한 사용법을 확인할 수 있습니다.
https://square.github.io/okhttp/recipes/
class MyOkHttpExample {
private val client = OkHttpClient()
fun run() {
val url = "https://publicobject.com/helloworld.txt"
val request = Request.Builder()
.url(url)
.get() // GET 요청 메서드를 설정
.build()
try {
val response: Response = client.newCall(request).execute()
if (response.isSuccessful) {
println("Headers: ${response.headers()}")
println("Response: ${response.body()?.string()}")
} else {
throw IOException("Unexpected code ${response.code()}")
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
기존에 HttpURLConnection
에서는 직접 connection 을 열고 닫는 등의 동작이 우리가 사용하는 코드에서는 보이지 않고 훨씬 더 직관적으로 이해가 되는 것을 볼 수 있습니다.
만약 클라이언트 - 서버에서 통신 시 자주 사용하는 JSON 포맷으로 바디에 데이터를 담아서 GET 요청을 한다면 아래처럼만 코드를 수정하면 됩니다.
class MyOkHttpExample {
private val client = OkHttpClient()
fun run() {
val url = "https://publicobject.com/helloworld.json" // JSON 엔드포인트로 변경
val request = Request.Builder()
.url(url)
.get()
.build()
try {
val response: Response = client.newCall(request).execute()
if (response.isSuccessful) {
val jsonResponse = response.body()?.string()
if (jsonResponse != null) {
val jsonObject = JSONObject(jsonResponse)
val message = jsonObject.getString("message")
println("Message: $message")
}
} else {
throw IOException("Unexpected code ${response.code()}")
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
Retrofit
우리가 정말로 많이 사용하는 Retrofit 입니다.
사실상 안드로이드에서 서버와 간단한 형태의 통신을 할 때는 대부분 Retrofit 을 사용한다고 할 수 있는데요.
OkHttp 처럼 Square 에서 만들어졌으며 OkHttp 를 네트워크 레이어로 활용하여 그 위에 레이어를 한 단계 더 추가해서 만든 라이브러리입니다.
OkHttp 가 저수준의 네트워크 명령(request, response, caching) 등을 수행하고 Retrofit 은 그 위에 추가로 abstraction layer 를 얹어서 조금 더 사용하기 편하고 간결하게 만든 것입니다.
사용할 때의 코드를 간단히 살펴봅시다.
interface ApiService {
@GET("helloworld.txt")
fun getHelloWorld(): Call<String>
}
class MyRetrofitExample {
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://publicobject.com/")
.client(OkHttpClient()) // OkHttp 클라이언트 사용
.addConverterFactory(ScalarsConverterFactory.create())
.build()
private val apiService: ApiService = retrofit.create(ApiService::class.java)
fun run() {
val call = apiService.getHelloWorld()
try {
val response = call.execute()
if (response.isSuccessful) {
println("Headers: ${response.headers()}")
println("Response: ${response.body()}")
} else {
throw IOException("Unexpected code ${response.code()}")
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
만약에 JSON 포맷으로 바디에 데이터를 담아서 통신한다면 아래와 같습니다.
data class MyData(val message: String) // JSON 데이터 구조와 일치해야 함
interface ApiService {
@GET("helloworld.txt")
fun getHelloWorld(): Call<MyData> // 반환 타입을 Kotlin 데이터 클래스로 변경
}
class MyRetrofitExample {
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://publicobject.com/")
.client(OkHttpClient()) // OkHttp 클라이언트 사용
.addConverterFactory(GsonConverterFactory.create()) // Gson 컨버터 사용
.build()
private val apiService: ApiService = retrofit.create(ApiService::class.java)
fun run() {
val call = apiService.getHelloWorld()
try {
val response = call.execute()
if (response.isSuccessful) {
val myData = response.body()
println("Message: ${myData?.message}")
} else {
throw IOException("Unexpected code ${response.code()}")
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
기본적으로 retrofit 은 api query 를 service interface 를 이용해서 작성합니다.
그리고 api call 을 하고자 하는 url
과 OkHttpClient
를 이용해서 Retrofit
의 인스턴스를 만들어줍니다.
위에서는 받은 JSON 형태의 데이터를 GSON 객체로 만들어주는 것도 간단히 추가되어 build 되고 있습니다.
이렇게 OKHttpClient
안에 Intercepter(예를 들어 HttpLoggingInterceptor
) 를 추가하여 http 통신 중간의 과정을 모두 log로 남길 수 있고,
ConverterFactory를 추가하여 api response를 원하는 format으로 받을 수 있습니다.(JSON → (GsonConverterFactory
), Xml → ... )
위에서 생성한 Retrofit Client 인스턴스로 service interface 의 구현체를 생성해서 api call 을 수행하는 것입니다.
Retrofit 을 사용해서 통신을 할 때 의존성 구조를 간단히 표현하면 아래처럼 되겠죠.
Retrofit 사용의 장점
사실 안드로이드에서 서버와의 대부분의 통신은 request 나 respone 의 Body 에 JSON 포맷의 데이터를 넣어서 수행됩니다.
즉, 위 두번째 MyRetrofitExample
클래스에서의 코드의 대부분을 대부분을 똑같이 사용하는 것이죠.
달라지는 부분은 리턴되는 데이터 타입과 baseUrl 뒤에 붙은 path 나 query 부분입니다.
그런데 이 리턴되는 데이터 타입, path, query 는 모두 Api Service interface 에서 결정합니다!!!
즉, 공통으로 사용되는 위의 MyRetrofitExample
부분만 잘 만들어두고 클라이언트에서 사용하는 각자 다른 부분은 Api Service interface 에서만 간단히 변경해가면서 사용하면, 객체지향의 관점에서 굉장한 이득을 볼 수 있습니다.
(물론 retrofit.create(ApiService::class.java)
의 부분도 패러미터로 빼내어 클라이언트에서 결정할 수 있도록 해야 겟죠.)
이는 굉장한 이득이죠!! 🔥🔥🔥🔥🔥🔥
만약 서버 클래스(MyRetrofitExample
부분을 개발하는) 개발자가 있다고 합시다.
그리고 Client 부분을 개발하는 클라이언트 부분 개발자가 있다고 하면, 클라이언트 개발자는 서버 클래스의 구현 부분에 대해 자세히 알지 못해도 간단히 사용할 수 있게 됩니다.
실제 코드로 장점 체감하기(추가적으로 Hilt 를 사용했음)
위에서의 링크 글 두 개(안드로이드 레트로핏 .... @Header 중복을 제거하기, Api 요청 시 ... 토큰 갱신 후 같은 api 재요청 글) 에서는 Hilt 를 사용하여 중복 코드를 굉장히 많이 줄인 것을 알 수 있습니다.
만약에 프로젝트에서 새로운 도메인 '친구' 가 생기고 그에 대한 api 를 호출하는 부분이 생긴다고 하면 내부 구현에 대해 자세히 알지 못하더라도 굉장히 간단회 추가할 수 있습니다.
- Api Service Interface(ex.
FriendApi
) 에서 URL 의 path 나 query 에 맞춰서 api 호출 함수를 만들어 주기 - api 호출 함수시 서버로부터 받는 데이터에 맞게
data class
만들어주기 - Hilt 를 이용해 DI 를 해주기
- 클라이언트에서 이를 사용하기.
간단한 코드로 보시죠.
1. FriendApi
에서 함수 만들어 주기
interface FriendApi {
@GET("friends")
suspend fun getAllFriends(
): Response<FriendList>
@GET("friend/{friendId}")
suspend fun getFriend(
@Path("friendId") friendId: String
): Response<Friend>
@POST("friend")
suspend fun addFriend(
@Body friend: Friend
): Response<Unit>
@DELETE("friend/{friendId}")
suspend fun deleteFriend(
@Path("friendId") friendId: String
): Response<Unit>
....
}
2. data class FriendList
, Friend
만들어주기
data class FriendList(
val friends: List<Friend>
)
data class Friend{
val friendId: String,
val isMarried: Boolean,
val location: String
}
3. Hilt 에서 DI 해주기
@Singleton
@Provides
fun provideFriendeApi(
@BabaRetrofit retrofit: Retrofit
): FriendApi = retrofit.create(FirendApi::class.java)
4. 클라이언트 코드에서 FriendApi
의 함수를 호출해서 사용.
이렇게만 하면 끝인 겁니다.
정말 이게 다입니다.
이렇게 요구사항 추가에 대해 빠르고 쉽고 간단하게 대응할 수 있습니다. 변경에 대해서도 그렇죠.
이렇게 Retrofit 에 대해서 매우 간단히 알아보았습니다.
객체지향에 대해 관심을 가지고 공부하니 OkHttp 와 Retrofit 이 얼마나 잘 만들어진 라이브러리인지가 잘 보이네요...
사실 Retrofit 은 내부적으로 Builder 패턴, 프록시 패턴 등의 디자인 패턴들을 사용해서 효율적으로 구현되어 있습니다.
다음에는 더 자세히 Retrofit 이 어떻게 구현되었는지에 대해 알아보겠습니다.
참고한 내용
https://square.github.io/okhttp/recipes/
https://developer.android.com/reference/java/net/HttpURLConnection