Retrofit은 HTTP API를 Java 인터페이스로 변환한다.
interface GitHubService {
@GET("users/{user}/repos")
fun listRepos(@Path("user") String user): Call<List<Repo>>
}
Retrofit 클래스는 GitHubService 인터페이스의 구현을 생성한다.
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build()
val service = retrofit.create(GitHubService::class.java)
생성된 GitHubService의 각 Call은 원격 웹 서버에 대해 동기식 또는 비동기식 HTTP 요청을 만들 수 있다.
val repos = service.listRepos("octocat")
annotation을 사용하여 HTTP 요청 설명합니다.
인터페이스 메서드 및 해당 매개 변수에 대한 주석은 요청을 처리하는 방법을 나타낸다.
REQUEST METHOD
모든 메서드에는 요청 메서드와 관련 URL을 제공하는 HTTP annotation이 있어야 한다.
8개의 기본 제공 주석이 있다.
HTTP, GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
리소스의 관련 URL은 주석에 지정된다.
@GET("users/list")
URL에서 query parameter를 지정할 수도 있습니다.
@GET("users/list?sort=desc")
요청 URL은 메서드의 대체 블록 및 매개 변수를 사용하여 동적으로 업데이트될 수 있다. 대체 블록은 { 및 }으로 둘러싸인 영숫자 문자열이다. 해당 매개 변수는 동일한 문자열을 사용하여 @Path로 주석을 달아야 한다.
@GET("group/{id}/users")
fun groupList(@Path("id") groupId: Int): Call<List<User>>
쿼리 매개 변수도 추가할 수 있다.
@GET("group/{id}/users")
fun groupList(@Path("id") groupId: Int, @Query("sort") sort: String): Call<List<User>>
복잡한 쿼리 매개 변수 조합의 경우 Map을 사용할 수 있다.
@GET("group/{id}/users")
fun groupList(@Path("id") groupId: Int, @QueryMap options: Map<String, String>): Call<List<User>>
객체는 @Body 주석과 함께 HTTP request body로 사용하도록 지정할 수 있다.
@POST("users/new")
fun createUser(@Body user: User): Call<User>
객체는 Retrofit 인스턴스에 지정된 컨버터를 사용하여 변환된다.
변환기를 추가하지 않으면 RequestBody만 사용할 수 있다.
def retrofit_version = "2.8.1"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.okhttp3:logging-interceptor:4.8.1"
먼저, 서버에 어떤 요청을 할 것인지 함수들로 정의한 인터페이스를 만들었다.
interface RetrofitService {
@POST("/signup")
fun requestSignUp(
@Query("userID") userId: String,
@Query("password") userPw: String
): Call<User>
@GET("/signup")
fun getIdList(): Call<List<User>>
}
Retrofit Client를 생성해서 반환해주는 클래스이다. 로그를 찍기위해서 OkHttp와 HttpLoggingInterceptor를 사용하고 있지만, 로그 확인이 필요 없다면 이 과정은 생략하고 그냥 Retrofit.Builder() 작업부터 진행하면 된다. 즉, 꼭 .client()를 할 필요가 없다는 말이다.
addConverterFactory는 Json형식의 파일을 Class 형식으로 자동 변환한다.
object RetrofitClient {
private var retrofitClient: Retrofit? = null
fun getClient(baseUrl: String): Retrofit {
val client = OkHttpClient.Builder()
val loggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Log.d("MainActivity", message)
}
})
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS)
client.addInterceptor(loggingInterceptor)
if (retrofitClient == null) {
retrofitClient = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(client.build())
.build()
}
return retrofitClient!!
}
}
.create()로 RetrofitService 객체를 얻을 수 있다. retrofit.requestSignUp()로 아이디와 비밀번호를 넣어주면 동기작업으로 서버에 요청을 보내고 받은 응답의 성공여부를 결과를 받을 수 있다. 성공여부 말고도 다른 정보도 볼 수 있다. (response.body() 등..)
여기서 execute()는 동기, enqueue()는 비동기로 동작한다. 여기서 비동기 작업으로 처리를 했는데, withContext로 감싸준것을 확인할 수 있다. 이 네트워크 작업은 메인 스레드에서 동작하면 안되기 때문에 백그라운드로 작업을 넘겨준 것이다. 안그러면 에러가 난다.
android.os.NetworkOnMainThreadException
class RetrofitManager {
private var retrofit =
RetrofitClient.getClient("http://z.ebadaq.com:45082").create(RetrofitService::class.java)
suspend fun signUp(userId: String, userPw: String): Boolean {
return withContext(CoroutineScope(Dispatchers.Default).coroutineContext) {
val response = retrofit.requestSignUp(userId, userPw).execute()
response.isSuccessful
}
}
companion object {
val instance = RetrofitManager()
}
}
비동기 작업을 살짝 보면
retrofit.requestSignUp(userId, userPw).enqueue(object: Callback<User>{
override fun onResponse(call: Call<User>, response: Response<User>) {
TODO("Not yet implemented")
}
override fun onFailure(call: Call<User>, t: Throwable) {
TODO("Not yet implemented")
}
})
응답이 성공이면 onResponse()가, 실패하면 onFailure()가 호출된다.
객체로 respone body를 받는 부분에서 조금 더 설명하자면, 원래 이런값이 응답 결과로 들어오는 경우가 있다고 가정하자.
{
"createdAt": 1662991084703,
"updatedAt": 1662991084703,
"id": 13,
"userID": "ivy",
"password": "YWVzLTI1Ni1nY20kJGRlZmF1bHQ=$BDCelAs9rnDXyvgg$vVevWdsfBQrWaICb$q1I4pObyVuO6NP8MRb57rg"
}
그러면 나는 이렇게 클래스를 정의할 수 있겠다.
data class User(
@SerializedName("createdAt")
val createdAt: String,
@SerializedName("updatedAt")
val updatedAt: String,
@SerializedName("id")
val id: String,
@SerializedName("userID")
val userID: String,
@SerializedName("password")
val password: String
)
여기서 몇가지 실험을 해봤는데, 먼저 code 변수를 추가해주었다. 이 변수에는 절대로 값이 들어오지 않는다. 오류가 날 줄 알았지만 그냥 null값으로 알아처 처리해준다.
data class User(
@SerializedName("createdAt")
val createdAt: String,
@SerializedName("updatedAt")
val updatedAt: String,
@SerializedName("id")
val id: String,
@SerializedName("userID")
val userID: String,
@SerializedName("password")
val password: String,
@SerializedName("code")
val code: String,
)
User(createdAt=1665210537031, updatedAt=1665210537031, id=411, userID=mococo111, password=YWVzLTI1Ni1nY20kJGRlZmF1bHQ=tfQ/g2klp5wopy3I$Oc37sCOCff+KD0k1prsgOg, code=null)
다음은, 반대로 멤버 변수를 하나 지워봤다. 이 또한 오류가 나지 않고, 그냥 이 값은 없는대로 그대로 처리해준다.
data class User(
@SerializedName("createdAt")
val createdAt: String,
@SerializedName("updatedAt")
val updatedAt: String,
@SerializedName("id")
val id: String,
@SerializedName("userID")
val userID: String,
)
User(createdAt=1665210581425, updatedAt=1665210581425, id=412, userID=ppppppppp)
Interceptor는 실제 서버 통신이 일어나기 직전, 직후에 요청을 가로채서 무언가 어썸한 작업을 한 후에 다시 원래 흐름으로 돌려놓는 기능을 제공한다.
logging interceptor를 사용하여 정보 흐름의 정보를 로그로 찍어볼 수 있다.
val client = OkHttpClient.Builder()
val loggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Log.d("MainActivity", message)
}
})
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS)
client.addInterceptor(loggingInterceptor)
HttpLoggingInterceptor는 요청 또는 응답 정보를 기록하는 OKHttp의 인터셉터이다. 위와 같이 HTTPLoggingInterceptor 객체를 생성하고 HttpLoggingInterceptor의 level을 설정해준다.
그 후 addInterceptor 메서드를 사용해서 위에서 만든 httpLoggingInterceptor를 호출해주면 된다.