[인프런] 2시간으로 끝내는 코루틴 - 3~5강 정리
안녕하세요, 해을입니다🦖
코루틴 복습 차원에서 인프런 강의 중 ‘2시간으로 끝내는 코루틴’ 강의를 수강하게 되었는데요.
이번 글에서는 코루틴 기초 섹션 내 3~5강내용을 정리해보겠습니다!
💡3강. 코루틴 빌더와 Job
🥨 runBlocking
새로운 코루틴을 만들고, 루틴 세계와 코루틴 세계를 이어줌
runBlocking으로 생성된 코루틴과 그 안에서 실행되는 모든 코루틴이 완료될 때까지 현재 스레드를 블로킹잘못 사용하면 스레드가 블락되어 다른 코드를 실행 할 수 없게 됨
⇒ 일반 로직에서는 지양해야 하며, 프로그램 진입점(main 함수)이나 테스트 코드에서만 사용하는 것이 권장됨fun main() { runBlocking { printWithThread("START") launch { delay(2_000L) // yield() printWithThread("LAUNCH END") } } printWithThread("END") }- END가 출력되기 위해서는
runBlocking내부의 모든 코루틴이 모두 완전히 종료되어야 함
- END가 출력되기 위해서는
🥨 launch
반환값이 없는 작업을 실행할 때 사용
runBlocking과 달리, 코루틴을 제어할 수 있는 Job 객체를 반환반환된
Job객체를 통해 코루틴의 시작 / 취소 / 완료 대기 등의 제어가 가능Job 객체 활용 정리
start(): 코루틴 시작fun example1(): Unit = runBlocking{ val job = launch(start = CoroutineStart.LAZY) { printWithThread("Hello launch") } delay(1_000L) job.start() }CoroutineStart.LAZY: 코루틴이 즉시 실행되지 않도록 설정job.start(): 시작 신호 전달
cancel(): 코루틴 취소fun example2(): Unit = runBlocking { val job = launch { (1..5).forEach { printWithThread(it) delay(500) } } delay(1_000L) job.cancel() }job.cancel(): 취소 신호 전달
join(): 코루틴 종료까지 대기fun example3(): Unit = runBlocking { val job1 = launch { delay(1_000L) printWithThread("Job 1") } job1.join() val job2 = launch { delay(1_000L) printWithThread("Job 2") } }job1.join()사용 X- 약 1.1초 소요
- job1에서 1초를 기다리는 동안 job2가 시작되어 함께 1초를 기다리기 때문
job1.join()사용 O- 2초 이상 소요
join()을 호출하여 첫 번째 코루틴이 완전히 끝날때까지 기다리기 때문
🥨 async
값을 반환하는 코루틴을 만들 때 사용
launch()와 마찬가지로 코루틴 제어 객체를 반환하지만, 반환 타입은Deferred<T>Deferred는Job의 하위 타입으로 Job과 동일한 기능들이 있고, 실행 결과를 가져오는await()함수를 제공fun main(): Unit = runBlocking { val job = async { 3+5 } val eight = job.await() printWithThread(eight) }async 장점
1️⃣ 여러 API를 동시에 호출하여 성능 개선
```kotlin fun main(): Unit = runBlocking { val time = measureTimeMillis { val job1 = async { apiCall1() } val job2 = async { apiCall2() } printWithThread(job1.await() + job2.await()) } printWithThread("소요 시간 : $time ms") } suspend fun apiCall1(): Int { delay(1_000L) return 1 } suspend fun apiCall2(): Int { delay(1_000L) return 2 } ``` ``` [main @coroutine#1] 3 [main @coroutine#1] 소요 시간 : 1013 ms ``` - 두 API가 병렬로 실행되어 전체 시간이 단축됨2️⃣ 콜백 없이 동기식 코드처럼 작성 가능
```kotlin fun main(): Unit = runBlocking { val time = measureTimeMillis { val job1 = async { apiCall1() } val job2 = async { apiCall2(job1.await()) } printWithThread(job2.await()) } printWithThread("소요 시간 : $time ms") } suspend fun apiCall1(): Int { delay(1_000L) return 1 } suspend fun apiCall2(num: Int): Int { delay(1_000L) return num + 2 } ``` ``` [main @coroutine#1] 3 [main @coroutine#1] 소요 시간 : 2024 ms ``` - 비동기 처리지만, 코드 흐름은 동기처럼 읽힘async 주의사항
1️⃣
CoroutineStart.LAZY옵션을 사용하면,await()함수를 호출했을 때 계산 결과를 계속 기다림```kotlin fun main(): Unit = runBlocking { val time = measureTimeMillis { val job1 = async(start = CoroutineStart.LAZY) { apiCall1() } val job2 = async(start = CoroutineStart.LAZY) { apiCall2() } printWithThread(job1.await() + job2.await()) } printWithThread("소요 시간 : $time ms") } suspend fun apiCall1(): Int { delay(1_000L) return 1 } suspend fun apiCall2(): Int { delay(1_000L) return 2 } ``` ``` [main @coroutine#1] 3 [main @coroutine#1] 소요 시간 : 2024 ms ``` - `await()` 호출 시점에 코루틴이 **순차적으로 실행** - 결과적으로 동시 실행 효과가 사라짐2️⃣ 지연 코루틴을
async()와 함께 사용하는 경우라도, 동시에 API 호출을 원한다면start()함수를 먼저 사용해야 함```kotlin fun main(): Unit = runBlocking { val time = measureTimeMillis { val job1 = async(start = CoroutineStart.LAZY) { apiCall1() } val job2 = async(start = CoroutineStart.LAZY) { apiCall2() } job1.start() job2.start() printWithThread(job1.await() + job2.await()) } printWithThread("소요 시간 : $time ms") } ``` [main @coroutine#1] 3 [main @coroutine#1] 소요 시간 : 1018 ms ``` - `start()`를 명시적으로 호출해야 동시에 실행
💡4강. 코루틴의 취소
- 더 이상 필요하지 않은 코루틴은 적절히 취소하여 컴퓨터 자원을 절약해야 함
🥨 취소 방법
cancel()함수 사용 => 단, 취소 대상인 코루틴이 취소에 협조(cooperative)해야 함
👉 코루틴의 취소는 강제 종료가 아닌 ‘요청’ 이기 때문에, 코루틴이 취소 신호를 인지하고 스스로 멈춰야 한다.
🥨 취소에 협조하는 방법
1️⃣ delay() / yield() 같은 kotlinx.coroutines 패키지의 suspend 함수 사용
fun main(): Unit = runBlocking {
val job1 = launch {
delay(1_000L)
printWithThread("Job 1")
}
val job2 = launch {
delay(1_000L)
printWithThread("Job 2")
}
delay(100) // 첫 번째 코루틴 코드가 시작되는 것을 잠시 기다림
job1.cancel()
}
[main @coroutine#3] Job 2
suspend 함수는 실행 전에 취소 여부를 체크해서 취소에 협조하도록 되어 있음
suspend 함수 사용하지 않을 경우 취소 안됨
fun main(): Unit = runBlocking { val job = launch { var i = 1 var nextPrintTime = System.currentTimeMillis() while (i <= 5) { if(nextPrintTime <= System.currentTimeMillis()){ printWithThread("${i++}번재 출력!") nextPrintTime += 1_000L } } } delay(100L) job.cancel() }[main @coroutine#2] 1 번째 출력! [main @coroutine#2] 2 번째 출력! [main @coroutine#2] 3 번째 출력! [main @coroutine#2] 4 번째 출력! [main @coroutine#2] 5 번째 출력!- 협력하는 코루틴이여야만 취소 가능
2️⃣ 코루틴이 스스로 상태를 확인하고 취소 처리
코루틴 내부에서 직접 취소 상태를 확인
취소 요청을 받았다면 CancellationException을 던져 종료
fun main(): Unit = runBlocking { val job = launch(Dispatchers.Default){ var i = 1 var nextPrintTime = System.currentTimeMillis() while (i <= 5) { if(nextPrintTime <= System.currentTimeMillis()){ printWithThread("${i++}번재 출력!") nextPrintTime += 1_000L } if(!isActive){ throw CancellationException() } } } delay(100L) printWithThread("취소 시작") job.cancel() }[DefaultDispatcher-worker-1 @coroutine#2] 1 번째 출력! [main @coroutine#1] 취소 시작isActive- 코루틴 빌더(
launch,async) 내부에서 접근 가능한 프로퍼티 - 현재 코루틴이 활성화 되어 있는지 / 취소 신호를 받았는지 구분
- 코루틴 빌더(
Dispatchers.Default- 취소 신호를 정상적으로 전달하려면, 우리가 만든 코루틴이 다른 스레드에서 동작해야 함
Dispatchers.Default를launch()함수에 전달하면 코루틴을 다른 스레드에서 동작시킬 수 있음
💡5강. 코루틴의 예외 처리와 Job의 상태 변화
🥨 중첩된 코루틴 간의 관계
코루틴은 부모-자식 관계를 가질 수 있음
가장 바깥에서 시작된 코루틴을 흔히 root 코루틴이라고 부름
fun main(): Unit = runBlocking { val job1 = launch { delay(1_000L) printWithThread("Job 1") } val job2 = launch { delay(1_000L) printWithThread("Job 2") } }runblocking: 부모 코루틴job1,job2: 자식 코루틴
🥨 새로운 root 코루틴 생성
코루틴을 새로운 CoroutineScope에서 생성하면 기존 부모와 분리된 독립적인 root 코루틴이 됨
이를 위해
CoroutineScope()를 사용fun lec05Example1(): Unit = runBlocking { val job1 = CoroutineScope(Dispatchers.Default).launch { delay(1_000L) printWithThread("Job 1") } val job2 = CoroutineScope(Dispatchers.Default).launch { delay(1_000L) printWithThread("Job 2") } }job1,job2:runBlocking과 부모-자식 관계가 아닌 독립적인 코루틴
🥨 예외 발생 처리
launch- 예외가 발생하면, 해당 예외를 출력하고 코루틴 종료
fun main(): Unit = runBlocking { val job = CoroutineScope(Dispatchers.Default).launch { throw IllegalArgumentException() } delay(1_000L) }Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.IllegalArgu mentExceptionasync- 예외가 발생해도, 예외를 출력하지 않음(예외 확인하려면,
await()사용) async는launch와 다르게 값을 반환하는 코루틴에 사용되기에,
예외 역시 값을 반환할 때 처리할 수 있도록 설계된 것
fun main(): Unit = runBlocking { val job = CoroutineScope(Dispatchers.Default).async { throw IllegalArgumentException() } delay(1_000L) job.await() }Exception in thread "main" java.lang.IllegalArgumentException- 예외가 발생해도, 예외를 출력하지 않음(예외 확인하려면,
🥨 예외 전파(자식 -> 부모)
async를 사용해도 예외가 발생하는 이유는?⇒ 자식 코루틴의 예외가 부모 코루틴으로 전파되기 때문
fun main(): Unit = runBlocking { val job = async { throw IllegalArgumentException() } delay(1_000L) }Exception in thread "main" java.lang.IllegalArgumentException자식 코루틴의 예외를 부모에게 전파하지 않는 방법
⇒
SupervisorJob()사용fun main(): Unit = runBlocking { val job = async(SupervisorJob()) { throw IllegalArgumentException() } delay(1_000L) // job.await() }async함수에SupervisorJob()을 넣어주면async자식 코루틴에서 발생한 예외가 부모 코루틴으로 전파되지 않음job.await()사용 해야 예외가 발생하는 원래 행동 패턴으로 돌아감
🥨 예외를 다루는 방법
try-catch사용fun main(): Unit = runBlocking { val job = launch() { try { throw IllegalArgumentException() }catch (e: IllegalArgumentException){ printWithThread("정상 종료") } } }[main @coroutine#2] 정상 종료try catch사용하면, 발생한 예외를 잡아 코루틴이 취소되지 않게 만들 수도 있고,
적절한 처리를 한 이후 다시 예외를 던질 수 있음
CoroutineExceptionHandler사용fun main(): Unit = runBlocking { val exceptionHandler = CoroutineExceptionHandler { _, throwable -> printWithThread("예외") throw throwable } val job = CoroutineScope(Dispatchers.Default).launch(exceptionHandler) { throw IllegalArgumentException() } delay(1_000L) }[DefaultDispatcher-worker-1 @coroutine#2] 예외CoroutineExceptionHandler는launch에만 적용 가능- 부모 코루틴이 있으면 동작하지 않음
🥨 코루틴 취소 예외 정리
CASE 1. 발생한 예외가
CancellationException인 경우⇒ 취소로 간주하고 부모 코루틴에게 전파 X
CASE 2. 그 외 다른 예외 발생
⇒ 실패로 간주하고 부모 코루틴에게 전파 O
👉 다만 내부적으로는 취소 실패 모두 취소됨 상태로 관리
.
이번 글에서는 코루틴 생성부터 취소와 예외처리까지 코루틴을 실제로 사용하기 위한 기본 방법들을 정리해봤습니다.
이번 강의 구간부터는 코루틴이 추상적인 개념이 아니라 실제 코드에서 어떻게 쓰이는지 조금씩 감이 잡히기 시작하더라구요!
다음 글에서는 코루틴의 구성 요소와 원리를 다룬 6~7강을 정리해보겠습니다.
끝!🦕
.
👍 참고