[인프런] 2시간으로 끝내는 코루틴 - 6~10강 정리

안녕하세요, 해을입니다🦖

코루틴 복습 차원에서 인프런 강의 중 ‘2시간으로 끝내는 코루틴’ 강의를 수강하게 되었는데요.

이번 글에서는 코루틴의 구성 요소와 원리를 다룬 6~10강내용을 정리해보겠습니다!

💡6강. Structued Concurrency

🥨 Structued Concurrency

부모 - 자식 관계의 코루틴이 한 몸 처럼 움직이는 것

fun main(): Unit = runBlocking {
    launch {
        delay(600L)
        printWithThread("A")
    }

    launch {
        delay(500L)
        throw IllegalArgumentException("코루틴 실패!")
    }
}
Exception in thread "main" 
java.lang.IllegalArgumentException:  코루틴 실패
  • 두 번째 코루틴에서 발생한 예외가 runBlocking으로 생성된 부모 코루틴에게 취소 신호를 전달
  • 부모 코루틴은 해당 취소 신호를 받아 다른 자식 코루틴(첫 번째 코루틴)에게도 취소를 전파

🥨 정리

  • 공식 문서 정의
    • 수많은 코루틴이 유실되거나 누수되지 않도록 보장
    • 코드 내의 에러가 유실되지 않고 적절히 보고될 수 있도록 보장
  • 취소 및 예외
    • 자식 코루틴에서 예외가 발생하면
      → Structured Concurrency에 의해 부모 코루틴이 취소되고,
      부모의 다른 자식 코루틴들도 함께 취소

    • 자식 코루틴에서 예외가 발생하지 않더라도 부모 코루틴이 취소되면 자식 코루틴들도 모두 취소

    • 단, CancellationException정상적인 취소로 간주되기 때문에 부모 코루틴으로 전파되지 않으며, 다른 자식 코루틴을 취소시키지도 않음

💡7강. CoroutineScope와 CoroutineContext

🥨 CoroutineScope

  • 지금까지 runBlocking이 코루틴과 루틴의 세계를 이어주며 CoroutineScope를 제공

  • launchasyncCoroutineScope의 확장함수

      fun main(): Unit = runBlocking {
          val job1 = CoroutineScope(Dispatchers.Default).launch {
              delay(1_000L)
              printWithThread("Job 1")
          }
      }
    
  • 직접 CoroutineScope를 만들면 runBlocking이 필요 없음

    • main 함수를 일반 함수로 만들어 코루틴이 끌날 때까지 main 스레드를 대기시킬 수 있음

        fun lec07Example2(): Unit = runBlocking {
            CoroutineScope(Dispatchers.Default).launch {
                delay(1_000L)
                printWithThread("Job 1")
            }
              
            Thread.sleep(1_500L)
        }
      
    • main 함수 자체를 suspend 함수로 만들어 join() 가능

        fun main(): Unit = runBlocking {
            val job = CoroutineScope(Dispatchers.Default).launch {
                delay(1_000L)
                printWithThread("Job 1")
            }
              
            job.join()
        }
      

💡CoroutineScope의 주요 역할은 CoroutineContext 라는 데이터를 보관하는 것

🥨 CoroutineCotext

  • 코루틴과 관련된 여러가지 데이터를 갖고 있음
    • 코루틴 이름
    • CoroutineExceptionHandler
    • 코루틴 자체(Job)
    • CoroutineDispatcher
  • Map + Set을 합쳐놓은 형태
    • Element(key - value)로 데이터 저장
    • 같은 key의 데이터는 유일
  • Element 추가/제거
    • + 기호 : 각 element를 합치거나 context에 추가 가능

        // + 기호를 이용한 Element 합성
        CoroutineName("나만의 코루틴") + SupervisorJob()
              
        // context에 Element를 추가
        coroutineContext + CoroutineName("나만의 코루틴")
      
    • minusKey 함수 : element 제거

        coroutineContext.minusKey(CoroutineName.key)
      

🥨 총 정리

  • CoroutineScope
    • 코루틴이 생성되는 영역
  • CoroutineContext
    • 코루틴과 관련된 데이터를 보관
  • Structured Concurrency 기반

    image.png

  • 클래스 내부에서 독립적인 CoroutineScope를 관리

      class AsyncLogic{
          private val scope = CoroutineScope(Dispatchers.Default)
        
          fun doSomething(){
              scope.launch {
                  // 무언가 코루틴이 시작되어 작업!
              }
          }
        
          fun destroy(){
              scope.cancel()
          }
      }
        
      val asyncLogic = AsyncLogic()
      asyncLogic.doSomething()
      asyncLogic.destory() // 필요 없어지면 모두 정리
    
    • 해당 클래스에서 사용하던 코루틴을 한 번에 종료시킬 수 있음

🥨 CoroutineDispatcher

  • 코루틴을 어떤 스레드에서 실행할지 결정하는 역할
  • 주요 디스패처 종류
    • Dispatchers.Default
      • 가장 기본적인 디스패처
      • CPU 자원을 많이 쓸 때 권장
      • 별다른 설정이 없으면 이 디스패처가 사용됨
    • Dispatchers.IO
      • I/O작업에 최적화된 디스패처
    • Dispatchers.Main
      • 보통 UI 컴포넌트를 조작하기 위한 디스패처
      • 특정 의존성을 갖고 있어야 정상적으로 활용 가능
    • Java의 스레드풀인 ExecutorService를 디스패처로 변환
      • asCoroutineDispatcher() 이라는 확장함수를 이용해 ExecutorService를 디스패처로 전환 가능

          val threadPool = Executors.newSingleThreadExecutor()
              CoroutineScope(threadPool.asCoroutineDispatcher()).launch { 
                  printWithThread("새로운 코루틴")
              }
        

💡8강. suspending function

🥨 suspending function

suspend 지시어가 붙은 함수를 의미

suspend 함수 호출 가능
fun main(): Unit = runBlocking {
    launch {
        delay(100L)
    }
}
  • suspend 함수인 delay() 함수가 호출 가능한 이유

    image.png

    launch 함수의 마지막 함수 타입이 suspend lambda 이기 때문

suspension point : 코루틴이 중지 되었다가 재개 될 수 있는 지점
fun main(): Unit = runBlocking {
    launch {
        a()
        b()
    }
    
    launch {
        c()
    }
}

suspend fun a() {
    printWithThread("A")
}

suspend fun b() {
    printWithThread("B")
}

suspend fun c() {
    printWithThread("C")
}

// 출력 결과
// [main @coroutine#2] A 
// [main @coroutine#2] B 
// [main @coroutine#3] C
  • suspend 함수를 호출하더라도 반드시 중지되는 것은 아니기에 A와 B가 먼저 출력되고 나중에 C가 출력됨
여러 비동기 라이브러리를 사용하는데 활용 가능
fun main(): Unit = runBlocking {
    val result1 = async {
        call1()
    }

    val result2 = async {
        call2(result1.await())
    }

    printWithThread(result2.await())
}

fun call1(): Int {
    Thread.sleep(1_000L)
    return 100
}

fun call2(num: Int): Int{
    Thread.sleep(1_000L)
    return num * 2
}
  • runBlocking 입장에서 result1result2 의 타입이 Deferred 이기에 Deffered에 의존적인 코드가 되는 것이 아쉬움
  • Deffered 대신 CompletableFuture 또는 Reactor 와 같은 다른 비동기 라이브러리 코드를 써야할 수도 있음
fun main(): Unit = runBlocking {
    val result1 = call1()
    val result2 = call2(result1)

    printWithThread(result2)
}

suspend fun call1(): Int {
    return CoroutineScope(Dispatchers.Default).async {
        Thread.sleep(1_000L)
        100
    }.await()
}

suspend fun call2(num: Int): Int{
    return  CompletableFuture.supplyAsync {
        Thread.sleep(1_000L)
        num * 2
    }.await()
}
  • suspend fun 으로 변경하여 이 안에서 어떤 비동기 라이브러리 구현체를 사용하건 해당 함수의 선택으로 남겨두는 것

🥨 함수 종류

coroutineScope
fun main(): Unit = runBlocking {
    printWithThread("START")
    printWithThread(calculateResult())
    printWithThread("END")
}

suspend fun calculateResult(): Int = coroutineScope {
    val num1 = async {
        delay(1_000L)
        10
    }

    val num2 = async {
        delay(1_000L)
        20
    }

    num1.await() + num2.await()
}

// 출력 결과
// [main @coroutine#1] START
// [main @coroutine#1] 30
// [main @coroutine#1] END
  • 새로운 코루틴을 만들지만, 주어진 함수 블록이 바로 실행됨
  • 만들어진 코루틴이 모두 완료된 이후, 반환
  • coroutineScope 로 만든 코루틴은 이전 코루틴의 자식 코루틴이 됨
withContext
fun main(): Unit = runBlocking {
    printWithThread("START")
    printWithThread(calculateResult())
    printWithThread("END")
}

suspend fun calculateResult(): Int = withContext(Dispatchers.Default) {
    val num1 = async {
        delay(1_000L)
        10
    }

    val num2 = async {
        delay(1_000L)
        20
    }

    num1.await() + num2.await()
}
  • coroutineScope 와 기본적으로 유사
    • 새로운 코루틴을 만들지만, 주어진 함수 블록이 바로 실행됨
    • 만들어진 코루틴이 모두 완료된 이후, 반환
  • context에 변화를 주는 기능이 추가적으로 존재 ⇒ Dispatcher를 바꿔 사용할 때 활용
withTimeout / withTimeoutOrNull
fun main(): Unit = runBlocking {
    val result: Int = withTimeout(1_000L){
        delay(1_500L)
        10 + 20
    }
    printWithThread(result)
}

// 출력 결과
// Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms

fun main(): Unit = runBlocking {
    val result: Int? = withTimeoutOrNull(1_000L){
        delay(1_500L)
        10 + 20
    }
    printWithThread(result)
}

// 출력 결과
// [main @coroutine#1] null
  • coroutineScope 와 기본적으로 유사
  • 단, 주어진 시간 안에 새로 생긴 코루틴이 완료되어야 함
    • withTimeout : TimeoutCancellationException 예외 발생
    • withTimeoutOrNull : null 반환

💡9강. 코루틴과 Continuation

🥨 suspending function

기본 예제 코드
class UserService {
    private val userProfileRepository = UserProfileRepository()
    private val userImageRepository = UserImageRepository()

    suspend fun findUser(userId: Long): UserDto{
        println("유저를 가져오겠습니다")
        val profile = userProfileRepository.findProfile(userId)
        println("이미지를 가져오겠습니다")
        val image = userImageRepository.findImage(profile)
        return UserDto(profile, image)
    }
}

data class UserDto(
    val profile: Profile,
    val image: Image
)

class UserProfileRepository{
    suspend fun findProfile(userId: Long): Profile {
        delay(100L)
        return Profile()
    }
}

class Profile

class UserImageRepository {
    suspend fun findImage(profile: Profile): Image{
        delay(100L)
        return Image()
    }
}

class Image
suspend 지점 경계 나누기
class UserService {
    private val userProfileRepository = UserProfileRepository()
    private val userImageRepository = UserImageRepository()

    suspend fun findUser(userId: Long): UserDto{
        // state machine의 약자, 라벨을 기준으로 상태를 관리
        val sm = object : Continuation{
            var label = 0 // 익명 클래스를 만들어 라벨을 갖게 만듦
        }

        when(sm.label){
            0 -> {
                // 0단계 - 초기 시작
                println("유저를 가져오겠습니다")
                val profile = userProfileRepository.findProfile(userId)
            }
            1 -> {
                // 1단계 - 1차 중단 후 재시작
                println("이미지를 가져오겠습니다")
                val image = userImageRepository.findImage(profile) // 에러 발생
            }
            2-> {
                // 2단계 - 2차 중단 후 재시작
                return UserDto(profile, image) // 에러 발생
            }
        }
    }
}
에러 수정
class UserService {
    private val userProfileRepository = UserProfileRepository()
    private val userImageRepository = UserImageRepository()

    suspend fun findUser(userId: Long): UserDto{
        // state machine의 약자, 라벨을 기준으로 상태를 관리
        val sm = object : Continuation{
            var label = 0 // 익명 클래스를 만들어 라벨을 갖게 만듦
            var profile: Profile? = null
            var image: Image? = null
        }

        when(sm.label){
            0 -> {
                // 0단계 - 초기 시작
                println("유저를 가져오겠습니다")
                sm.label = 1
                val profile = userProfileRepository.findProfile(userId)
                sm.profile = profile
            }
            1 -> {
                // 1단계 - 1차 중단 후 재시작
                println("이미지를 가져오겠습니다")
                sm.label = 2
                val image = userImageRepository.findImage(sm.profile!!)
                sm.image = image
            }
            2-> {
                // 2단계 - 2차 중단 후 재시작
                return UserDto(sm.profile!!, sm.image!!)
            }
        }
    }
}
라벨 호출
// 라벨을 갖고 있을 인터페이스, 점점 더 많이 기능이 추가될 예정
interface Continuation {
    suspend fun resumeWith(data: Any?)
}

class UserService {
    private val userProfileRepository = UserProfileRepository()
    private val userImageRepository = UserImageRepository()

    suspend fun findUser(userId: Long, continuation: Continuation): UserDto{
        // state machine의 약자, 라벨을 기준으로 상태를 관리
        val sm = object : Continuation{
            var userId = userId
            var label = 0 // 익명 클래스를 만들어 라벨을 갖게 만듦
            var profile: Profile? = null
            var image: Image? = null
            
            override suspend fun resumeWith(data: Any?) {
                findUser(this.userId, this)
            }
        }

        when(sm.label){
            0 -> {
                // 0단계 - 초기 시작
                println("유저를 가져오겠습니다")
                sm.label = 1
                val profile = userProfileRepository.findProfile(userId, sm)
                sm.profile = profile
            }
            1 -> {
                // 1단계 - 1차 중단 후 재시작
                println("이미지를 가져오겠습니다")
                sm.label = 2
                val image = userImageRepository.findImage(sm.profile!!, sm)
                sm.image = image
            }
            2-> {
                // 2단계 - 2차 중단 후 재시작
                return UserDto(sm.profile!!, sm.image!!)
            }
        }
    }
}

data class UserDto(
    val profile: Profile,
    val image: Image
)

class UserProfileRepository{
    suspend fun findProfile(userId: Long, continuation: Continuation): Profile {
        delay(100L)
        return Profile()
    }
}

class Profile

class UserImageRepository {
    suspend fun findImage(profile: Profile, continuation: Continuation): Image{
        delay(100L)
        return Image()
    }
}

class Image
최종코드
suspend fun main() {
    val service = UserService()
    println(service.findUser(1L, null))
}

// 라벨을 갖고 있을 인터페이스, 점점 더 많이 기능이 추가될 예정
interface Continuation {
    suspend fun resumeWith(data: Any?)
}

class UserService {
    private val userProfileRepository = UserProfileRepository()
    private val userImageRepository = UserImageRepository()

    private abstract class FindUserContinuation() : Continuation {
        var label = 0 // 익명 클래스를 만들어 라벨을 갖게 만듦
        var profile: Profile? = null
        var image: Image? = null
    }

    suspend fun findUser(userId: Long, continuation: Continuation?): UserDto{
        // state machine의 약자, 라벨을 기준으로 상태를 관리
        val sm = continuation as? FindUserContinuation ?: object : FindUserContinuation(){
            override suspend fun resumeWith(data: Any?) {
                when (super.label){
                    0 -> {
                        profile = data as Profile
                        label = 1
                    }
                    1 -> {
                        image = data as Image
                        label = 2
                    }
                }
                findUser(userId, this)
            }
        }

        when(sm.label){
            0 -> {
                // 0단계 - 초기 시작
                println("프로필을 가져오겠습니다")
                userProfileRepository.findProfile(userId, sm)
            }
            1 -> {
                // 1단계 - 1차 중단 후 재시작
                println("이미지를 가져오겠습니다")
                userImageRepository.findImage(sm.profile!!, sm)
            }
        }

        return UserDto(sm.profile!!, sm.image!!)
    }
}

data class UserDto(
    val profile: Profile,
    val image: Image
)

class UserProfileRepository{
    suspend fun findProfile(userId: Long, continuation: Continuation) {
        delay(100L)
        continuation.resumeWith(Profile())
    }
}

class Profile

class UserImageRepository {
    suspend fun findImage(profile: Profile, continuation: Continuation){
        delay(100L)
        continuation.resumeWith(Image())
    }
}

class Image
  • Continuation을 통해 최초 호출인지 아니면 callback 호출인지 구분 가능
  • Continuation의 구현 클래스에서 라벨과 전달할 데이터 등을 관리 가능
  • Continuation Passing Style(CPS)

💡코루틴은 내부적으로 Continuation을 매개변수마다 추가해서 callback처럼 사용하게끔 변경되는 것이라고 이해하면 충분

💡10강. 코루틴과 Continuation

🥨 코루틴 특징

  • callback hell 해결
  • Kotlin 언어 키워드가 아닌 라이브러리

⇒ 비동기 non-blocking이 필요하거나 동시성이 필요한 곳에 적극 활용

🥨 코틀린 활용

  • Client : 비동기 UI 구성
  • Server : 여러 API(I/O) 동시 호출
  • Webflux와 같은 프레임워크에서 사용 가능
  • 동시성 테스트에 적용 가능

.

이번 글에서는 코루틴 생성부터 취소와 예외처리까지 코루틴을 실제로 사용하기 위한 기본 방법들을 정리해봤습니다.

이번 강의 구간부터는 코루틴이 추상적인 개념이 아니라 실제 코드에서 어떻게 쓰이는지 조금씩 감이 잡히기 시작하더라구요!

다음 글에서는 코루틴의 구성 요소와 원리를 다룬 6~7강을 정리해보겠습니다.

끝!🦕

.


👍 참고


© 2022. Haeeul All rights reserved.

🐾해을의 개발자국🐾

Powered by Hydejack v9.1.5