[Android] Coroutine(코루틴)

·2022년 6월 29일
0

💡 Thread

코루틴에 대해 설명하기 전에,
Android에서의 Thread에 대해 간단히 정리해보자.

Android에는 기본적으로 다음과 같은 두 가지의 스레드가 존재한다.

- Main Thread(UI Thread)
- Worker Thread(Background Thread)

이 중에서 Worker Thread는 여러 개가 존재할 수 있다.

Android는 왜 여러 개의 Thread를 사용하는가❓

: 예를 들어 우리가 카톡을 하며 영화를 다운 받는다고 가정했을 때, 한 개의 Thread로 동작한다면 영화를 다운 받는 중에는 카톡을 할 수 없게 된다.

이는 Thread가 한 가지의 작업밖에 처리할 수 없기 때문인데,
이때 필요한 것이 ❗멀티 스레딩❗이다.

→ Android는 왜 꼭 Main Thread에서만 UI 작업을 해야 하도록 설계했는가❓

: 여러 스레드가 같은 UI 자원에 접근해 동시에 서로 다른 수정을 요구하게 되면 수행해야 할 작업이 무엇인지 모호해진다. 이러한 이유로 Android에서는 UI 작업을 오직 Main Thread에서만 가능하도록 설계되었다.
따라서 Main Thread가 아닌 Worker Thread에서 UI를 변경하려고 하면, Exception이 발생한다.

또한, Main Thread가 일정 시간 어떤 Task에 붙잡혀 있으면 ANR(Application Not Responding) 이 발생하게 된다.
Main Thread는 하나만 존재하고 작업을 순차적으로 진행하기 때문에, 앞에서 30초짜리 작업을 한다고 가정하면 30초 동안 UI를 불러오지 못하고 30초 뒤에서야 불러오게 된다. 그리고 이렇게 되면 화면에 '응답 없음(ANR)'이라는 팝업이 뜨게 될 것이다.


💡 Coroutine(코루틴)

코루틴은 무엇인가❓

: 안드로이드 개발자 공식 홈페이지에 따르면, "코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴"이다.

코루틴을 왜 사용할까❓

: 우리는 안드로이드로 앱을 만들때 화면에 무엇인가를 표시하는 작업 말고, 내부적으로 조금 긴 시간 동안 무엇인가를 처리하는 작업을 만들어야 한다.

화면에 무엇인가를 표시하는 작업은 Main Thread에서,
내부적으로 동작하는 작업은 별도 스레드를 만들어서 처리해야 한다.

Main Thread에서 그러한 작업들을 처리하게 되면 Main Thread가 블로킹(Blocking) 되어 UI 작업을 처리할 수 없다.

결국에는 비동기적(Asynchronous) 으로 수행되는 코드를 작성해야 하고, 그 방법에는 여러 가지가 있다.

코루틴은 그 방법 중 하나이고, 현재로서 가장 간단하고 쉽게 사용할 수 있는 방법이다.

Asynchronous-Blocking 이란❓

: 비동기적(Asynchronous) 이라는 것은 어떤 작업을 요청했을 경우 그 작업이 종료될 때까지 기다리지 않으며, 다른 작업을 하고 있다가 요청했던 작업이 종료되면 해당 작업에 대한 추가 작업을 수행하는 방식이다.

반면, 동기적(Synchronous) 이라는 것은 어떤 작업이 요청되었을 때 그것이 끝날 때까지 기다린 후 다음 작업이 수행되는 방식이다.

→ 블로킹(Blocking) 된다는 뜻은 무엇일까❓

: 요청받는 함수가 작업을 모두 마치고 나서야 요청자에게 제어권을 넘겨주는 것을 말한다. 위에서 언급한 Main Thread에서 어떤 긴 작업의 함수를 호출했다면 제어권은 그 함수로 넘어간다. 더 이상 요청했던 함수에서 다른 작업이 실행될 수 없는 블로킹 상황이다.

반면에, 논블로킹(Non-blocking) 이란 요청받은 함수가 요청자에게 제어권을 바로 넘겨주어 다른 일을 처리할 수 있는 상황을 말한다.

그렇다면 우리가 앱을 만드는 데 필요한 구조는 Asynchronous-Blocking 일 것이다.  '비동기적으로 작업을 처리하면서 비동기 작업을 위해 블로킹 되지 않는 상황'. 이것은 더 나은 성능을 가진 앱을 만들기 위해서 필수적인 처리 방식이다.

기존의 스레드 방식

: OS에서는 멀티태스킹을 지원하고, CPU 같은 공용자원을 나누어서 작업을 처리한다. 이때 CPU는 작업 단위를 나누어서 처리하는데, 작업 단위는 프로세스이다. 우리는 프로세스 내에서의 작업 단위를 나누어야 한다. 이때 안드로이드에서 작업의 단위는 스레드이다. 스레드는 하나의 프로세스 내에서 여러 개가 생성될 수 있으며, 힙 메모리를 공유하고 각각의 스택 메모리를 가진다.
context-switch-between-threads.png

그러면 프로세스 즉 앱 내에서 비동기 처리를 위해 우리는 스레드를 생성해야 한다. 안드로이드는 기본적으로 Main Thread에서 실행된다. UI를 그리고 뷰와 관련된 콜백들이 Main Thread에서 수행된다. 따라서 긴 작업은 별도의 Worker Thread를 만들어야 한다.

하지만 Thread를 만드는 것 그리고 상태를 관리하고, 작업 교환을 하는 것은 매우 까다롭다. Thread를 다루기 위해 우리는 핸들러나 루퍼, AsycTasks를 이용하기도 하고, 일반적으로 전통적인 콜백 구조를 통해 이런 작업을 처리했었다. 이런 방식은 유연한 구조가 되지 못했고 가독성도 매우 떨어졌다.

→ 따라서 메모리 관리와 취소가 가능한 코루틴이 많은 기여를 했다고 한다. 여기서 코루틴은 Thread의 대안이 아니라 기존의 Thread를 더 잘게 쪼개어 사용하기 위한 개념이다. 하나의 Thread가 다수의 코루틴을 수행할 수 있기 때문에 더 이상 작업의 수만큼 Thread를 양산하며 메모리를 소비할 필요가 없다.

no-context-switch-between-coroutines.png


코루틴은 스레드인가❓

: 아니다. 코루틴은 일시 정지 가능한 함수 (코드블럭) 으로서 기본적으로 하나의 스레드를 가진다. 스레드의 기능을 대신하기 위함이 아니라 스레드를 포함한 비동기 처리를 위한 라이브러리이자 일종의 패턴이다.

다만, 코루틴을 경량화된 스레드라고 표현하기도 하는데, 이는 코루틴의 목적이 스레드와 같지만, 성능이 더 좋고 가볍기 때문이다.
코루틴은 많은 수의 스레드를 만들지 않고도 작업을 처리할 수 있으면서 스레드 간의 전환이 아니기 때문에 Context-switching이 필요하지 않고, 메모리 사용성도 효율적이다.

코루틴의 장점은 무엇인가❓

  • 경량 : 코루틴을 실행 중인 스레드를 차단(Blocking)하지 않는 정지(Suspending)를 지원하므로 단일 스레드에서 많은 코루틴을 실행할 수 있다.

    기존의 스레드는 생성, 해제(GC), Context-switching 시 CPU와 메모리를 소모하기 때문에 많은 수의 스레드를 갖기 어렵다.
    그러나 코루틴은 스레드가 아닌 서브루틴을 일시 중단(suspend)하는 방식이기 때문에 Context-switching에 비용이 들지 않는다.

    → 하나의 스레드에 코루틴이 여러 개 존재할 수 있는데, 실행 중이던 하나의 코루틴이 정지(suspend)되면, 현재 스레드에서 재개(resume)할 다른 코루틴을 찾는다. 근데 이제 이 다른 코루틴을 같은 스레드 내에서 찾는 것이다. 따라서 스레드를 바꾸는 게 아니라 switch 비용으로 인한 오버헤드가 없다.

    📍 Context-switching
    : 스레드 실행 또는 종료 시 스레드의 상태를 저장하고 복구하는 프로세스
    = 두 개의 스레드에서 작업 시 CPU가 매번 스레드를 점유했다가 놓아주는 것을 반복할 때 비용 발생

    📍 Blocking과 Suspending의 차이
    Block(막다, 차단하다) vs Suspend(유예하다, 연기하다)

    (참고: https://www.charlezz.com/?p=45962)

    Blocking : 함수B를 수행하기 위해서는 함수A가 먼저 끝나야 한다.
    Blocking
    Suspending: 함수A가 시작 된 후 정지(suspended)될 수 있으며, 함수B가 수행되고 끝난 뒤 함수A가 재개(resume)될 수 있다. 즉 이 스레드는 함수A에 의해 블락되지 않는다.
    Suspending

  • 메모리 누수 감소 : scope와 같은 개념(구조화된 동시 실행)을 사용하여 범위 내에서 작업을 실행하여 메모리 누수를 방지한다.
  • 처리 도중 취소 가능 : 실행 중인 코루틴 계층 구조를 통해 자동으로 취소가 전달된다. → 다른 처리를 중단(blocking)시키지 않고 중지(suspend)하는 형태로 가볍게 동작
  • Jetpack 통합 : Android KTX와 같은 Jetpack 라이브러리에 코루틴을 지원한다.

코루틴은 어떻게 동작하는가❓

: 코루틴은 스레드에서 동작하겠지만 여러 작업 단위로 나누어 번갈아 가며 실행될 수 있다. 여러 작업이 각각의 스레드를 가질 필요는 없다. 많은 수의 스레드를 만들지 않고도 작업을 처리할 수 있는 것이다. 스레드 간의 전환이 아니기 때문에 OS 레벨의 Context-switching도 필요하지 않다. 또한 메모리 사용성에 있어서도 효율적이다.

📍 코루틴의 사용 흐름
Context로 Scope를 만들고, Builder를 이용하여 그 Scope 안에서 실행!
= CoroutineContext를 이용하여 Coroutine이 실행될 CoroutineScope를 만들고, 만들어준 CoroutineScope에서 CoroutineBuilder를 이용하여 { } 안의 코드를 Coroutine으로 실행시킨다.
(참고: https://oasisfores.com/kotlin-android-coroutine/
https://velog.io/@ditt/%EC%BD%94%EB%A3%A8%ED%8B%B4
https://jminie.tistory.com/165)

  • Coroutine Context
    : 코루틴을 어떻게 처리할 것인지에 대한 여러 가지 정보의 집합

  • Coroutine Scope

    • 제어범위와 실행범위를 지정할 수 있다.
    • 스코프 내에서 생성된 코루틴을 주시하며 실행을 취소하거나, 실패 시 예외를 처리할 수 있다.
    • 스코프의 Job을 취소하면 해당 스코프 안에서 시작된 코루틴(자식 job)은 모두 취소된다.
      단, 스코프 안에 다른 스코프를 생성하면, 분리가 되기때문에 외부 스코프의 코루틴을 취소해도 해당 별도의 스코프는 중지되지 않는다.

  • Coroutine Builder
    : 설정해준 Context와 Scope를 통해 Coroutine을 실행시켜주는 ‘함수’,
    CoroutineScope의 확장함수로써, {} 내부의 코드를 Coroutine으로 실행시켜주는 역할을 한다.

    • 기본적인 Coroutine builder 종류
      1. launch : non-blocking
      2. async : non-blocking
      3. runBlocking : blocking
      * runBlocking{}은 {} 내부의 Coroutine들이 모두 완료될 때까지 현재 Thread를 Blocking하여, Coroutine의 장점인 ‘일시 중단’을 못 쓰게 되어버리므로 사용이 권장되지 않는다.
      특히 UI 작업을 관장하는 Main Thread에서 runBlocking을 사용하여 Thread를 장시간 점유하고 있을 경우 ANR이 발생할 수 있다.


💡 Thread와 Coroutine 비교

코루틴이 하나의 실행-종료되어야 하는 일(Job)이라면, 스레드는 그 일이 실행되는 곳이다.

  • 코루틴과 스레드는 모두 동시성을 보장하기 위한 기술이다.

  • 하나의 스레드에 여러 개의 코루틴이 동시에 실행될 수 있다.

  • 스레드는 여러 스레드가 있다면 병렬로 실행된다.

  • 코루틴의 경우 스레드풀을 사용하는 dispatcher로 코루틴들을 실행하면 스레드처럼 병렬 프로그래밍을 할 수 있다.

    📍 동시성(Concurrency)과 병렬성(Parallelism)

    동시성 : 시분할, 각 task들을 조금씩 나누어서 실행
    병렬성 : 병렬수행, 각 task들을 동시에 수행


요약

: 코루틴은
1. 협력형 멀티 태스킹
2. 동시성 프로그래밍 지원
3. 비동기 처리를 쉽게 도와줌


✔️ 참고 사이트
https://velog.io/@plz_no_anr/Android-Coroutine
https://starrytheo.tistory.com/34
https://kotlinworld.com/139
https://coding-food-court.tistory.com/153
https://www.charlezz.com/?p=45962
https://oasisfores.com/kotlin-android-coroutine/
https://velog.io/@ditt/%EC%BD%94%EB%A3%A8%ED%8B%B4
https://jminie.tistory.com/165

profile
깡통 채우기

0개의 댓글