해당 내용은 코드스쿼드 2022 안드로이드 미션을 수행하면서 공부한 내용을 정리한 글입니다.
또한 https://jminie.tistory.com/154에 이어지는 포스팅입니다.
앱에서 가장 많이 수행하는 처리 중 하나는 서버에 데이터를 요청하고 받아온 데이터를 단말기로, 즉 클라이언트의 화면에 표시하는 일이다. 이때 클라이언트와 서버가 통신하는 방식은 크게 소켓 연결과 HTTP 연결 두 가지로 나눌 수 있다.
📌 소켓 연결 & HTTP 연결
소켓 연결
- 소켓은 네트워크 상의 두 프로그램 사이에 일어나는 양방향 통신 중 한쪽의 엔드 포인트를 의미한다. (엔드 포인트란 IP와 포트의 조합)
- 소켓 연결방식에서는 클라이언트와 서버가 특정 포트를 통해 연결을 계속 유지하고 있기 때문에 실시간으로 양방향 통신을 할 수 있다.
- 주로 동영상 스트리밍이나, 온라인 게임 등에서 사용된다.
HTTP 연결
- 80번 포트를 사용하여 웹 상에서 정보를 주고받을 수 있는 프로토콜
- HTTP 통신에서는 클라이언트가 서버에 헤더(header)와 바디(body)로 이루어진 메시지를 요청 즉 Request 한다.
- 서버는 이 요청을 처리하고 응답코드와 함께 응답 즉 Response를 하게 된다.
HTTP에 대한 자세한 내용은 다음 포스팅을 참고하면 된다.
https://jminie.tistory.com/116?category=1031317
📌 Android HTTP 통신 라이브러리
Volley
구글에서 2013년 발표한 기존에서 사용하던 HttpUrlConnection의 불편함을 해소할 수 있는 라이브러리
Volley의 특징
- Network Request 들을 자동으로 스케줄링
- 다중 동시 네트워크 연결 지원
- Requset 우선순위 부여 기능
- 커스터마이징 하기 편하다. (예를 들어 retry 나 backoff)
- 디버깅과 트래킹 툴을 지원한다.
Volley 사용법
HTTP 메서드와 url 정보를 가진 Request를 만들어서 RequestQueue에 넣어준다. 그러면 Volley가 알아서 스레드를 만들고 HttpUrlConnection으로 통신을 수행한 뒤 response를 반환한다.
val textView = findViewById<TextView>(R.id.text)
val queue = Volley.newRequestQueue(this)
val url = "https://www.google.com"
val stringRequest = StringRequest(Request.Method.GET, url,
Response.Listener<String> { response ->
// Display the first 500 characters of the response string.
textView.text = "Response is: ${response.substring(0, 500)}"
},
Response.ErrorListener { textView.text = "That didn't work!" })
queue.add(stringRequest)
Retrofit
OkHttp를 개발한 Square에서 2013년 발표한 라이브러리이다.
HttpURLConnection을 사용하기 편하도록 랩핑 한 게 Volley라면 Retrofit은 OkHttp를 랩핑 한 것이다.
Retrofit의 특징
- OkHttp에서는 사용 시 대개 Asynctask를 통해 비동기로 실행하는데, Asynctask가 성능상 느리다는 이슈가 있었다. Retrofit에서는 Asynctask를 사용하지 않고 자체적인 비동기 실행과 스레드 관리를 통해 속도를 훨씬 빠르게 끌어올렸다.
- OkHttp에서도 쿼리 스트링, request, response 설정 등 반복적인 작업이 필요한데, Retrofit에서는 이런 과정을 모두 라이브러리에 넘겨서 처리하도록 하였다. 따라서 사용자는 함수 호출 시에 파라미터만 넘기면 되기 때문에 작업량이 훨씬 줄어들고 사용하기 편리하다.
- 인터페이스 내에 annotation을 사용하여 호출할 함수를 파라미터와 함께 정의해놓고, 네트워크 통신이 필요한 순간에 구현 없이 해당 함수를 호출하기만 하면 통신이 이루어지기에 코드를 읽기가 매우 편하다. Asynctask를 쓰지 않기에 불필요하게 코드가 길어질 필요도 없으며, 콜백 함수를 통해 결과가 넘어오도록 되어있어 매우 직관적인 설계가 가능하다.
Retrofit 사용법
REST API 콜을 인터페이스 형식으로 준비한다. 그리고 Retrofit 객체를 만들어서 인터페이스의 인스턴스를 생성하고, 마지막으로 인터페이스를 동기 혹은 비동기적으로 구동시켜 response를 반환받는다.
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
GitHubService service = retrofit.create(GitHubService.class);
Call<List<Repo>> repos = service.listRepos("octocat");
JSON
Json(JavaScript Object Notiation)은 키-값(key-value)으로 이루어진 구조화된 데이터를 표현하기 위한 문자 기반의 표준 포맷이다.
자바스크립트뿐만 아니라 여러 프로그래밍에서 사용된다.
읽고 쓰는 것이 간단하고, 언어 독립적이며, XML보다 가볍다는 장점이 있어서 네트워크 통신에서 자주 사용된다.
JSON 구조
json은 하나의 문자열로 구성되며 "{key : value}"의 객체 형태를 기본으로 하고 콤마(,)로 구분된다.
value에는 문자열, 숫자, 배열, boolean값과 다른 객체를 포함할 수 있다.
{
"이름": "홍길동",
"나이": 25,
"성별": "남",
"주소": "서울특별시 양천구 목동",
"반려견 유무": true
}
Android에서의 JSON
안드로이드 플랫폼에서는 별도의 라이브러리를 추가하지 않아도 기본적으로 json을 다룰 수 있는 json.org 패키지를 제공한다.
JSONObject, JsonArray 클래스는 전달받은 문자열을 파싱 하여 객체화하고, key를 통해 value를 얻을 수 있는 메서드를 제공한다.
https://developer.android.com/reference/org/json/package-summary
Gson
Gson는 google에서 제공하는 Java 라이브러리로 Java 객체를 Json으로 변환 또는 Json을 Java 객체로 변환하는 데 사용된다.
Gson은 java의 기본적인 데이터 객체뿐만 아니라 사용자가 정의한 데이터 클래스로도 간편하게 변환이 가능하다.
Gson을 사용하려면 build.gradle에 의존성을 추가해야 한다. (버전은 지속해서 업그레이드된다.)
dependencies {
...
implementation 'com.google.code.gson:gson:2.8.6'
}
📌 프로젝트 구현
의존성 추가
위에서 말한 대로 Retrofit과 okHttp 그리고 이 프로젝트에서 추후에 쓰인 Coroutine을 사용하려면 라이브러리를 추가해야 한다.
dependencies {
...
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.8.1'
// OkHttp
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
// Coroutine
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
}
Manifest에 권한 추가
HTTP 통신을 하려면 기본적으로 인터넷을 이용해야 하므로 AndroidMainfest에 인터넷 관련 권한을 추가해준다.
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission-sdk-23 android:name="android.permission.INTERNET"/>
요구사항 분석
API에 다음과 같은 POST 요청 메시지를 보낸다.
- 서버 API 주소 : "BASE_URL/singup"
- "id"는 사용자 아이디와 매칭 한다.
- "password"는 사용자 패스워드와 매칭 한다.
가입 요청이 진행되고, 성공 값이 리턴된다. {"result":"OK", "status":"200"}
SignUpRequestBody data class 구현
요구사항에서 API 통신을 위해 id와 password를 요구했으므로 API 통신 시 RequestBody에 담아줄 DTO를 SignUpRequestBody라는 이름의 data class로 만들어준다.
data class SignUpRequestBody(
val id: String?,
val password: String?
)
SignUpResponseBody data class 구현
요구사항에서 성공 값이 {"result":"OK", "status":"200"}로 알려주었으므로 이에 맞춰 SignUpResponseBody를 만들어준다.
data class SignUpResponseBody(
@SerializedName("result")
val result: String?,
@SerializedName("status")
val status: String?
)
@SerializedName를 사용하면 JSON 키와 다른 이름의 변수명을 사용할 수 있다. 예를 들어, 위 예제에서처럼 @SerializedName을 사용해 JSON 키 "result"이 kotlin 변수 "result"에 매핑되어야 한다고 Gson에게 알려준다.
또한 JSON 키는 일반적으로 snake_case 규칙을 따르기 때문에 @SerializedName 어노테이션을 사용해 변수명에 대한 camelCase 규칙을 일관적으로 유지할 수 있다.
하지만 위에서는 JSON의 키와 kotlin 변수가 동일하다.
이름의 변경이 없는 경우에도 @SerializedName 어노테이션을 붙이는 것이 좋다. 그 이유는 애플리케이션을 Release 할 때 소스 코드가 난독화 되는 과정에서 kotlin 변수가 변환되고, 이로 인해 Gson 매핑에 오작동이 일어날 수 있기 때문에 @SerializedName는 되도록 사용하는 것이 좋다고 한다.
SignUpService 인터페이스 구현
Retrofit은 다음과 같은 방법으로 HTTP API를 Java Interface로 type-safe 하게 변환한다.
interface SignUpService {
@Headers("Content-Type: application/json")
@POST("signup")
fun addUserByEnqueue(@Body userInfo: SignUpRequestBody): Call<SignUpResponseBody> // Call 은 흐름처리 기능을 제공해줌
}
@Headers 어노테이션에 Request Header에 담아줄 데이터를 넣어주고 @POST 어노테이션에 요구사항에 주어진 서버 API 주소의 BASE_URL을 제외한 경로를 적어준다.
메서드의 인자에 @Body 어노테이션을 통해 위에서 만든 SignUpRequestBody를 userInfo라는 이름의 변수로 넣어주고, 메서드의 반환 타입은 Call <SignUpResponseBody>로 지정해준다.
여기서 Call은 흐름 처리 기능을 제공하는 retrofit2 패키지 안에 있는 interface이다.
Retrofit 객체 구현
Retrofit 객체는 비용이 높기 때문에 여러 객체가 만들어지면 자원낭비 및 통신에 혼선이 올 수 있기 때문에 object 키워드를 통해 싱글턴으로 만들어준다.
addConverterFactory()는 데이터를 파싱 할 converter를 추가하는 메서드이다.
JSON과 같은 데이터는 자바나 코틀린에서 바로 사용할 수 있는 데이터 형식이 아니기 때문에
이를 변환해주기 위해 이러한 converter를 사용해야 한다. 우리는 이때 Gson의 GsonConvertFactory를 사용하는 것이다.
안드로이드 클라이언트단 쪽에서 Interceptor를 추가로 사용하면 안드로이드에서 서버에게 데이터 전송 및 수신받을 때 인터셉터 말 그대로 중간에 매개체가 되어 어떠한 처리를 해줄 수 있다.
즉 Interceptor는 실제 서버 통신이 일어나기 직전, 직후에 요청을 가로채서 원하는 작업을 한 후에 다시 원래 흐름으로 돌려놓는 기능을 제공한다.
이렇게 만든 okHttpClient 변수를 retrofit을 만드는 Builder 패턴에서. client의 인자로 넣어준다.
object RetrofitAPI {
private const val BASE_URL = "주어진 BASE_URL"
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
}
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient) // 로그캣에서 패킷 내용을 모니터링 할 수 있음 (인터셉터)
.build()
}
val emgMedService: SignUpService by lazy {
retrofit.create(SignUpService::class.java)
}
}
RetrofitWork 클래스 구현
실제로 서버와 API통신을 하는 클래스를 구현해준다.
여기서 Call 작업은 두 가지로 실행된다.
- execute를 사용하면 request를 보내고 response를 받는 행위를 동기적으로 수행한다. (동기 방식)
- enqueue 작업을 실행하면 request는 비동기적으로 보내고, response는 콜백으로 받게 된다. (비동기 방식)
동기 방식의 execute를 사용해본 적은 없지만 찾아보니 UnitTest 등을 수행할 때 쓰이는 경우가 있다고 한다.
class RetrofitWork(private val userInfo: SignUpRequestBody) {
fun work() {
val service = RetrofitAPI.emgMedService
// Call 작업은 두 가지로 실행됨
// execute 를 사용하면 request 를 보내고 response 를 받는 행위를 동기적으로 수행한다.
// enqueue 작업을 실행하면 request 는 비동기적으로 보내고, response 는 콜백으로 받게 된다.
service.addUserByEnqueue(userInfo)
.enqueue(object : retrofit2.Callback<SignUpResponseBody> {
override fun onResponse(
call: Call<SignUpResponseBody>,
response: Response<SignUpResponseBody>
) {
if (response.isSuccessful) {
val result = response.body()
Log.d("회원가입 성공", "$result")
}
}
override fun onFailure(call: Call<SignUpResponseBody>, t: Throwable) {
Log.d("회원가입 실패", t.message.toString())
}
})
}
}
enqueue의 인자인 Callback 인터페이스는 필수적으로 2가지 메서드를 오버 라이딩해야 하는데 이는 2가지로 나뉜다.
- 통신에 성공했을 경우는 onResponse
- 통신에 실패했을 경우는 onFailure
실패한 경우는 그에 따른 예외처리를 수행해준다.
MainAcvitiy
MainAcvitiy는 입력된 ID와 Password를 RequestBody에 넣어주고 버튼이 눌렸을 때 RetrofitWork클래스의 work 메서드가 실행되게 처리해준다.
val userData = SignUpRequestBody(
binding.idTextInputLayout.editText?.text.toString(),
binding.passwordTextInputLayout.editText?.text.toString()
)
binding.nextButton.setOnClickListener {
val retrofitWork = RetrofitWork(userData)
retrofitWork.work()
}
📱 결과물
📌 + 코루틴 사용
위에서는 SignUpService의 addUser메서드의 반환 타입을 Call 타입으로 지정해주고 RetrofitWork 클래스에서 enqueue를 통해 비동기 작업을 수행하였다.
이번에는 이 비동기 작업을 코루틴으로 바꾸어 보자.
interface SignUpService {
@Headers("Content-Type: application/json")
@POST("signup")
suspend fun addUser(@Body userInfo: SignUpRequestBody): Response<SignUpResponseBody> // 코루틴은 자체적으로 비동기적인 기능을 수행하기 때문에 Call 이 필요 없음
}
위의 SignUpService와 바뀐 점은 메서드 앞에 suspend 키워드가 추가되었다. 코루틴 스코프 안에서 쓰일 모든 메서드는 suspend 키워드를 붙여주어야 한다.
또한 반환 타입이 Call에서 Response로 바뀌었다. 이는 코루틴이 자체적으로 비동기 기능을 수행하기 때문에 흐름 처리를 해주는 Call이 필요하지 않기 때문이다.
class RetrofitWork(private val userInfo: SignUpRequestBody) {
fun work() {
val service = RetrofitAPI.emgMedService
CoroutineScope(Dispatchers.IO).launch {
// POST request 를 보내고 response 를 받는다.
val response = service.addUser(userInfo)
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
val result = response.body()
Log.d("회원가입 성공", "$result")
} else {
Log.d("회원가입 실패", response.code().toString())
}
}
}
}
}
RetrofitWork 메서드에서는 CoroutineScope 안에서 네트워크 작업을 해주기 위해 Dispatchers를 Dispatchers.IO로 지정해주고 enqueue메서드를 통해 구현했던 2가지 메서드가 사라지고 단지 response.isSucessful을 통해 통신의 성공 여부를 가려준다.
동작의 결과는 위의 결과물과 동일하다.
'Android' 카테고리의 다른 글
안드로이드 - Activity Lifecycle (액티비티 생명주기) (0) | 2022.05.05 |
---|---|
안드로이드 [Kotlin] - 코드스쿼드 미션 중요 내용 및 피드백 정리 (0) | 2022.05.04 |
안드로이드 [Kotlin] - TextInputLayout 및 정규식을 이용하여 회원가입 UI 구현 (2) | 2022.04.16 |
안드로이드 [Kotlin] - Retrofit, Moshi 를 이용하여 다운받은 코로나 선별 진료소 Json 데이터를 RecyclerView에 표시하기 (3) | 2022.03.29 |
안드로이드 [Kotlin] - RecyclerView에서 ListAdapter와 DiffUtil 사용기 (0) | 2022.03.27 |