개발 공부 기록하기/01. JAVA & Kotlin

코루틴이란 무엇인가? (루틴과 코루틴)

lannstark 2023. 8. 30. 15:55

이번 시간에는 ‘코루틴’을 간단히 사용해 보면서 코루틴이 무엇인지~ 감을 잡아보도록 하자

도대체 코루틴이란 무엇일까? 코루틴을 영어로 살펴보면 co-routine 으로 여기서 접미어 co 는 ‘협력하는’ 이라는 의미가 있다. 뒤에 있는 routine 은 컴퓨터 공학에서 이야기하는 루틴으로 간단히 ‘함수’라고 생각해도 좋다.

그렇다면 co-routine 은 협력하는 루틴, 협력하는 함수라는 의미이다.

잠깐, 우리가 생각하는 그냥 루틴(함수)도 서로 호출과 반환을 주고받으며 협력을 한다.

그런데 협력하는 루틴이라니 도대체 그냥 루틴과 협력하는 루틴은 어떤 차이가 있길래 ‘협력하는’ 이라는 의미가 있을까?

다음 코드를 살펴보자.

fun main() {
  println("START")
  newRoutine()
  println("END")
}

fun newRoutine() {
  val num1 = 1
  val num2 = 2
  println("${num1 + num2}")
}

굉장히 직관적인 이 코드는 2개의 루틴이 있다. main 루틴과 new 루틴이다. 또한 이 코드를 실행하면 쉽게 예상할 수 있는 것처럼 다음과 같은 결과물이 나온다.

START
3
END

이런 결과물이 나오는 과정은 이렇다.

  1. main 루틴이 START를 출력한 이후 new 루틴을 호출한다.
  2. new 루틴은 1과 2를 계산해 3을 출력한다
  3. 그 이후, new 루틴은 종료되고 main 루틴으로 돌아온다.
  4. main 루틴은 END 를 출력하고 종료된다.

이 과정을 그림으로 나타내면 다음과 같다.

또한 이때, new 루틴이 종료된 이후에는 new 루틴에서 사용했던 num1num2 에는 다시 접근할 수 없게 되고, 따라서 메모리에서도 정보가 사라질 것이다.

여기까지 정리해 보면

  • 루틴은 진입하는 곳이 한 곳이고
  • 루틴이 종료되면 그 루틴에서 사용했던 정보가 초기화

된다고 생각할 수 있다.

자 이제 루틴을 사용하는 코드를 살펴보았으니, 이번에는 협력하는 루틴, 코루틴을 살펴보자. 코루틴을 사용하려면 의존성을 한 가지 추가해 주어야 한다.

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2")
}

처음 보는 코드도 많을 텐데 우선 타이핑 해보고 각 함수의 의미를 살펴보자.

fun main(): Unit = runBlocking {
  println("START")
  launch {
    newRoutine()
  }
  yield()  
  println("END")
}

suspend fun newRoutine() {
  val num1 = 1
  val num2 = 2
  yield()
  println("${num1 + num2}")
}

가장 먼저 처음 보는 함수는 runBlocking 이다. runBlocking 함수는 일반 루틴 세계와 코루틴 세계를 연결하는 함수이다. 이 함수 자체로 새로운 코루틴을 만들게 되고, runBlocking 에 넣어준 람다가 새로운 코루틴 안에 들어가게 된다.

다음으로 launch 라는 함수도 보인다. launch 라는 함수 역시 새로운 코루틴을 만드는 함수이다. 주로 반환 값이 없는 코루틴을 만드는데 사용된다.

즉, 우리는 runBlocking { }launch { } 를 사용해 2개의 코루틴을 만든 것이다.

다음으로 yield() 라는 함수가 보인다. yield 라는 단어는 양보하다라는 의미가 있는데, 이 의미에 걸맞게 **yield() 라는 함수는 지금 코루틴의 실행을 잠시 멈추고 다른 코루틴이 실행되도록 양보**한다.

마지막으로 suspend fun 이라는 키워드가 보인다. 코틀린에서 일반적인 함수는 fun 으로 만드는 것과 다르게 suspend 라는 키워드가 붙었는데 **suspend 라는 키워드가 붙으면 다른 suspend fun 을 호출하는 특수 능력을 갖게 된다.** 우리가 사용한 yield()가 바로 suspend fun 이기 때문에 함수 newRoutinesuspend 를 붙여 주었다.

자 이렇게 새로운 기능들의 의미를 모두 살펴보았다. 이제 두 개의 코루틴을 사용하는 코드를 실행시켜보자. 실행 결과는 다음과 같다.

START
END
3

앗..! 일반-루틴을 코-루틴으로 변경했을 뿐인데 실행 결과가 달라졌다! 3 출력이 END 뒤에 나온 것이다! 혹시 양보하는 기능인 yield() 때문에 그럴까?! 2개의 yield() 를 지우더라도 여전히 동일하게 위의 출력이 나온다.

출력이 나오는 과정을 살펴보자.

  1. main 코루틴이 runBlocking 에 의해 시작되고 START가 출력된다.
  2. launch 에 의해 새로운 코루틴이 생긴다. 하지만, newRoutine의 실행은 바로 일어나지 않는다.
  3. main 코루틴 안에 있는 yield()가 되면 main 코루틴은 new 코루틴에게 실행을 양보한다. 따라서 launch 가 만든 새로운 코루틴이 실행되고, newRoutine 함수가 실행된다.
  4. newRoutine 함수는 다시 yield()를 호출하고 main 코루틴으로 되돌아온다.
  5. main 루틴은 END 를 출력하고 종료된다.
  6. 아직 newRoutine 함수가 끝나지 않았으니 newRoutine 함수로 되돌아가 3 이 출력되고 프로그램이 종료된다.

일반 루틴 세계에 비해 복잡하고 직관적이지 않다.. 😅 이 과정을 그림으로 나타내면 다음과 같다.

그렇다! 루틴과 코루틴의 가장 큰 차이는 중단재개이다. 루틴은 한 번 시작되면 종료될 때까지 멈추지 않지만, 코루틴은 상황에 따라 잠시 중단이 되었다가 다시 시작되기도 한다. 때문에 완전히 종료되기 전까지는 newRoutine 함수 안에 있는 num1 num2 변수가 메모리에서 제거되지도 않는다.

추가적으로 만약 출력이 일어날 때 어떤 스레드에서 출력이 실행되는지 확인하려면, 다음과 같은 유틸성 함수를 만들 수도 있다.

fun printWithThread(str: Any) {
  println("[${Thread.currentThread().name}] $str")
}

또한, IntelliJ IDE에서 vm option으로 -Dkotlinx.coroutines.debug 를 주게 되면 어떤 코루틴에서 출력이 일어났는지 확인할 수도 있다.

이 두 옵션을 사용하면, 출력 결과가 다음과 같이 변경된다.

[main @coroutine#1] START
[main @coroutine#1] END
[main @coroutine#2] 3

매우 좋다~! 😊 이번 시간에는 루틴과 코루틴의 차이에 대해 살펴보았다. 우리가 항상 직관적으로 사용했던 루틴과 다르게, 코루틴은 코드가 중간에서 멈추었다가 다시 시작할 수 있다는 특징이 핵심이다. 그런데 아직 코루틴을 완벽하게 파악한 것은 아니다.

또 코루틴과 자주 비교되는 존재가 있는데 바로 ‘스레드’이다. 다음 시간에는 코루틴과 스레드가 어떤 차이를 갖는지 살펴보도록 하자. 👍

 

코루틴에 대해 더 알아보고 싶다면 아래 내용을 참고해주세요! 😊

내용 출처 : https://inf.run/dLNp

 

2시간으로 끝내는 코루틴 - 인프런 | 강의

비동기 프로그래밍의 필수 라이브러리 코루틴! 코루틴의 개념, 사용법, 그리고 내부 원리까지 한 번에 얻어가세요!, 코틀린 비동기 프로그래밍 필수템! 코루틴, 2시간에 개념부터 실습까지 ⏰ [

www.inflearn.com