코딩하는 개굴이

[Kotlin] Coroutine, 잠시 멈춰! 본문

안드로이드/KOTLIN

[Kotlin] Coroutine, 잠시 멈춰!

개굴이모자 2024. 3. 3. 21:42
반응형
해당 내용은 “코틀린 코루틴” 서적을 기반으로 작성되었습니다.

 

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 의 경우 특벌한 케이스라고 볼 수 있다. 코틀린 컴파일러가 대신 코루틴을 실행시켜 주는 것이기 때문에 헷갈리지 않아야한다.

 

반응형
Comments