안드로이드 공식문서 예제 따라하는 포스팅입니다.
1. 백그라운드 스레드에서 실행 예제
- '네트워크 요청' 작업은 작업이 지연되거나 부하되는 경우 문제가 되므로 이 작업을 코루틴에서 백그라운드 스레드에서 이 작업을 실행해보고자 한다.
▽ 네트워크 요청을 기본 스레드에서 했을 때 위험상황
기본 스레드에서 네트워크 요청을 보내면 응답을 받을 때까지 스레드가 대기하거나 차단됩니다. 스레드가 차단되는 경우 이로 인해 OS는 onDraw()를 호출할 수 없으므로 앱이 정지되고 애플리케이션 응답 없음(ANR) 대화상자가 표시될 수 있습니다
위와 같은 문제를 가진 코드가 아래와 같으며 문제해결을 위해 코루틴을 적용한다.
▽
1) 네트워크 요청 작업 (@Repository클래스)
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
class LoginRepository(private val responseParser: LoginResponseParser) {
private const val loginUrl = "https://example.com/login"
// Function that makes the network request, blocking the current thread
fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
val url = URL(loginUrl)
(url.openConnection() as? HttpURLConnection)?.run {
requestMethod = "POST"
setRequestProperty("Content-Type", "application/json; utf-8")
setRequestProperty("Accept", "application/json")
doOutput = true
outputStream.write(jsonBody.toByteArray())
return Result.Success(responseParser.parse(inputStream))
}
return Result.Error(Exception("Cannot open HttpURLConnection"))
}
}
ㄴ> makeLoginRequest() 가 동기식이며 호출 스레드를 차단합니다.
ㄴ> 네트워크 요청의 응답을 모델링하기 위해 자체 Result 클래스를 사용합니다.
▼
2) 사용자가 버튼을 클릭할 때 네트워크 요청을 트리거 (@ViewModel클래스)
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
ㄴ> ViewModel은 사용자가 예를 들어 버튼을 클릭할 때 네트워크 요청을 트리거합니다.
ㄴ> 위 LoginViewModel은 네트워크 요청을 보낼 때 UI 스레드를 차단합니다
▼
이제 이 실행을 기본 스레드 외부로 이동하는 방법으로
새로운 코루틴을 만들고 I/O 스레드에서 네트워크 요청을 실행하도록 수정해보자!
▼
3) 2번의@ViewModel클래스 수정
새 코루틴을 만들며, I/O 작업용으로 예약된 스레드에서 독립적으로 네트워크 요청이 이루어집니다.
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine to move the execution off the UI thread
viewModelScope.launch(Dispatchers.IO) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
}
ㄴ> 7라인) viewModelScope.launch(Dispatchers.IO) {
- viewModelScope는
- ViewModel KTX 확장 프로그램에 포함된 사전 정의된 CoroutineScope
- ViewModel 범위(Scope)에서 실행되므로 사용자가 화면 밖으로 이동하는 것으로 인해 ViewModel이 소멸되는 경우 viewModelScope가 자동으로 취소되고 실행 중인 모든 코루틴도 취소됩니다. - launch { ... } 는 코루틴을 만들고 함수 본문의 실행을 해당하는 디스패처에 전달하는 함수
- Dispatchers.IO 는 이 코루틴을 I/O 작업용으로 예약된 스레드에서 실행해야 함을 나타냅니다.
▼
위의 예에서 한 가지 문제는 makeLoginRequest()를 호출하는 모든 항목이
명시적으로 실행을 기본 스레드 외부로 이동해야 한다는 점입니다.
(∵ 기본 스레드에서 makeLoginRequest를 호출하면 UI가 차단되기 때문이며
그러므로 makeLoginRequest 함수는 기본 안전 함수가 아니다.)
기본 안전(main-safe) 함수 : 기본 스레드에서 UI 업데이트를 차단하지 않는 함수
이 문제를 해결하기 위해 Repository를 수정하는 방법을 알아보겠습니다.
▼
4) 1번의 네트워크 요청 작업 (@Repository클래스) 수정
class LoginRepository(...) {
...
suspend fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
// Move the execution of the coroutine to the I/O dispatcher
return withContext(Dispatchers.IO) {
// Blocking network request code
}
}
}
ㄴ> return withContext(Dispatchers.IO) {
- withContext(Dispatchers.IO) { ... } 코루틴 실행을 다른(I/O)스레드로 이동하여 호출 함수를 기본 안전 함수로 만들고 필요에 따라 UI를 업데이트하도록 설정합니다.
ㄴ> suspend fun makeLoginRequest(
- suspend 키워드 : 코루틴 내에서 함수가 호출되도록 강제하는 Kotlin의 방법입니다.
▼
5) 3번의@ViewModel클래스 수정
- 위에서 makeLoginRequest가 실행을 기본 스레드 외부로 이동했으므로 이제 login 함수의 코루틴이 기본 스레드에서 실행될 수 있어 수정
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine on the UI thread
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
// Make the network call and suspend execution until it finishes
val result = try {
loginRepository.makeLoginRequest(jsonBody)
} catch(e: Exception) {
Result.Error(Exception("Network request failed"))
}
// Display result of the network request to the user
when (result) {
is Result.Success<LoginResponse> -> // Happy path
else -> // Show error in UI
}
}
}
}
ㄴ> viewModelScope.launch {
- viewModelScope.launch { ... } 는
- suspend 함수는 코루틴에서 실행되어야 한다. 따라서 suspend 함수인 makeLoginRequest에서 코루틴은 여전히 필요하다.
- launch가 Dispatchers.IO 매개변수를 사용하지 않습니다.
Dispatcher를 launch에 전달하지 않으면 viewModelScope에서 실행된 코루틴은 기본 스레드에서 실행됩니다.
ㄴ > when (result) { ... }
- 네트워크 요청의 결과가 이제 성공 또는 실패 UI를 표시하도록 처리 추가함
ㄴ> val result = try { .. } catch(e: Exception) {.. }
- try-catch 블록을 이용한 예외처리 추가
- 코루틴 예외를 처리하려면 Kotlin에서 기본으로 제공되는 예외 지원을 사용해도 된다.
6) 이제 로그인 함수 동작 과정
- 앱이 기본 스레드의 View 레이어에서 login() 함수를 호출합니다.
- launch가 기본 스레드에서 네트워크 요청을 보낼 새 코루틴을 만들며, 코루틴이 실행을 시작합니다.
- 코루틴 내에서 이제 loginRepository.makeLoginRequest() 호출은 makeLoginRequest()의 withContext 블록 실행이 끝날 때까지 코루틴의 추가 실행을 정지합니다.
- withContext 블록이 완료되면 login()의 코루틴이 네트워크 요청의 결과와 함께 기본 스레드에서 실행을 재개합니다.
[참조]
-