Android

[Android] Retrofit + Kotlin Coroutine 서버 통신하기

Sangyoon98 2023. 1. 29. 18:58

지난번에 포스팅 했던 Retrofit을 사용해서 서버와 통신하는 글을 보고 오시는 것을 추천합니다.

 

[Android] Retrofit을 사용해서 서버와 통신하기

Retrofit이란? Rest API 통신을 위해 구현된 라이브러리 Squareup사의 OkHttp 라이브러리의 상위 구현체 Retrofit은 OkHttp를 네트워크 계층으로 활용하고 그 위에 구축됨 초창기 안드로이드 네트워크 통신은

sangyoon98.tistory.com

Intro

Retrofit을 사용해서 자바와 코틀린 두가지 언어로 통신하는 방법을 포스팅했었다.

하지만 CallBack 메소드를 호출하던 방식이라 코드의 길이도 길었고 완전한 비동기 프로그래밍 방법이 아니었다.

그래서 이번에는 Coroutine을 사용해서 서버 통신을 비동기 처리하도록 하는 방법을 알아 볼 것이다.


Coroutine이란?

코루틴은 비동기 프로그래밍의 한 종류이다.

동시성 프로그래밍의 성질을 띄고 있으며, 매우 가볍고 간결하게 사용할 수 있다.

코틀린에만 있는 기술이 아니라 파이썬, C#, Go, Javascript 등 여러 언어에서 지원하는 기술이다.

 

코루틴의 자세한 내용은 코루틴의 개념을 매우 잘 설명한 글이 있어서 아래 글을 보고 코루틴의 개념을 익히고 오는 것을 추천한다.

 

코틀린 코루틴(coroutine) 개념 익히기 · 쾌락코딩

코틀린 코루틴(coroutine) 개념 익히기 25 Aug 2019 | coroutine study 앞서 코루틴을 이해하기 위한 두 번의 발악이 있었지만, 이번에는 더 원론적인 코루틴에 대해서 알아보려 한다. 코루틴의 개념이 정확

wooooooak.github.io

 

비동기 프로그래밍이라고 하면 자바에는 기본적으로 async, await을 이용해서 비동기로 함수를 호출하거나, 대표적인 라이브러리로 RX Programming(ReactiveX)을 이용한 RXJava가 있다.

 

자바의 ASyncTask와 RXJava를 사용해서 웹 크롤링을 하는 방법도 포스팅을 했었으니 궁금한 사람은 아래 포스팅을 참고하면 된다.

 

[Android] Jsoup 라이브러리를 이용하여 웹 크롤링 (Web Crawling) 해보기 (AsyncTask, RxJava)

웹 크롤링 웹사이트(website), 하이퍼링크(hyperlink), 데이터(data), 정보 자원을 자동화된 방법으로 수집, 분류, 저장하는 것. 웹 크롤링은 이와 같이 인터넷에 나와있는 정보들을 수집하여 보여주는

sangyoon98.tistory.com

 

코틀린도 RXKotlin이 있지만 자바와 다르게 Coroutine을 지원하는 언어이고 어려운 Rx 프로그래밍을 사용할 바에 쉽게 구현할 수 있는 Coroutine을 많이 사용하는게 트렌드이다.


Coroutine으로 서버 통신 해보기

아래 코드는 이전 글인 Retrofit을 사용해서 서버와 통신하기의 코드를 모두 작성했다는 전제 하에 작성된 글입니다.

 

[Android] Retrofit을 사용해서 서버와 통신하기

Retrofit이란? Rest API 통신을 위해 구현된 라이브러리 Squareup사의 OkHttp 라이브러리의 상위 구현체 Retrofit은 OkHttp를 네트워크 계층으로 활용하고 그 위에 구축됨 초창기 안드로이드 네트워크 통신은

sangyoon98.tistory.com

 

의존성 추가

안드로이드 스튜디오 최신 버전이면 Coroutine이 안드로이드 스튜디오에 내장 되어 있을 수 있다.

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0' //Coroutine
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1' //Coroutine-Android
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' //LifecycleScope

API Interface 정의

기존에 사용하던 Interface를 수정해야 하는데 통신하는 코드를 비동기 함수로 전환을 해서 코루틴이 해당 함수에 접근할 수 있도록 suspend를 추가해주어야 한다.

//기존 통신 코드
interface RetrofitInterface {
	@POST("/api/v1/login")
	fun getLogin(@Body loginRequest: LoginRequest): Call<LoginResponse>
}

//Coroutine을 사용한 통신 코드
interface RetrofitInterface {
	@POST("/api/v1/login")
	suspend fun getLogin(@Body loginRequest: LoginRequest): LoginResponse
}

통신처리

먼저 기존의 사용하던 통신 코드를 살펴보면,

Retrofit.Builder클래스를 불러와 loginRequest를 인자로 retrofitInterface의 getLoginResponse 함수를 CallBack 메소드로 호출하여 비동기 작업을 실행했다.

결과에 따라 통신 성공 시 onResponse, 통신 실패 시 onFailure 함수를 호출했다.

//기존 통신 코드
RetrofitClient.retrofitInterface.getLoginResponse(loginRequest).enqueue(object : Callback<LoginResponse> {
	override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
		//통신 성공
		if(response.isSuccessful() && response.body() != null) {
			//response.body()를 result에 저장
			var result: LoginResponse = response.body()!!

			//받은 코드 저장
			var resultCode: Int = response.code()
						
			var success = 200 //로그인 성공
			var errorTk = 403 //토큰 유효x
			var errorId = 500 //아이디, 비밀번호 일치x

			if (resultCode == success) {
				//성공 처리 code == 200    ex)결과 저장
				var tokenList: LoginResponse.TokenList = result.getList()
				var accToken: String = tokenList.getAccessToken()
				var refToken: String = tokenList.getRefreshToken()

			} else if (resultCode == errorId) {
				//실패 처리 code == 403
			} else if (resultCode == errorTk) {
				//실패 처리 code == 500
			} else {
				//실패 처리
			}

		} else {
			//실패 처리
		}
	}

	override fun onFailure() {
		// 실패 처리
	}
}

하지만 CallBack 메소드 없이 Coroutine을 사용하는 방법은 아래와 같다.

  1. CoroutineScope를 생성하여 Coroutine이 작동할 Scope를 작성한다.
  2. launch()를 통해 코루틴을 실행한다.
  3. 통신 성공 실행 코드를 작성하고 통신 실패 시 예외 처리를 작성한다.

1. 먼저, CoroutinScope를 생성하여 Coroutine이 작동할 Scope를 전역변수로 설정한다.

//CoroutineScope
private val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
	get() = lifecycle.coroutineScope

 

모든 코루틴은 Scope 내에서 실행되어야 하는데 이를 통해서 액티비티 또는 프래그먼트의 생명주기에 따라 소멸 될 때 관련 코루틴을 취소할 수 있다. 이는 곧 메모리 누수를 방지할 수 있다.

CoroutineScope는 CoroutineContext 타입 필드를 launch 등 확장 함수 내부에서 사용하기 위한 매개체 역할만 담당한다.

CoroutoneContext는 실제로 코루틴이 실행중인 여러 작업(Job)과 디스패쳐를 저장하는 일종의 맵이라고 보면 된다. 이를 통해 코틀린 런타임은 다음에 실행할 작업을 고르고 어떤 스레드에 배정할지 결정한다.

GlobalScope 앱의 생명주기와 함께 동작하기 때문에 실행 도중에 별도 생명 주기 관리가 필요 없음. 시작~종료까지 긴기간 실행되는 코루틴의 경우에 적합
CoroutineScope 버튼을 눌러 다운로드 하거나 서버에서 이미지를 열 때 등 필요할 때만 열고 완료되면 닫아주는 스코프
ViewModelScope Jetpack 아키텍쳐의 뷰모델 컴포넌트 사용시 ViewModel 인스턴스에서 사용하기 위해 제공되는 스코프. 해당 스코프로 실행되는 코루틴은 뷰모델 이스턴스가 소멸될 때 자동으로 취소 됨

 

안드로이드에서는 CoroutineScope를 사용하지만 그중에서도 lifecycle에 따라 코루틴을 자동으로 취소해주는 기능까지 추가된 lifecycleScope를 사용하는 것이 적절하다. 그냥 CoroutineScope를 사용해도 되지만 굳이 lifecycleScope를 사용해야하는지에 대한 내용은 아래의 글에서 자세하게 분석되어 있으니 아래 글을 참고하면 된다.

 

Coroutine과 Retrofit을 같이 사용하면 enqueue를 쓰지 않아도 되는 이유는?

이 포스팅을 작성하는 이유 안드로이드를 처음 접할 때 retrofit을 이용해 서버와 통신할 경우 아래와 같은 코드를 작성하게됩니다. interface UserApi { @GET("api/") suspend fun getUserList( @Query("page") page: Int,

seokzoo.tistory.com


2. launch()를 통해 코루틴을 실행한다.

//Retrofit + Coroutines
lifecycleScope.launch(exceptionHandler) {
	val result = retrofitInterface.getLogin(LoginRequest)
    
	var tokenList: LoginResponse.TokenList = result.list
	var accToken: String = result.data.accessToken
	var refToken: String = result.data.refreshToken
}

위 코드처럼 result 변수에 Interface에서 선언한 suspend 함수를 호출한 결과를 저장만 하면 통신한 값들이 result에 들어오게 된다.

기존의 CallBack 메소드를 이용하여 통신했을 때 보다 코드가 매우 간결해졌다.

또한 코드가 마치 동기 프로그래밍인 것 처럼 비동기 처리를 했다고 생각도 안 들 정도로 쉽게 통신을 할 수 있게 되었다.

 

하지만 위 코드대로 작성을 하면 exceptionHandler 부분이 에러가 날 것이다.

이 부분은 예외 처리를 위한 핸들러인데, CallBack 메소드로 통신한 코드와 다르게 위 코드는 onFailure 함수와 같은 것이 없어서 예외처리를 할 수 없다.

그래서 코루틴에서는 예외 처리를 위한 방법이 따로 있다.

  • try..catch 블럭
  • Wrapper Class
  • Coroutine Exception Handling

위 세가지 방법으로 통신 에러 처리를 해야한다.

여기서는 Coroutine Exception Handling을 사용해서 예외처리를 할 것이다.

왜냐하면 try..catch 블럭을 사용하면 try..catch 구문이 수없이 늘어날 수 있고, Wapper Calss를 사용하면 또다른 많은 class나 함수를 작성해야한다.

따라서 핸들러로 예외에 따라 예외값을 반환하는 Coroutine Exception Handler를 사용할 것이다.

 

예외 처리에 관한 자세한 내용은 아래 글에서 확인할 수 있다.

 

[Android] Coroutine, Retrofit을 활용한 비동기 네트워킹 처리 중 에러 핸들링

개요 안드로이드에서 비동기 처리를 하는 대표적인 방법 중 하나는 Retrofit과 Coroutine을 활용하는 것이다. 이 과정에서 다양한 네트워크 오류 상황에 대응하기 위한 다양한 에러 핸들링 방법에 대

bb-library.tistory.com


3. 통신 성공 실행 코드를 작성하고 통신 실패 시 예외 처리를 작성한다.

//Coroutine Exception Handler
private val exceptionHandler = CoroutineExceptionHandler{ _, throwable ->
	throwable.printStackTrace()

	when(throwable){
		is SocketException -> Log.d("logFish", "SocketException")
		is HttpException -> {
			Log.d("logFish", "HttpException: code = ${throwable.code()}, message = ${throwable.message()}, error = ${throwable.response()?.errorBody()?.string()}")
		}
		is UnknownHostException -> Log.d("logFish", "UnknownHostException")
		else -> Log.d("logFish", "Exception")
	}
}

CoroutineScope에서 예외 종류에 따라 로그를 출력하도록 해놨다.

로그 자리에 에외 처리 코드를 작성하면 된다.

 

실제로 status = 400 Bad Request를 유도해서 에러를 통신해본 결과이다.

예외 종류가 HttpException에 에러코드는 400이라고 잘 뜬다.

 

추가로 에러 내용이 담긴 errorBody()가 필요하다면 throwable.response()?.errorBody()?.string()으로 에러 내용을 확인 할 수 있다.


마치며

코루틴을 사용해서 통신을 해보았는데 아직 미비한 점도 많다.

예외 처리 핸들러도 싱글톤 패턴을 사용하면 더욱 코드가 간결해질 것 같은데 코루틴에 Context까지 액티비티 주기와 연관 된 것들이 많아서 쉽게 클래스 분할을 할 수 없었다.

그래도 예외 처리에 성공하고 에러 코드랑 에러 바디까지 모두 조회에 성공해서 통신에는 큰 문제가 없을 것이라고 생각한다.

 

몇주간 코루틴을 공부하면서 느낀건데 지금까지 쓴 코루틴은 극히 일부분일 것이라는 생각이 든다.

Scope라던지 Dispatcher라던지 알아야 할 게 아직도 많다.

그래도 확실한건 Rx프로그래밍 보단 쉬운 것 같다^^7


참고

https://wooooooak.github.io/kotlin/2019/08/25/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EA%B0%9C%EB%85%90-%EC%9D%B5%ED%9E%88%EA%B8%B0/

https://seokzoo.tistory.com/4

https://whyprogrammer.tistory.com/596

https://bb-library.tistory.com/264

https://gngsn.tistory.com/207