[안드로이드스튜디오_문화][Coroutines]

기말 지하기포·2023년 10월 14일
0

#유튜버 "새차원" 형님 강의 영상 보고 공부함 (감사함당)
1. 유튜브 링크 -> https://xn--youtube-ix1a.com/@user-te8xl2tj4i
2. 블로그 링크 -> https://blog.naver.com/cenodim

#"Coroutines"란?

-이전에 자신의 실행이 마지막으로 중단되었던 지점 다음의 장소에서 실행을 재개한다.
> 코드의 로직이 실행되는 동안 중단 되었다가 다시 재개 될 수 있어서 코루틴은 입구점과 출구점이 여러개인 로직으로 볼 수 있다. [중단 : 출구점 , 재개 : 입구점]
> 코루틴을 일반화 시키면 (입구점과 출구점을 한개로 지정하는 것) 일반 routine()이 된다.
>
부모 Coroutine과 자식 Coroutine은 별도의 Thread에서 돌아간다.

#"글링이가 추천해주는 Coroutines 사용하면 좋은 때"

-1. 비동기 코드를 간소화 해줘서 비동기 코드 작성 시 추천해줌.
-2. UIThread가 막히거나 앱의 비정상 종료를 유발 시킬 수 있는 오랜 시간이 걸리는 작업을 관리 할 필요가 있을 때 추천해줌.
+ 아래는 공식문서 사진임 + 링크 첨부

>Android Developer 주소 : https://developer.android.com/kotlin/coroutines?hl=ko


-3. 비동기적인 콜백 코드 지옥들을 일반적인 순차적인 코드로 간단하게 작업하고 싶을 때
-4. suspend 함수를 사용해서 비동기 로직을 순차적 코드 로직으로 간단하게 작업하고 싶을 때

Android Codelabs 주소 : https://developer.android.com/codelabs/kotlin-coroutines#0

**결론** Coroutine은 비동기작업을 간단하게 작업 할 수 있게 해줌
**구글 IO 영상(Coroutine) 링크** https://www.youtube.com/watch?v=BOHK_w09pVA

#"Coroutine Scope 종류"

  1. Coroutine Scope
  2. Global Scope

#"Coroutine Builder(:Corutine을 시작하고 관리함.)의 종류"

  • 중요 : CoroutineBuilder는 기본적으로 Coroutine을 만드는 것은 똑같지만 반환값의 형태는 달라.
  1. launch : Job 객체 (Coroutine이라고 생각해_) 반환
  • 결과를 반환하지 않는 Coroutine을 원할 때

  1. runBlocking : [T] 제너릭(block 되고 실행된 로직의 결과값) 반환
  • runBlocking{} 블락 내부의 코드는 해당 블록이 완료되야지 다음 코드가 실행된다. 그래서 다른 코드를 실행하고 싶으면 suspend 함수를 사용하여서 코루틴의 제어권을 다른 코루틴에게 넘겨주면 되고 주로 메인함수의 진입점이나 동기적으로 수행해야하는 작업을 하는 Coroutine을 원할 때

  1. async : Deffered(Job 상속받음 : 따라서 Job과 관련돈 메서드 사용 가능) 반환
  • 결과를 반환하는 Coroutine을 원할 때

#"suspend function(일시중단 명령을 내리는 함수)의 종류 : 다른 Coroutine으로 제어권을 넘김." : Coroutine을 일시 중단 시키고 새로운 작업을 시작하는데 이 새로운 작업 또한 Coroutine의 일부이나, suspend 함수는 활용조건에 아래에 써있는 것처럼 조건이 붙기 때문에 일반적인 Coroutine Builder라고 볼 수 없다.

  • 중요 : suspend function은 Coroutines 내부에서 또는 suspend function 내부에서만 사용이 가능하다.
  • suspend function은 현재 Coroutine의 실행이 중지되고, 다른 Coroutine이 실행될 수 있도록 허용한다. 이때 새롭게 실행되는 다른 Coroutine의 실행 방식은 suspend function에 따라 다름.
  • 따라서 비동기작업을 실행하고 싶을 때 suspend function을 사용하면 된다.
  1. delay() :
    -delay()를 호출한 Coroutine 블록의 로직을 중단시키고, 지정된 시간 동안 대기하게 함.단, 로직을 중단시키지만 다른 Coroutine을 호출하는 것은 아님.

  2. join() :
    -join()을 포함하게 되어버린 Coroutine의 로직을 중단시키고, join()을 호출한 Coroutine이 종료될 때까지 대기함.(하나의 Coroutine이 다른 Coroutine의 완료를 기다릴 때 사용함.)
    -join()을 호출한 Coroutine이 종료되면, join()을 포함하게 되어버린 Coroutine의 로직이 다시 실행 됨.
    -join()을 호출한 Coroutine의 코드진행을 지켜줘

  3. yield() :
    -yield()를 포함하게 되어버린 Coroutine의 로직을 중단시키고, 코드 로직 상 올바른 Coroutine에게 Coroutine의 제어권을 넘겨준다.
  4. withContext() : Exception 발생 X
    -Coroutine의 동작방식을 바꾼다. :
  5. withTimeout() : Exception 발생 O
    -Coroutine을 생성할 때 이 시간이 지나면 종료되도록 한다. :

#"Job 객체와 관련된 메서드"

  1. join()
  2. cancel() : 반드시 Coroutine이 Cancellation에 협조적이어야 한다.
  • 방법1_명시적으로 그리고 주기적으로 suspend 함수(yield()를 추천=>exception 처리도 쌉가능) 호출 => Coroutine이 Cancellation되면, Coroutine 내부의 suspend 함수가 재개되면서(Coroutine이 실행되려고 해) 그 위치에서 JobCancellationException이 발생한다. 그 위치에서 Resources를 해제하면 된다.(Resources를 사용했었 다면)
  • 방법2_isActive(확장 Property임 + 기본값이 true로 설정되어 있음) => cancel() 함수 요청이 들어오면 isActive 상태가 false 로 변경됨. + exception을 던지지 않음(yield()와의 차이점)

#"Deffered 객체와 관련된 메서드"

  1. Job을 상속받았으므로 Job과 관련된 함수 사용 가능
  2. await() : Deffered의 결과를 수신 받는다.

#"Coroutine의 Context"
-Coroutine의 작동방법?을 정의함.

  1. NonCancellable : Coroutine이 절대 취소 안 되기를 원할 때
  2. Dispatchers.Default : Coroutine이 기본 백그라운드 작업 할 때
  3. Dispatchers.IO : Coroutine이 네트워크 또는 DB 작업 할 때
  4. Dispatchers.Main : Coroutine이 화면 UI 작업 할 때
  5. ...

#"코루틴 공부"

  1. GlobalScope : GlobalScope는 CoroutineScope의 한 종류임. + 반환값은 Job객체
    - 앱이 시작하고 끝날 때 까지 계속 실행함.
    - 다른 CoroutineScope들은 필요한 범위 내에서 생성하고 생성한 범위에서만 유효하고, CoroutineScope의 생명주기는 해당 범위에 따라 달라지지만, GlobalScope는 Singletone이여서 사용범위와 생명주기를 신경쓰지 않아도 됨.(어디에서나 접근 가능함)
    - 대신 앱이 작동하고 끝날 때 까지 실행되서 메모리 누수(안좋다는거같음)의 위험이 있어서 특정 범위 또는 특정 생명주기에서만 사용하겠다고 무턱대고 쓰면 메모리 누수(ㅋ있어보이는 단어 ㅋ 안좋다는 뜻같음 일단은ㅋㅋ)가 난다.

    - Coroutine은 Coroutine Scope의 블록 내부에서만 실행이 된다.
    - launch 함수를 호출하면 내부적으로 Coroutine을 하나 만들어서 반환을 해준다.
    - launch 함수를 CoroutineBuilder라고 생각하면 된다.
    - launch 함수를 호출하기 위해서는 CoroutineScope가 필요한다.
    - CoroutineScope의 종류는 다양하며 밑으로 내리면 더 있음.
    - delay : suspend function이야 => 일시중단 되는 함수 + Coroutine Scope 내부에서만 실행 또는 다른 suspend function에서만 실행이 된다. 그러나 좋은 접근 방법이 아니야!!!!!!!!!!!!!!!!!!!!그래서 명시적으로 job이라는 것을 만들어서 기다린다.
  1. runBlocking : CoroutineBuilder임. 반환값은 [T] => T는 제네릭 타입 매개변수로, runBlocking 함수를 호출할 때 block 블록 내부에서 계산된 결과의 타입을 의미함.
    - runBlocking에서 생성된 Coroutine 블록 내부의 로직들이 종료될 때까지 MainThread가 대기한다. 따라서 UI 변경 작업중에 무턱대고 쓰면 ㅋ ANR 빠방!!!

    - 아래 사진이 더 깔끔한 runBlocking을 사용한 코드임. 왜냐하면 어차피 runBlocking에서 반환된 coroutine.joinBlocking()이라는 Coroutine 블록 내부의 로직이 종료되기 전까지는 Main Thread가 종료되지 않기 때문임.
  1. join() : delay의 단점을 극복한다 => delay는 좋은 접근 방법이 아니야 위에서 사용한 코드에서 깔끔한 runBlokcing 코드라고 작성하였지만, runBlockig 블록만 본다면 깔끔한 코드이지만, 실제로 내부 로직을 살펴보면 delay의 시간이 조금만 변경되어도 원하는 로직이 구성되지 않는다. 일례로 처음 delay(1000L)을 delay(3000L)로 변경해보면 GlobalScope가 실행되기 전에 마지막 delay(2000L)로 인하여 프로그램이 종료되서 "Hello,"만 출력된다. 아례 사진 참고 ㅋ 논리갑 황건희 ㅋ

    - 위와 같은 단점 때문에 join이 필요함 ㅋ
    - join이라는 suspend 함수는 join을 호출한 Job 객체 또는 Coroutine이 실행되고 종료 될 때 까지 기다림.
    - 아래는 예시 코드임 ㅋ
  1. Structured concurrency
    > 위 코드처럼 join()을 사용하지 않으면 Coroutine이 실행되지 않고 Hello 만 출력되고 프로그램이 종료된다. 왜냐면 각각의 job1 , job2에 delay가 걸려있어서 그래서 주석 처리를 풀고 실행하면 아래와 우리가 원하는 결과값이 출력된다.
    > 그러나 코드를 작성하면 할수록 job들이 무수히 많아질텐데 일일이 job들을 관리하기는 힘들어서 Structured concurrency가 나온거야.
    > 위의 코드들을 비교 분석해보면 TopLevelCoroutine(GlobalScope.launch에서 생성된)와 runBlockcing이 구조적으로 아무 관련이 없어서 runBlocking 블록이 실행되는 동안 내부의 GlobalScope들이 끝나던지 말던지 아무 상관이 없어(if join() 들이 없으면).
    > 그렇다면 만약에 GlobalScope.launch에서 생성된 Job객체(Coroutine이라고 생각)가 구조적으로 runBlockin이랑 관련이 있어서 서로 기다려 줄 수 있었다면? 에서 출발해서 나온 결과물이 바로 Structured concurrency임.
    > 방법은 아주 간단함. GlobalScope에서 launch를 하지 말고 this를 사용해서 runBlocking에서 들어온 CoroutineScope에서 launch를 하면 된다. 근데 사실 어차피 람다식에서 this를 받아오기 때문에 this를 빼도 됨. 아래 코드 봐봐.
    > 위에서 보는거처럼 runBloking이 만든 Coroutine과 childCoroutine들이 구조적으로 엮여있으므로 parentCoroutine(runBlocking에서 만들어진)이 childCoroutine들의 로직이 끝날때까지 기다릴 수 밖에 없음. 겁나 효율적임. join() 안 써도 되서 코드가 간단해짐ㅋ.
  1. suspend function : 코드 로직의 일시 중단을 의미하는 키워드 (함수 선언 시 fun 앞에 붙혀서 사용한다.
    - Coroutine 내부 또는 suspend function 내부에서만 사용가능하다.
    > 위 코드를 보면 delay는 suspend function이기 때문에 일반 함수 블록 내부에 들어가는 로직에 사용 될 수 없다. 왜냐하면 suspend function은 오직 Coroutine 또는 suspend function 내부에서만 사용 될 수 있기 때문이다. 따라서 아래와 같은 키워드를 붙혀서 함수를 선언해야 한다. 궁금하지?ㅋ 정답은 바로 suspend임.ㅋ
    > 이처럼 suspend 키워드를 사용해야지 delay() 함수를 사용 할 수 있다.
  1. Coroutines ARE light-weight
    - Thread()를 사용하는 것 보다 Coroutines를 사용 하는 것이 더 메모리 상의 안정성이 있어.
    - Coroutine이 Thread 보다 구조적으로 가볍다.
  1. Global coroutines are like daemon threads
    - coroutine이 살아있다고 해서 process가 계속해서 동작하는 것은 아니다. == 즉, Process가 살아있을 때에만 coroutine이 동작을 한다.
    > 위 코드에서 보이는 것처럼 process가 종료되면 coroutine도 종료가 된다.
  1. .cancel() : 진행중인 Coroutine을 중단시키고 종료함. : 단, 단순히 cancel() 함수를 호출했다고 진행중인 Coroutine이 종료되는 것은 아님.
    **중요** 반드시 종료시키고자 하는 Coroutine이 cancel() 작업에 협조적인 코드로 작성 되어져야 한다.
    > job.cancle()을 활용해서 job 변수에 저장된 Job 객체(Coroutine)의 실행을 멈춘다.
    > 주석처리된 job.join()은 써도 되고 안써도 된다.
    > 여기서 job.cancel()이 먹힐 수 있는 이유는 job 변수에 저장된 Corutine Block 내부에 delay(500L)가 있기 때문인데, 이는 Main Thread에서 받아온 Coroutine의 제어권을 다시 Main Thread에 넘기는 역할을 한다. 그래서 runBlocking에서 생성된 Coroutine이 Coroutine 제어권을 얻어서 job.cancel()이 실행 될 수 있게 하는 것이다. 아래 사진은 delay(500L)이 없을 때 출력된 값이므로 참고 바람.ㅋ
    >9번에서 더 깔끔하게 Coroutine을 종료시키는 방법을 알아보자.
  1. yield() : suspend 함수로서 , Coroutine의 제어권을 다른 Coroutine에게 주고 , 현재 실행되는 Coroutine을 중단시킨다.

    위 코드는 yield() 함수와 repeat을 함께 사용한 경우인데 보통 자주 안쓰는데 이유는 다음과 같음.
    > repeat()이 들어가있는 Coroutine이 실행될 때마다 yield()를 호출하게 되는데, 이때마다 runBlocking이 생성한 Coroutine에 제어권을 넘겨주는데, 1m/s가 지나기 전까지 200회의 반복이 일어난다. delay(1L)이 지나야지 runBlocking이 생성한 Coroutine이 제어권을 넘겨 받을 수 있어 그래서 200번의 반복이 끝나고 나서야 제어권을 넘겨받는다.
    > 그동안은 그래서 200번의 반복이 일어나는데 이럴빠엔 그냥 delay()를 repeat() 블록에 넣는 것이 효율적일듯. (이거 찾는데 겁나 짱구돌림ㅋ)
    > 결론 : 웬만하면 repeat()이랑 yield()랑 엮이게 하지말자, 보통 while() 블록 내부에서 if문이랑 함께 사용하는게 좋을듯 ㅋ.
    1m/s 시간에 println()이 200번 출력된다는 정보 얻음. + job.cancelAndJoin()이 진행될 때는 22번의 println()이 출력되었으므로 job.cancelAndjoin()의 컴파일 시간은 0.11m/s임 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
  1. yield() 함수 예시
    > cancel() + yield() +
    > cancel() + yield() + exception
  1. isActive 활용해서 Coroutine을 .cancel() 시키기.
    > isActive를 활용하여 Coroutine을 종료시킨다면, JobCancellationException이라는 Exception이 발생하지 않는다. (yield()이 경우는 발생한다.)
  1. Coroutine을 suspend function을 활용해서 Cancellation 할 때 Resources를 해제 할 수 있는 방법
  • Resources를 닫아주는 위치는 finally Block이다. :
  • Coroutine을 .cancle()하게 되면 suspend function이 재개되면서 exception을 발생시키는데, 그 위치에서 Resources를 해제하면 된다. 거기가 finally Block이야.
  • finally Block에서 Resources를 해제하는 이유는 Coroutine이 Cancellation의 여부의 관계없이 Coroutine의 종료되는 순간에 항시 Resources를 해제해야지 메모리 누수와 같은 문제를 방지 할 수 있기 때문이다.
  • 아래는 finally Block에서 Resource 해제를 진행하는 예시 코드이다. job.cancel()과 job.cancelAndJoin()과 비교해서 보도록.


    -1. .cancel()을 사용해서 Coroutine을 종료시켰을 때
    > runBlocking{}에서 생성된 Corutine의 마지막 println()이 실행되고 난 후 Resources 해제가 이루어진 것으로 보아 cancel()만 사용하게 되면, MainThread의 잔여 작업이 launch에서 생성된 Corutine의 잔여작업보다 우선순위가 높다고 볼수 있음 + 그러니까는, .cancel()을 사용하면 Resources해제가 Corutine 해제 후 실행된 Corutine들의 잔여작업의 우선순위가 아니라는 거임.

    2. .cancelAndJoin()을 사용해서 Coroutine을 종료시켰을 때> Resources 해제가 이루어지고 runBlocking{}의 블록에서 생성된 Coroutine의 마지막 println()문이 실행된 것으로 보아 cancelAndJoin()을 사용하게 되면, launch{}에서 생성된 Coroutine의 블록의 잔여작업 우선순위가 runBlocking{}에서 생성된 Corutine의 잔여작업 우선순위보다 높다는 것을 알 수 있다. 그러니까는, .cancelAndJoin()을 사용하면 Resources해제가 Corutine 해제 후 실행된 Corutine들의 잔여작업의 우선순위라는 거임.
  1. withContext()
  • Parameter로 주어진 Coroutine Context를 사용하여 lamda bloc을 실행하고, 람다 블록의 실행이 완료될 때까지 suspended된다. 즉, suspend function이라는 거임.
  • 어떤 Coroutine Context를 주냐에 따라서 Coroutine이 실행될 방식이 바뀌므로, Coroutine의 실행방식을 바꾸고 싶을 때 withContext를 사용함.
  • 아래는 예시 코드임.
    > 위 코드에서 보는 것처럼 delay(1000L)이 실행되고 나면, Coroutine이 대기상태에 들어가면서 제어권이 runBlocking에서 생성된 Coroutine으로 넘어가서 println("job : And I've just delayed for 1 sec because I'm non-cancellabe")이 출력되지 않는다. : 그런데 withContext를 통해서 launch{} 블록으로부터 생성된 Coroutine의 Context를 NonCancellable로 변경하면 어떻게 될까?
    -아래는 withContext(NonCancellable)을 사용한 코드임.
    > 역시 예상대로 Coroutine의 작업상태가 바뀌어서 println("job : And I've just delayed for 1 sec because I'm non-cancellabe")이 출력된다.
  1. withTimeout()
  • withTimeout()을 사용하면 Coroutine을 실행 할 때 특정 시간이 지나면 Coroutine을 종료시킬 수 있도록 할 수 있다. 이때 특정 시간동안 실행된 결과의 조금도 보여주지 않는다. 겁나게 업격해 근데 만약에 시간이 초과되었을 때 조금의 결과라도 보고싶으면 try catch 문을 사용하면 된다. 그러니까 변수에 저장도 해야해.
  • 항상 TimeoutCancellationException을 throw하고 코루틴을 종료함.
    >안드로이드 스튜디오에서 확인한 withTimeout 이라는 suspend function이 만들어진 부분인데 위 코들를 해석하면, 아래와 같은 의미이므로 참고 바람.
    >Parameter로 주어진 시간을 초과해서 Coroutine이 실행 될 수 없고 초과하면 TimeoutCancellationExeption을 던지고 Coroutine을 종료시킴.
    >Parameter로 주어진 시간을 초과히지 않고, Coroutine이 종료되면, 임의의 T가 반환된다.
    >Parameter로 주어진 시간이 0보다 작거나 같으면 TimeoutCancellationException을 던지고 "Time out immediately"도 같이 던져진다. 즉, Parameter로 0보다 큰 숫자를 달라는 거임.
    withTimeout()은 코드 블록의 결과를 반환하므로 withTimeout()의 반환값은 항상 코드 블록의 결과가 되어서 따로 변수에 반환 값을 저장하지 않아도 된다. 그 이유는 코드에서
  • Exception을 발생시키며, Exception을 발새 시키지 않는 withTimeourOrNull()이라는 것이 있는데 아래를 보면 됨.
  1. withTimoutOrNull()
  • withTimeoutOrNull()을 사용하면 withTimeout()과 달리 Exception을 발생시키지 않으면서 Coroutine을 실행 할 때 특정 시간이 지나면 Coroutine을 종료 시킬 수 있다. 그리고 변수에 저장한채로 실행하기 때문에 특정 시간동안 실행된 결과를 볼 수 있기는 하다. 왜냐하면 그 이유는 아래에 나와있음.
    >위 코드를 해석해보면은 Exception이 발생한 코루틴이 내 코루틴과 같다면 null을 반환하도록 되어있음("==="이라는 참조 비교를 수행하는 연산자를 사용했기 때문에 가능한거야)
    >withTimeoutOrNull의 경우에는 반환값을 변수에 받아서 사용해야 한다. 왜냐하면 시간이 초과되더라고 어떻게든 결과 값을 보여주도록 설계 되었기 때문임. 설계 된 곳의 코드는 setupTimeout이 작성된 곳에 timeoutCoroutine이라는 코드 블락이 들어가면서 이 코드 블락에서의 결곽 값을 반환하기 때문임. 하 겁나 빡세네진짜
  1. suspend function을 조합해서 Coroutine을 작성하기.
  • Coroutine에서는 일반 코드처럼 다다닥 작성해도 작성한 코드가 비동기 코드 일지라도, 지가 알아서 순차적으로 다다닥 해준다.
  • 아래는 예시 코드임.
  1. async
  • Coroutine에서는 비동기 코드일지라도 순차적으로 작성하는 것이 기본 base이므로 아예 비동기적으로 실행하고 싶으면 항상 명시적으로 Coroutine Block을 만들어야 한다.
  • async : Coroutine Builder이며 반환값은 Deffered객체이다. 또한 Deffered는 Job을 상속 받았기 때문에 async이 반환값인 Deffered 또한 Job과 관련된 메서드들을 사용 할 수 있다.
    > 위 코드에서 aync + await()을 사용해서 순차적으로 진행되는 코드를 완전히 비동기로 바꾸어서 결과값을 받아오고 있다.
profile
포기하지 말기

0개의 댓글