본문 바로가기
Language/Kotiln

[Kotlin] 코틀린 코루틴 개념과 basics - 새차원 강의1,2요약

by 카프리썬 2022. 1. 31.
728x90

지금까지 코틀린 기본세팅을 진행했다.

2022.01.29 - [Kotlin] 코틀린 기본개념 - 코틀린이란? 도대체 왜 쓰는가? 특징!

2022.01.28 - [Kotlin] 코틀린 환경설정 - Intellj에서 코틀린 프로젝트 생성하기

2022.01.29 - [Kotiln] Intellj에서 코틀린 코딩컨벤션 Kotlin Style Guide 적용하기

2022.01.30 - [Kotiln] 코틀린 실행하기 - Scratch File (스크래치 파일) 만들기

 

그리고 간단한 기본문법도 살펴봤다. 

2022.01.29 - [Kotiln] 코틀린 기본문법1 (변수선언/함수선언/자료형/반복문/조건문)

2022.01.30 - [Kotiln] 코틀린 기본문법2 (NULL처리 : lateinit/lazy/!!/?/?.)

2022.01.30 - [Kotiln] 코틀린 기본문법3 (클래스와 컬렉션: List/Map/Set)

 

이번엔 조금 깊게 심화문법으로 몇가지를 더 살펴볼 예정이다.

이제 진짜 본격적으로 메인인 코루틴을 알아보려고 한다.

그래서 코틀린 공식문서도 참고하면서 새차원님의 코루틴 강의를 들은 내용을 요약해보려고 한다. 

 

반응형

 


1. Why Coroutines?

현재 플레이스토어에 등록된 앱의 약 70% 이상의 앱이 코틀린으로 개발되어 있다.

안드로이드 스튜디오 공식 문서에서도 권장사항으로 coroutine 을 이용하여 비동기 처리를 하라고 명시되어 있다.

코루틴(coroutines)이란?

  • co : with, togher 
  • routine : 규칙적으로 하는일의 통상적인 순서와 방법
  • coroutine : co + routine의 합성어로 협동루틴

즉, 코루틴이란 함께 동작하면서 규칙이 있는 일의 순서를 의미한다.

 

결국 코루틴은 실행이 마지막으로 중단되었던 지점의 바로 다음장소에서 실행을 재개한다고 볼 수 있다. 

보통 일반적인 함수는 중단되는 개념없이 어떤 루틴이 있으면 계속 실행하고, 끝나면 빠져나오는 방식

반면에 코루틴은 진입을 하고 어느정도 수행하고 난 다음에 중단될수 있으며 다시 재개될 수 있다. (입구점, 출구점이 여러개)

 

코루틴을 어디에 쓰이는가?

  • Coroutines simplify async code by replcaing callbacks
  • 구글에서는 코루틴을 비동기처리에서 쓰면 코드를 간단하게 작성할 수 있다고 한다. 

코루틴이 메인스레드가 blocking되는 부분에 대해서도 도움을 주고. 비동기 처리코드를 순차적인 코드로 만들수 있게 해준다. 

 

비동기처리란?

비동기실행은 동시에 여러가지 작업을 처리할 수 있다는 의미이다. >> 이곳을보고 이해했다. 

예를 들어 우리가 흔히 멀티가 안된다고 하면서 한번에 하나씩 작업을 해야한다고 하는걸 "동기"라고 하고,

멀티가 잘된다면서 밥먹으면서 유튜브보면서 말하는거까지 여러가지 작업을 동시에 할 수 있는걸 "비동기"라고 한다. 

 

코드에선 비동기처리가 필요할때가 있다. 

예를 들어 메인스레드에서는 네트워크 작업을 처리하지 못하도록 막혀있다. 

메인스레드에서 화면 UI를 바꾸는 작업도 진행해야하는데 네트워크 작업이 오래걸리면 그냥 앱을 나가버릴것이기 때문이다. 

그래서 메인스레드가 아닌 별개의 스레드에서 네트워크 작업을 처리하도록 되어 있다. 

 

코루틴에서는 아래와 같이 3가지 스레드를 선언하고, 동기는 lanuch, 비동기는 async로 표현한다.

  • Main: 메인 스레드. 화면 UI 작업 등을 하는 곳
  • IO: 네트워크, DB 등 백그라운드에서 필요한 작업을 하는 곳
  • Default: 정렬이나 무거운 계산 작업 등을 하는 곳

그래서 예를 들어 코루틴에서는 아래와 같이 코드를 작성할 수 있다. 

CoroutineScope(Dispatchers.쓰레드종류).동기/비동기 {
 
}

//example : IO스레드를 비동기처리한다. 
CoroutineScope(Dispatchers.IO).async {
 
}

 

728x90

 

2. Basic

앞으로 관련 예제들을 각각 코틀린 파일로 만들것이다.

코루틴 기본세팅

기본적으로 코루틴을 사용하기 위해서 아래와 같은 dependecy를 추가해야한다. build.gradle.kt 파일에 추가한다. 

implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2")

그리고 코틀린 파일에서 아래와 같은 import를 해줘야 코틀린 구문을 사용할 수 있다. 

import kotlinx.coroutines.*

 

코루틴 Hello World : launch

이 코드를 바탕으로 코루틴을 간단하게 이해할 수 있다. 아래코드는 코틀린에서 기본제공하는 예제이다. 

import kotlinx.coroutines.*


fun main(args: Array<String>){

    GlobalScope.launch {  // 새로운 코루틴을 백그라운드에서 실행시킨다.
        delay(1000L) 	  // 1초 딜레이
        println("World!") // "World!" 콘솔 프린트
    }
    println("Hello,") 	// "Hello," 콘솔 프린트
    Thread.sleep(2000L) // 메인 스레드 2초간 정지
}

//출력
GlobalScope-1초딜레이
Hello,
메인스레드-2초딜레이
1초 후
World!
메인스레드-2초딜레이
=> 메인스레드와 코루틴이 거의 동시에 실행

 

1. launch는 coroutine builder이다.

내부적으로 stand alone coroutine을 반환하는 역할을 한다.

2. launch를 하려면 coroutine scope가 필요하다.

globalScope가 미리준비된 coroutine scope 중에 하나인데 프로그램 전체가 전역인 scope를 의미한다.

3.본질적으로 코루틴은 light-weight 스레드를 의미한다.

globalScope.launch{}를 thread{}로 바꿔도 같은 결과를 가지게 된다. 

처음에 메인스레드로 hello가 출력이 되고, 다름스레드가 호출이 되서 world가 찍히게 되는 것이다. 

 

코루틴 Hello World : Non-blocking

첫번째 코드에 있던 sleep을 delay로 통일시켰다. 

fun main() {
    GlobalScope.launch {
        delay(1000L) // non-blocking delay for 1 second
        println("World!") // print after delay
    }
    println("Hello") // main thread continues while coroutine is delayed
    runBlocking {
    	delay(2000L) // block main thread for 2 seconds to keep JVM alive
    }
}

//출력
GlobalScope-1초딜레이
Hello,
메인스레드-2초딜레이
1초 후
World!
메인스레드-2초딜레이

 

1.delay는 noblocking함수, sleep은 blocking함수이다. 

논블로킹함수인 delay는 suspend function로 직접 호출이 되진 못하고, 명시적으로 블로킹 하는 코루틴 안에 있어한다. 

2.runblocking은 coroutine builder이다.

블로킹 코루틴으로 그 안에서 자신을 호출한 스레드를 블로킹한다. 

 

조금더 관용적인 표현은 GlobalScope 안에 내용이 완료되기 전까진

메인스레드가 종료되지 않고 리턴되지 않길 원하기 때문에 메인함수전체를 noblocking 코루틴으로 감싼다.

fun main() = runBlocking { // runBlocking 으로 전체를 감싸 안았다.
    GlobalScope.launch {  
        delay(1000L) 	  
        println("World!") 
    }
    println("Hello,") 	
    delay(2000L) 
}

 

코루틴 Hello World :  Waiting For Job

delay는 좋은방식이 아니다. 그래서 delay 대신 명시적으로 job을 만들어서 기다려볼 수 있다. 

지금 코투린 하나를 launch하고, 메인스레드에서 2초동안 기다리고 있다가 코루틴 스레드를 시작한다. 

fun main() = runBlocking { // runBlocking 으로 전체를 감싸 안았다.
    GlobalScope.launch {  
        delay(3000L) 	  
        println("World!") 
    }
    println("Hello,") 	
    delay(2000L) 
}

그런데 만약 위의 코드처럼 runblocking을 2초뒤에 끝난다고 했는데, globalscope.launch가 3초뒤에 끝난다고 한다면? 

그럼 globalscope.launch에 있는 World!는 출력도 해보지도 못하고 메인 runblocking만 진행하고 끝나서 Hello만 찍힌다. 

그래서 이럴경우 job을 하나 만들어서 기다리게 한다. 

fun main() = runBlocking { // this: CoroutineScope
    val job = GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    
    println("Hello")
    job.join() //이게 없으면 아에 job이 실행이 안됌!!
}

launch할 경우 job이 반환되는데, 이 job 객체에다가 join을 한다.

그럼 job이 완료될때까지 기다리게 된다. 

 

코루틴 Hello World : structured concurrency

만약 코루틴으로 만들어야할 job이 여러개가 있다면? 아래처럼 일일이 하나씩 다 만들어야하는가?

그리고 join이 없으면 globalscope에서 코루틴이 끝나든 말든 상관없이 메인함수는 코투린을 기다리지 않고 매정하게 끝나버린다.

그래서 항상 여러가지 job을 만들고, 다 join을 일일이 해줘야하는 문제가 발생한다. 

fun main() = runBlocking { // this: CoroutineScope
    val job1 = GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    
    val job2 = GlobalScope.launch {
        delay(1000L)
        println("World!222")
    }
    
    println("Hello")
    job1.join() 
    job2.join()
}

이런 문제를 structured concurrency로 구성하면 해결할 수 있다.

structured concurrency란 

위에 코드처럼 job1, job2 이렇게 top-level 코루틴을 만들지 말고,

runblocking에 의해 생성된 코루틴의 child로 코루틴을 만들어주는것이다. 

 

그래서 부모 코루틴과 child코루틴이라는 구조적인 관계가 생겨

부모코루틴은 join없이 여러 child 코루틴이 완료될때까지 기다려줄 수 있다. 

사실 launch앞에 this 가 생략되어 있다고 보면된다. 각각의 child 코루틴을 생성할 수 있다. 

fun main() = runBlocking {

    launch {
        delay(1000L) 
        println("World!")
    }
    
    launch {
        delay(1000L) 
        println("World!22")
    }
    
    println("Hello")
}

//출력
Hello
World!
World!22

 

코루틴 Hello World : suspend function

코루틴 안에서는 supspend 함수만 호출가능하다.

코루틴 Lauch를 일반함수를 가지고 할 경우, 그 일반함수 앞에 suspend라는 키워드를 붙여서 선언해야한다.

 그럼 왼쪽에 중단될 수 있다는 표시가 나온다!! 이게 바로 코루틴의 특징, 중단되었다가 다시 실행을 재개할 수 있다는걸 나타낸다.

 

Coroutines ARE light-weight

아래는 *를 10만개정도 반복적으로 찍는 코드이다.

이걸 코루틴으로 적용했을때랑 스레드로 적용했을때랑 속도차이가 느껴진다. 

그만큼 코루틴이 가볍다

//코루틴
fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}

//스레드
fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        Thread {
            Thread.sleep(5000L)
            print(".")
        }
    }
}

 

Global coroutines are like daemon threads

코루틴은 데몬스레드와 비슷하다.

코루틴이 계속 실행이 되고 있다고 해서 프로세스가 계속 유지되는게 아니다.

프로세스가 끝나면 코루틴도 끝난다. 

 

 

2. 추가 Suspend <-> Resume 느껴보기

코루틴의 핵심개념은 실행이 중지(suspend)되고 그 지점의 바로 다음장소에서 실행을 재개(resume)할 수 있다는 것이다.

예를 들어 delay를 통해 중지되는 시점을 임의로 만들었다. (왼쪽에 화살표 모양으로 중지되는 시점을 알려준다)

 

이걸 아래의 코드를 디버깅모드로 돌려보면서 어떤 스레드에서, 어떤 코루틴에 의해 출력이 되었는지 확인해볼 수 있다. 

  • 현재 스레드 출력 : println 오버라이딩
  • vm option 추가 : -Dkotlinx.coroutines.debug

그럼 각 print가 어떤 스레드의 어떤 코루틴으로부터 출력이 됐는지 디버깅결과를 확인할수 있다.

그럼 모두다 main스레드에서 실행이 되었고, 코루틴 3개에서 각각 실행된걸 볼 수 있다. 

첫번째 코루틴은 runBlocking에 의해서 만들어졌고,

두번째 세번쨰 코루틴은 child코루틴으로 coroutine A, coroutine B를 의미한다.  

 

첫번째 코루틴은 Coroutine Outer 를 출력한다. 

두번째 코루틴은 첫번째 launch에 의해 만들어졌고, repeat로 5번 반복적으로 수행하려고 했는데, 한번 수행하고 delay가 걸렸다. (중지)그래서 그 다음 세번째 코루틴은 그다음 launch에 의해 만들어졌고, delay가 없어서 repeat로 5번 반복적으로 수행한다. 

그러고 이제 세번째 코루틴이 다 끝나니까 아까전에 중지한 두번째 코루틴으로 돌아가서 resume 하려고 남은 repeat을 다시 수행한다. 

 

만약에 coroutine A, coroutine B를 반복적으로 하나씩 수행하려고 하려면 아래와 같이 delay를 각자 걸면 된다.



출처: https://impactyoung.tistory.com/entry/동영상-보며-코루틴동영상-보며-코루틴-요약하기 [기본지식 다음엔 연습만이 살길!]

 

 

반응형