[코틀린 동시성 프로그래밍] 2장 코루틴 인 액션 -1

Sdoubleu·2023년 1월 16일
0

코틀린 동시성

목록 보기
3/10
post-thumbnail

2장에서 다루는 주제

  • 코틀린과 코루틴을 사용하는 프로젝트를 위한 안드로이드 스튜디오 설정
  • 안드로이드의 UI 스레드
  • 코루틴을 사용하는 백그라운드 스레드에서 REST 호출
  • 코루틴 빌더인 async()와 launch()
  • 코루틴 디스패처 소개

코루틴 지원 추가하기

  1. Gradle Scripts -> build.gradle(Project: RssReader)
  2. ext.kotlin.version 밑에 다음과 같이 추가한다.
ext.coroutines_version = '1.3.6'
  1. 코루틴에 대한 의존성을 추가 -> build.gradle(Module: app)
dependencies {
	implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
    <...>
  1. 코루틴으로 앱을 컴파일하거나 실행할 때 나오는 여러 가지 경고를 없애려면
    -> module.gradle(Module: app)에 다음을 추가
kotlin {
	experimental {
    	coroutines "enable"
    }
}

코루틴이 더 이상 실험적인 기능으로 간주되지 않으면, 위 코드는 스니펫은 추가하지 않아도 된다.

🛠️스니펫에 대해 정보


안드로이드의 UI 스레드

앱에는 UI를 업데이트하고 사용자와의 상호작용을 리스닝하며, 메뉴를 클릭하는 것과 같은 사용자에 의해 생성된 이벤트 처리를 전담하는 스레드 존재
할 수 있는 최선의 방법으로 UI 스레드와 백그라운드 스레드를 확실하게 분리하기 위해서, UI 스레드의 기본적인 사항을 검토!

CallFromWrongThreadException

  • 안드로이드는 뷰 계층을 생성하지 않은 스레드가 관련 뷰를 업데이트하려고 할 때마다 CallFromWrongThreadException을 발생

  • 실제로 이 예외는 UI 스레드가 아닌 다른 스레드가 뷰를 업데이트할 때마다 발생

  • UI 스레드만이 뷰 계층을 생성할 수 있는 스레드이며 뷰를 항상 업데이트 할 수 있음

  • ⚡ UI를 업데이트하는 코드가 UI 스레드에서 실행되도록 보장하는 것이 중요

NetworkOnMainThreadException

  • 자바에서의 네트워크 동작은 기본적으로 블로킹
    ex) 웹 서비스를 호출하면 응답이 수신되거나 타임 아웃/오류가 발생하기 전까지 현재의 모든 스레드는 블로킹이 된다.

  • UI 스레드가 블로킹된다는 것은 애니메이션이나 기타 상호작용을 포함한 모든 UI가 멈추는 것을 의미 -> UI 스레드에서 네트워크 작업을 수행할 때마다 안드로이드는 중단

  • 이런 상황이 발생할 때마다 NetworkOnMainThreadException 예외가 발생
    -> 개발자는 백그라운드 스레드를 사용해서 사용자 경험 개선해야 함

백그라운드에서 요청하고, UI 스레드에서 업데이트할 것

두 가지글 합쳐서 서비스 호출을 구현하려면 백그라운드 스레드가 웹 서비스를 호출하고, 응답이 처리된 후에 UI 스레드에서 UI를 업데이트하도록 해야 함


스레드 생성

  • 코틀린은 스레드 생성 과정을 단순화해서 쉽고 간단하게 스레드 생성 가능

  • 단일 스레드만으로도 충분하지만, CPU/ 바운드와 I/O 바운드 작업을 모두 효율적으로 수행하기 위해 스레드 풀도 생성할 것

CoroutineDispatcher

  • 코틀린에서는 스레드와 스레드 풀을 쉽게 만들 수 있지만 직접 액세스하거나 제어하지 않는 다는 점을 숙지 !

  • 여기서는 CoroutineDispatcher을 만들어야 함
    -> 기본적으로 가용성, 부하, 설정을 기반으로 스레드 간에 코루틴을 분산하는 오케스트레이터

  • CoroutineDispatcher에 추가하는 모든 코루틴은 특정 스레드에서 실행
    -> 단 하나의 스레드만 갖는 CoroutinDispatcher를 확장한 ThreadPoolDispatcher를 생성

클래스 레벨에서 디스패처를 생성하기 위해 파일을 업데이트한다.
class MainActivity : AppCompatActivity() {
	val netDispatcher = newSingleThreadContext(name = "ServiceCall")
    override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

코드를 줄이기 위해 private, protected, open 등과 같은 접근 제어자를 적지 않음으로써 가독성을 높였으나 각 멤버들을 가능한 접근하기 어렵도록 만드는 것임을 명시해야 한다.

🛠️newSingleThreadContext, 접근 제어자에 대해


디스패처에 코루틴 붙이기

코루틴을 시작하는 두 가지 방법을 살펴볼 텐데, 결과와 에러를 처리하려면 둘 사이의 차이점을 알아야 함

async 코루틴 시작

  • 결과 처리를 위한 목적으로 코루틴을 시작했다면 async()를 사용

  • async()Deferred<T>를 반환, 디퍼트 코루틴 프레임워크에서 제공하는 취소 불가능한 넌블로킹 퓨처를 의미, T는 그 결과의 유형을 나타냄

  • async()를 사용할 때 결과를 처리하는 것을 잊어버리면 안됨!

fun main(args: Array<String>) = runBlocking {
	val task = GlobalScope.async {
    	doSomething()
	}
    task.join()
    println("Complete")
}


doSomeThing()은 단순히 예외를 던진다.
fun doSomeThing() {
	throw UnsupportedOperationException("Can't do")
}

🛠️CoroutineScope, GlobalScope의 차이점

↪ 예외를 통해 애플리케이션 실행이 멈추고 예외 스택이 출력되며
앱의 종료 코드는 0이 아닐 것이라고 생각할 수 있음

async() 블록 안에서 발생하는 예외는 그 결과를 첨부됨
-> isCancelledgetCancellationException() 메소드를 함께 사용해 안전하게 예외를 가져올 수 있음

task.join()
if (task.isCancelled) {
	val exception = task.getCancellationException()
    println("Error with message: ${exception.cause}")
    } else {
    println("Success")
    }
}

결과

예외를 전파하기 위해서 디퍼드에서 await()을 호출할 수 있음

fun main(args: Array<String>) = runBlocking {
	val task = GlobalScope.async {
    	doSomeThing()
    }
    task.await()
    println("Completed")
}

결과

  • await()를 호출해서 중단되는데 이 경우가 감싸지 않고 전파
    -> 감싸지 않은 디퍼드

join()으로 대기한 후 검증하고 어떤 오류를 처리하는 것과 await()를 직접 호출하는 방식의 주요 차이

  • join(): 예외를 전파하지 않고 처리
    -> 대기하고 iscancelledgetCancellationException()을 사용해 에러를 처리한 경우 성공을 의미하는 코드 0을 반환

  • await(): 단지 호출하는 것만으로 예외가 전파
    -> 실행 중 에러를 의미하는 코드 1을 반환

launch 코루틴 시작

  • 결과를 반환하지 않는 코루틴을 시작하려면 launch()를 사용해야 함

  • launch()는 연산이 실패한 경우에만 통보 받기를 원하는 파이어-앤-포겟 시나리오를 위해 설계, 필요할 때 취소할 수 있는 함수도 제공

fire-and-forget scenario:
이벤트나 메세지 기반 시스템에서 널리 활용되는 패턴
실행 후 결과에 대해서 신경 쓸 필요 없는 경우와 같은 시나리오를 의미

fun main(args: Array<String>) = runBlocking {
    val task = GlobalScope.launch {
        doSomeThing()
    }
    task.join()
    println("Completed")
}
fun doSomeThing() {
    throw UnsupportedOperationException("Can't do")
}

결과

↪ 예외가 스택에 출력되지만 실행이 중단 X, 앱은 main()의 실행을 완료

코루틴을 시작할 때 특정 디스패처 사용하기

지금까지 aync()와 **launch()로 코루틴을 만드는 방법을 살펴봤지만,
두 경우 모두 기본 디스패처를 사용

fun main(args: Array<String>) = runBlocking {
	val task = launch {
    	printlnCurrentThread()
    }
    task.join()
}

fun printlnCurrentThread() {
	println("Running in Thread [${Thread.currentThread().name}]")
}

↪ 표준 출력에 현재 스레드의 이름을 출력
코루틴이 DafaultDispatcher에서 실행됨을 알 수 있음
-> CommonPool과 같은 디스패처지만 향후에는 바뀔 수 있음을 기억

  • CoroutineDispatcher를 생성하기 위해 main()을 변경
  • 그 디스패처를 launch()로 전달하면 지정된 스레드에서 코루틴이 실행
fun main(args: Array<String>) = runBlocking {
    val dispatcher = newSingleThreadContext(name = "ServiceCall")
    val task = GlobalScope.launch(dispatcher) {
        doSomeThing()
    }
    task.join()
}

MainActivity에서도 똑같이 작업할 것

class MainActivity : AppCompatActivity() {
    val netDispatch = newSingleThreadContext(name = "ServiceCall")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        GlobalScope.launch(netDispatch) {  
            //TODO Call coroutine here
        }
    }
}

2편에 계속 ..

profile
개발자희망자

0개의 댓글