- Today
- Total
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- jlpt
- posting
- 일본어기초
- 인공지능
- rxjava
- Kotlin
- 코틀린
- 안드로이드
- CustomTab
- 일본어문법
- pullrequest
- 책추천
- errorhandling
- 학습지
- ai
- webflux
- KotlinInAction
- suspend
- 진짜일본어
- 진짜학습지후기
- blog
- PR
- 진짜학습지
- github
- GIT
- 책리뷰
- n3문법
- Android
- androidstudio
- coroutine
코딩하는 개굴이
[Kotlin] Coroutine, 잠시 멈춰! 본문
해당 내용은 “코틀린 코루틴” 서적을 기반으로 작성되었습니다.
Coroutine 의 중단은 어떻게 작동할까?
코루틴의 중심이 되는 기능이자 스레드 등과의 큰 차이점 중 하나가 잠시 멈추는, 중단에 있다는 것을 들어보았을 것이다.
마치 비디오 게임을 하다가 체크포인트나, 세이브를 하는 것처럼 중단 후 돌아올 수 있는 것이다.
이 부분에서 코루틴은 스레드와 크게 차이점을 가진다. 스레드는 저장이 불가하고 멈추는 것만 가능하나, 코루틴은 중단 시에 어떤 자원도 사용하지 않으면서 돌아올 때 다른 스레드에서 실행하는 것도, 중단되었을 때 반환하는 Continuation 객체의 직렬화/역직렬화까지 가능하기 때문이다.
그렇다면 도대체 어떤 점에서 다른지, 어떻게 중단과 재개는 동작하는지 한번 알아보도록 하자.
재개
중단은 스레드든 어디에서도 할 수 있지만 재개가 어찌 가능할까?
그 예시를 들기 위해 우선 코루틴 빌더 (runBlocking/launch 등)로 코루틴을 만들어도 가능하지만, 중단 함수를 만들어보자.
중단함수란? | 코루틴을 중단 할 수 있는 함수로, 함수 내부의 코루틴이 일시 중단해야하는 동작이 있으면 코루틴이 suspend (일시 중단) 되고, 그 기간동안 thread 에서 다른 작업을 수행할 수 있도록 자원 효율을 높인다. 참고 링크: https://hooun.tistory.com/161 |
suspend fun main() {
println("Before")
suspendCoroutine<Unit> {}
println("After")
}
// Before
// After
위 코드를 보면, Before 와 After 사이의 지점에 suspendCoroutine 함수를 사용해 중단시킨 것을 볼 수 있다.
실행 시, “After” 는 출력되지 않으며 코드는 실행된 상태로 끝나지 않고 유지되게 된다.
재개에 사용되는 Continuation,
아까 서론에서 우리는 코루틴의 중단 시, Continuation 이 반환되며 이를 이용해 재개할 수 있다고 분명히 들었는데, suspendCoroutine 에서는 아무것도 보이지 않는다. 사실 람다 안에는 인자가 하나 생략되어있다.
suspend fun main() {
println("Before")
suspendCoroutine<Unit> { continuation ->
println("Before suspending")
}
println("After")
}
// Before
// Before suspending
이 continuation 객체를 사용해 재개할 수 있도록 suspendCoroutine 안의 람다는 중단되기 직전에 호출된다. 따라서, 이를 이용해 resume 에 사용할 수 있다.
suspend fun main() {
println("Before")
suspendCoroutine<Unit> { continuation ->
continuation.resume(Unit)
}
println("After")
}
// Before
// After
드디어 After 문구를 보았다. 그러나, 사실 위 예시는 곧바로 람다 안에 재개를 넣었기 때문에, 사실 정확하게 보면, 최적화로 인해 중단 후 재개되는 것이 아니라 아예 중단되지 않는다.
더 좋은 예시가 없을까? 그렇다면 잠시 멈추었다가 resume 이 될 수 있도록 하는 방법을 생각해보자.
fun continueAfterSleep(continuation: Continuation<Unit>) {
thread {
Thread.sleep(1000)
continuation.resume(Unit)
}
}
suspend fun main() {
println("Before")
suspendCoroutine<Unit> { continuation ->
continueAfterSleep(continuation)
}
println("After")
}
// Before
// ... 1 sec after ...
// After
의도한 대로 잘 멈추었다가 재개되었다. 한가지 보안해보자면, thread 에 대해 생성하는 비용이 많이 들기 때문에 대신, JVM 이 제공하는 ScheduledExecutorService 를 사용해 알람시계처럼 정해진 시간이 지나면 continuation.resume(Unit) 을 호출하도록 알람을 설정 할 수 있다.
private val executor = Executors.newSingleThreadScheduledExecutor {
Thread(it, "scheduler").apply { isDaemon = true }
}
suspend fun delay(timeMillis: Long): Unit =
suspendCoroutine { cont ->
executor.schedule({
cont.resume(Unit)
}, timeMillis, TimeUnit.MILLISECONDS)
}
suspend fun main() {
println("Before")
delay(1000)
println("After")
}
// Before
// ... 1 sec after ...
// After
위 예시에서 Thread 를 사용했기 때문에 아까 스레드를 생성하는 비용이 많이 들었다는 얘기가 떠오를 것이다. 그러나, executor 의 경우 스레드를 사용하긴 하지만, delay 함수를 사용하는 모든 코루틴의 전용 스레드이므로, 기존의 방식처럼 하나의 스레드를 대기할 때 마다 블로킹 시키는 방법보다 낫다고 한다.
자, 여기까지 왔다면 위 예시가 무언가 어디서 많이 본 것 같은 기분이 들 수도 있다.
그렇다. 위 코드는 사실 코루틴 라이브러리에서 delay 가 구현되는 방법과 동일하다. Thread.sleep 과 delay 의 차이점을 이제 확연히 알 수 있을 것이다.
값으로 재개하기
이전 코드에서 cont.resume(Unit) 라는 것이라거나, suspendCoroutine<Unit> 과 같이 사용하던 것이 마음이 걸렸을 수도 있다.
이것 들은 왜 넣는 것일까?
우선 간단히 말하자면 Unit 타입은 Continuation 의 제네릭 타입 인지이자, suspendCoroutine 을 호출 시, 객체로 반환될 값에 대해 지정되는 값이다.
예를들어, API 를 호출해 특정 데이터에 대한 네트워크 응답을 기다리는 흔한 상황을 가정해보자.
만일 코루틴이 없다면 스레드는 마냥 응답을 기다리고 있을 수 밖에 없지만, 코루틴은 중단함과 동시에,
‘데이터를 받으면 resume 함수에 보내줘.’ 라고 continuation 객체를 통해 라이브러리에 전달하고 스레드 본인은 다른 일을 하러 가는 것이다.
suspend fun requestUser(): User {
return suspendCoroutine<User> { cont ->
requestUser { user ->
cont.resume(user)
}
}
}
suspend fun main() {
println("Before")
val user = requestUser()
println(user)
println("After")
}
// Before
// ... 1 sec after ...
// User (name = Test)
// After
예외로 재개하기
위 예시에서 흔히 발생하는 404 등 뭔가의 에러가 발생하는 경우 어떻게 처리할까?
resumeWithException 은 중단된 지점에서 인자로 넣어준 예외를 던질 수 있는데, 이를 try catch 로 감싸는 것도 방법이 될 수 있다.
그러나, 위의 requestUser 에 이를 적용하자면 어떻게 될까? Cancel 이 가능한 suspendCoroutine 인 suspendCancellableCoroutine 을 아래처럼 사용할 수 있다.
suspend fun requestUser(): User {
return suspendCancellableCoroutine<User> { cont ->
requestUser { resp
if(resp.isSuccessful) {
cont.resume(user)
} else {
cont.resumeWithException(ApiException(resp.code, resp.message))
}
}
}
}
suspend fun main() {
println("Before")
val user = requestUser()
println(user)
println("After")
}
// Before
// ... 1 sec after ...
// User (name = Test)
// After
코루틴의 중단 | 예시에서 지금까지 suspend main 을 많이 사용하였지만, 어디까지나 중단 함수는 함수가 아닌 코루틴을 중단시킨다. 그러나, suspend main 의 경우는 누가 코루틴을 이용해서 실행해주는 것일까? main 의 경우 특벌한 케이스라고 볼 수 있다. 코틀린 컴파일러가 대신 코루틴을 실행시켜 주는 것이기 때문에 헷갈리지 않아야한다. |
'안드로이드 > KOTLIN' 카테고리의 다른 글
[KOTLIN] 코루틴 와다다 훑기 (~잡과 자식 코루틴 기다리기) (0) | 2024.05.18 |
---|---|
[Kotlin] Data class, equals 를 재정의 할 거면 hashCode는 ? (0) | 2024.03.11 |
[Kotlin] Serializable 과 Parcelable (1) | 2024.02.04 |
[Android] Jetpack Compose 한 입 찍먹하기(List/Navigation/Dialog) (0) | 2024.01.21 |
코드로 Coroutine 동작 파악하기 (0) | 2023.09.17 |