ext.coroutines_version = '1.3.6'
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" <...>
kotlin { experimental { coroutines "enable" } }
코루틴이 더 이상 실험적인 기능으로 간주되지 않으면, 위 코드는 스니펫은 추가하지 않아도 된다.
🛠️스니펫에 대해 정보
앱에는 UI를 업데이트하고 사용자와의 상호작용을 리스닝하며, 메뉴를 클릭하는 것과 같은 사용자에 의해 생성된 이벤트 처리를 전담하는 스레드 존재
할 수 있는 최선의 방법으로 UI 스레드와 백그라운드 스레드를 확실하게 분리하기 위해서, UI 스레드의 기본적인 사항을 검토!
안드로이드는 뷰 계층을 생성하지 않은 스레드가 관련 뷰를 업데이트하려고 할 때마다 CallFromWrongThreadException을 발생
실제로 이 예외는 UI 스레드가 아닌 다른 스레드가 뷰를 업데이트할 때마다 발생
UI 스레드만이 뷰 계층을 생성할 수 있는 스레드이며 뷰를 항상 업데이트 할 수 있음
⚡ UI를 업데이트하는 코드가 UI 스레드에서 실행되도록 보장하는 것이 중요
자바에서의 네트워크 동작은 기본적으로 블로킹
ex) 웹 서비스를 호출하면 응답이 수신되거나 타임 아웃/오류가 발생하기 전까지 현재의 모든 스레드는 블로킹이 된다.
UI 스레드가 블로킹된다는 것은 애니메이션이나 기타 상호작용을 포함한 모든 UI가 멈추는 것을 의미 -> UI 스레드에서 네트워크 작업을 수행할 때마다 안드로이드는 중단
이런 상황이 발생할 때마다 NetworkOnMainThreadException 예외가 발생
-> 개발자는 백그라운드 스레드를 사용해서 사용자 경험 개선해야 함
두 가지글 합쳐서 서비스 호출을 구현하려면 백그라운드 스레드가 웹 서비스를 호출하고, 응답이 처리된 후에 UI 스레드에서 UI를 업데이트하도록 해야 함
코틀린은 스레드 생성 과정을 단순화해서 쉽고 간단하게 스레드 생성 가능
단일 스레드만으로도 충분하지만, CPU/ 바운드와 I/O 바운드 작업을 모두 효율적으로 수행하기 위해 스레드 풀도 생성할 것
코틀린에서는 스레드와 스레드 풀을 쉽게 만들 수 있지만 직접 액세스하거나 제어하지 않는 다는 점을 숙지 !
여기서는 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()는 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") }
↪ 예외를 통해 애플리케이션 실행이 멈추고 예외 스택이 출력되며
앱의 종료 코드는 0이 아닐 것이라고 생각할 수 있음
async() 블록 안에서 발생하는 예외는 그 결과를 첨부됨
-> isCancelled와 getCancellationException() 메소드를 함께 사용해 안전하게 예외를 가져올 수 있음
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") }
join(): 예외를 전파하지 않고 처리
-> 대기하고 iscancelled와 getCancellationException()을 사용해 에러를 처리한 경우 성공을 의미하는 코드 0을 반환
await(): 단지 호출하는 것만으로 예외가 전파
-> 실행 중 에러를 의미하는 코드 1을 반환
결과를 반환하지 않는 코루틴을 시작하려면 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과 같은 디스패처지만 향후에는 바뀔 수 있음을 기억
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 } } }