withContext() 함수

홍성덕·2024년 8월 26일
0

Coroutines

목록 보기
5/14

withContext()

fun main() = runBlocking<Unit> {
    println(Thread.currentThread().name)
    launch(Dispatchers.IO) {
        println(Thread.currentThread().name)
    }
}

// 출력 :
// main @coroutine#1
// DefaultDispatcher-worker-1 @coroutine#2

위의 예시코드에서 출력 결과를 보면 runBlocking()을 통해 생성된 코루틴과 launch()에 의해 생성된 코루틴이 각각 coroutine#1, coroutine#2로 다른 것을 알 수 있다. 참고로 coroutine#1 이렇게 코루틴 이름을 출력하는 방법은 이 글에 설명하였다.

이걸 withContext()를 사용한 코드로 바꿔서 실행해보자.

fun main() = runBlocking<Unit> {
    println(Thread.currentThread().name)
    withContext(Dispatchers.IO) {
        println(Thread.currentThread().name)
    }
}

// 출력 :
// main @coroutine#1
// DefaultDispatcher-worker-1 @coroutine#1

출력 결과를 보면 runBlocking()을 통해 생성된 coroutine#1이 DefaultDispatcher-worker-1 스레드에서 그대로 사용되었다는 것을 알 수 있다.

정리하자면, withContext() 함수는 실행 중이던 코루틴을 그대로 유지한 채로 코루틴의 실행 환경(CoroutineContext)만 변경하여 작업을 처리하는 함수이다. 변경된 코루틴의 실행환경은 withContext() { ... } 블록 내에서만 유효하며 블록 내의 코드가 실행완료되면 다시 이전의 코루틴 실행환경으로 돌아간다.

async-await 대신 withContext() 사용

fun main() = runBlocking<Unit> {
    val resultDeferred: Deferred<String> = async(Dispatchers.IO) {
        delay(1000L)
        return@async "Result"
    }
    val result = resultDeferred.await() // 결과값이 반환될 때까지 대기
    println(result)
}

위의 async-await을 사용하여 결과값을 받는 대신,

fun main() = runBlocking<Unit> {
    val result: String = withContext(Dispatchers.IO) {
        delay(1000L)
        return@withContext "Result"
    }
    println(result)
}

withContext()를 사용하여 결과값을 받을 수도 있다.

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

withContext() 함수의 선언부를 보면 Deferred<T>가 아닌 T 객체를 리턴한다는 것을 알 수 있다. async를 사용할 때는 결과값을 수신할 때까지 대기하기 위해서 await() suspend 함수를 호출해야 하지만, withContext()는 이미 suspend 함수이므로 내부적으로 결과값을 수신할 때까지 대기 후 결과값 T 객체를 리턴한다.


출력결과는 똑같지만 내부적으로 동작하는 방식은 다르다.

fun main() = runBlocking<Unit> {

	// 1. async()
	async(Dispatchers.IO) { ... }

	// 2. withContext()
	withContext(Dispatchers.IO) { ... }
    
}

async() 함수는 코루틴 빌더 함수로 새로운 코루틴을 생성하는 함수이다. runBlocking()을 통해 생성된 코루틴과 상관없이 아예 새로운 코루틴이 생성되어 작업을 처리한다. 예를 들어 runBlocking()을 통해 생성된 코루틴이 coroutine#1 이라고 한다면, async()를 통해 coroutine#2가 생성된다.

위의 예시에서 생성된 coroutine#2는 Dispatchers.IO의 작업 대기열로 이동했다가 Dispatcher가 공유 스레드풀의 백그라운드 스레드로 전송(Dispatch)한다.

하지만 withContex() 함수는 새로운 코루틴을 생성하지 않고 기존의 코루틴을 그대로 사용하기 때문에 runBlocking()을 통해 생성된 coroutine#1이 Dispatchers.IO의 작업 대기열로 이동했다가 Dispatcher가 공유 스레드풀의 백그라운드 스레드로 전송(Dispatch)한다.

withContext() 사용 시 주의할 점

async-await보다 withContext()를 사용하면 코드가 깔끔해지기 때문에 매번 사용하면 좋을 것 같지만, 복수의 독립적인 코루틴을 동시에 처리해야 할 때는 withContext()를 사용하지 말아야 한다.

fun main() = runBlocking<Unit> {
    val first: String = withContext(Dispatchers.IO) {
        delay(1000L)
        return@withContext "First"
    }

    val second: String = withContext(Dispatchers.IO) {
        delay(1000L)
        return@withContext "Second"
    }
}

위의 예시 코드에서 first와 second 작업은 비동기적으로 작동하는 것처럼 보이지만 사실은 그렇지 않다. withContext() 함수는 새로운 코루틴을 생성하지 않기 때문에 위 코드는 순차적으로 실행된다. withContext()가 suspend 함수라는 것도 다시 한번 상기하자.

그래서 위 코드가 모두 실행되는 데에 걸리는 시간은 2000ms(2초) 정도가 걸릴 것이다. first와 second는 서로 관련 없는 독립적인 작업인데 이런 식으로 작업이 처리되면 효율성이 굉장히 떨어진다.

이러한 경우에는 withContext()를 사용하지 말고, 모든 코루틴이 실행된 후 한꺼번에 await() 함수를 호출하거나, awaitAll()을 호출하여 작업을 처리해야 1000ms(1초) 정도 걸리도록 효율적으로 처리될 것이다.


참고자료

profile
안드로이드 주니어 개발자

0개의 댓글

관련 채용 정보