[코틀린 동시성 프로그래밍] 3장 라이프 사이클과 에러 핸들링

Sdoubleu·2023년 1월 16일
2

코틀린 동시성

목록 보기
5/10
post-thumbnail

3장에서 다루는 주제

  • Job과 그 사용사례
  • Job과 Deferred의 라이프 사이클
  • Deferred의 사용사례
  • Job의 각 상태별 예상되는 사항
  • Job의 현재 상태를 산출하는 방법
  • 예외 처리 방법

잡과 디퍼드

비동기 함수를 두 그룹으로 나눠볼 수 있음

  • 결과가 없는 비동기 함수:
    일반적인 시나리오로는 로그에 기록하고 분석 데이터를 전송하는 것과 같은 백그라운드 작업을 들 수 있음 완료 여부를 모니터링할 수 있지만 결과를 갖지 않는 백그라운드 작업이 이런 유형에 속함

  • 결과를 반환하는 비동기 함수:
    예를 들어 비동기 함수가 웹 서비스에서 정보를 가져올 때 거의 대부분 해당 함수를 사용해 정보를 반환하고자 할 것

두 가지중 어떤 경우이건 해당 작업에 접근하고 예외가 발생하면 그에 대응하거나, 해당 작업이 더 이상 필요하지 않을 때 취소함

잡(Job)

  • 잡은 파이어-앤-포겟 작업
    -> 한 번 시작된 작업은 예외가 발생하지 않는 한 대기하지 ❌
  • 코루틴 빌더인 launch()를 사용해 Job을 생성하는 방법이 가장 일반적
fun main(args:Array<String>) = runBlocking {
	val job = GlobalScope.launch {
    	// Do background task here
	}
}


다음과 같이 Job() 팩토리 함수를 사용할 수도 있다.
fun main(args:Array<String>) = runBlocking {
	val job = Job()
}


Job은 인터페이스로, launch()와 Job()은 모두 JobSupport의 구현체를 반환한다.
앞으로 보게 될 텐데 JobSupport는 잡을 확장한 인터페이스인 Job.Deferred의 여러 구현체의 기반이다.

예외 처리

  • 기본적으로 잡 내부에서 발생하는 예외는 잡을 생성한 곳까지 전파
  • Job이 완료되기를 기다리지 않아도 발생
fun main(args:Array<String>) = runBlocking {
	val job = GlobalScope.launch {
    		TODO("Not Implemented")
	}
    delay(500)
    /*delay()를 사용해 충분한 시간 동안 앱을 실행하게 해서 예외가 발생하게 했다.
    잡이 완료 될 때까지 대기하지 않더라도 예외가 전파된다는 것을 보여주기 위해
    의도적으로 join()을 사용하지 않았다.
    */
}

↪ 현재 스레드의 포착되지 않은 예외 처리기에 예외가 전파

라이프 사이클


기본적으로 Job은 생성되는 즉시 시작된다.
이것은 Job이 launch()로 생성되거나 Job()으로 생성될 때 발생한다.
Job을 생성할 때 시작하지 않게 하는 것도 가능하다.

다이어그램의 다섯 가지 상태에 대해

  • New(생성): 존재하지만 아직 실행되지 않는 잡
  • Active(활성): 실행 중인 잡. 일시 중단된 잡도 활성으로 간주
  • Completed(완료됨): 잡이 더 이상 실행되지 않는 경우
  • Canceling(취소 중): 실행 중인 잡에서 cancel()이 호출되면 취소가 완료될 때까지 시간이 걸리기도 함. 이것은 활성과 취소의 중간 상태
  • Cancelled(취소 됨): 취소로 인해 실행이 완료된 잡. 취소된 잡도 완료로 간주될 수 있음

생성

  • 잡은 기본적으로 launch()나 Job()을 사용해 생성될 때 자동으로 생성됨
  • 잡을 생성할 때 자동으로 시작되지 않게 하려면 CoroutineStart.LAZY를 사용
fun main(args:Array<String>) = runBlocking {
	GlobalScope.launch(start = CoroutineStart.LAZY) {
    	TODO("Not implemented yet!")
	}
    delay(500)
}

↪ 코드를 실행하면 오류가 출력되지 ❌
작업은 생성됐지만 시작된 적이 없으므로 예외가 발생 ❌

활성

  • 생성상태에 있는 잡은 다양한 방법으로 시작할 수 있지만 일반적으로 start()join() 을 호출해서 실행
    -> 둘의 차이점:
    start(): 잡이 완료될 때까지 기다리지 않고 잡을 시작
    join(): 잡이 완료될 때까지 실행을 일시 중단

start() 예시

fun main(args:Array<String>) {
	val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
    	delay(3000)
	}
    job.start()
}

↪ job.start()가 호출될 때 실행을 일시 중단하지 않으므로 앱이 job이 완료되는 것을 기다리지 않고 실행을 끝냄


start()는 실행을 일시 중단하지 않으므로 일시 중단 함수나 코루틴에서 호출할 필요가 없다. 앱의 어느 부분에서도 호출할 수 있다.

join() 예시

fun main(args:Array<String>) {
	val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
    	delay(3000)
	}
    job.join()
}

↪ join()을 사용하면 앱이 job을 완료할 때까지 대기

⚡중요⚡
join()은 실행을 일시 중단할 수 있으므로 코루틴 또는 일시 중단 함수에서 호출해야 한다. 이를 위해 runBlocking()이 사용되고 있음에 유의

  • 시작된 모든 잡은 활성 상태이며 실행이 완료되거나 취소가 요청될 때까지 활성 상태

취소 중

  • 취소 요청을 받은 활성 잡은 취소 중이라고 하는 스테이징 상태로 들어갈 수 있음
  • 잡에 실행을 취소하도록 요청하려면 cancel() 함수를 호출

cancel() 예시

fun main(args:Array<String>) {
	val job = GlobalScope.launch {
    // Do Some work here
    	delay(5000)
	}
    delay(2000)
    job.cancel()
}

↪ 잡 실행은 2초 후에 취소

  • cancel()에는 선택적 매개변수인 cause가 있음
  • 예외가 취소의 원인일 때는 원인을 같이 제공해 주면 나중에 찾아볼 수 있음
fun main(args:Array<String>) {
	val job = GlobalScope.launch {
    // Do Some work here
    	delay(5000)
	}
    delay(2000)
    // cancel with a cause
    job.cancel(cause = Exception("Timeout!")
    // kotlinx.coroutines 1.0.0-RC1 에서 
    // job.cancel(cause)는 deprecated 됐다.
}
  • cancelAndJoin() 함수도 존재
    -> 실행을 취소할 뿐 아니라 취소가 완료될 때까지 현재 코루틴을 일시 중단

취소됨

  • 취소 또는 처리되지 않은 예외로 인해 실행이 종료된 잡은 취소됨으로 간주
  • 잡이 취소되면, getCancellationException() 함수를 통해 취소에 대한 정보를 얻을 수 있음
    -> CancellationException을 반환
fun main(args:Array<String>) {
	val job = GlobalScope.launch {
    // Do Some work here
    	delay(5000)
	}
    delay(2000)
    // cancel 
    job.cancel(cause = CancellationException("Tired of waiting"))

    val cancellation = job.getCancelltaionException()
    println(cancellation.message)
}

↪ 취소된 잡과 예외로 인해 실패한 잡을 구별하기 위해 다음과 같이
CoroutineExceptionHandler를 설정해 취소 작업을 처리하는 것이 좋음👍

fun main(args:Array<String>) = runBlockin {
	val exceptionHandler = CoroutineExceptionHandler {
    	_: CoroutineContext, throwble: Throwble ->
        println("Job cancelled due to ${throwable.message}")
    }

    GlobalScope.launch(exceptionHandler) {
    		TODO("Not implemented yet!")
	}
    delay(2000)
}



다음과 같이 invokeOnCompletion()을 사용할 수도 있다.
fun main(args:Array<String>) = runBlockin {
	GlobalScope.launch {
    	TODO("Not implemented yet!")
	}.invodeOnCompletion { cause ->
    	cause?.let {
        	println("Job cancelled due to ${it.message}")
		}
	}
    delay(2000)
}

완료됨

  • 실행이 중지된 잡은 완료됨으로 간주
  • 실행이 정상적으로 종료됐거나 취소됐는지 또는 예외 때문에 종료됐는지 여부에 관계없이 적용
    -> 이러한 이유로 취소된 항목은 완료된 항목의 하위 항목으로 간주

잡의 현재 상태

  • 잡에는 상태가 많아서 외부에서 현재 상태를 파악하는 방법이 필요

잡은 세 가지 속성을 가짐

  • isAcitive: 잡이 활성 상태인지 여부. 잡이 일시 중지인 경우 true 반환

  • isCompleted: 잡이 실행을 완료했는지 여부

  • isCancelled: 잡 취소 여부. 취소가 요청되면 즉시 true 반환

상태(State)isActiveisCompletedisCancelled
생성됨(Created)falsefalsefalse
활성(Active)truefalsefalse
취소 중(Cancelling)falsefalsetrue
취소됨(Cancelled)falsetruetrue
완료됨(Completed)falsetruefalse

잡을 설명하는 문서에는 완료중(completing)이라고 하는 내부 상태가 있다.
이 상태는 내부 상태이며, 시그니처는 활성 상태와 유사하다는 점을 고려할 때 개별 상태로는 다루지 않는다.


디퍼드

  • 디퍼드(Deferred, 지연)는 결과를 갖는 비동기 작업을 수행하기 위해 잡을 확장

  • 기본적인 컨셉은 연산이 객체를 반환, 객체는 비동기 작업이 완료될 때까지 비어 있다는 것

  • 디퍼드와 그 상태의 라이프 사이클은 잡과 비슷

디퍼드를 만들려면 async를 사용할 수 있음

fun main(args:Array<String>) = runBlockin {
	val headlinesTask = GlobalScope.async {
    	getHeadlines()
	}
    headlinesTask.await()
}


또는 CompletableDeferred의 생성자를 사용할 수 있다.
val articlesTask = CompletableDeferred<List<Article>>()

예외 처리

  • 순수한 잡과 달리 디퍼드는 처리되지 않은 예외를 자동으로 전파하지 ❌
    -> 디퍼드의 결과를 대기할 것으로 예상하기 때문에 이런 방식을 사용
    -> 실행이 성공했는지 확인하는 것은 사용자의 몫
fun main(args:Array<String>) = runBlockin {
	val deferred = GlobalScope.async {
    	TODO("Not implemented yet!")
	}
    // Wait for it to fail
    delay(2000)
}

앞의 예제는 지연된 실패를 갖지만 예외를 전파 X

예외를 쉽게 전파할 수 있는 방법

fun main(args:Array<String>) = runBlockin {
	val deferred = GlobalScope.async {
    	TODO("Not implemented yet!")
	}
    // Let it fail
    deferred.await()
}
// 앞의 코드와 다르게 이 코드는 예외를 전파하고 앱을 중단시킬 것이다.

디퍼드의 실행이 코드 흐름의 필수적인 부분임을 나타내는 것이기 때문에
await()을 호출하는 방식으로 설계

try-catch 블록을 사용해 예외를 처리

fun main(args:Array<String>) = runBlockin {
	val deferred = GlobalScope.async {
    	TODO("Not implemented yet!")
	}

	try {
		deferred.await()
	} catch (throwable: Throwable) {
    	println("Deferred cancelled due to ${throwable.message}")
    }
}

이 장의 나머지 부분에서는 잡과 디퍼드를 모두 잡으로 표기
잡이 베이스 인터페이스이기 때문에 별도로 명시하지 않는 이상 모두 디퍼드에도 적용된다.


상태는 한 방향으로만 이동

  • 잡이 특정 상태에 도달하면 이전 상태로 되돌아가지 않는다.
fun main(args: Array<String>) = runBlocking {
    val time = measureTimeMillis {
        val job = GlobalScope.launch {
            delay(2000)
        }

        // Wait for it to complete once
        job.join()

        // Restart the Job
        job.start()
        job.join()
    }
    println("Took $time ms")
}

↪ 코드는 2초 동안 실행을 일시 중단하는 잡을 만듦
처음 호출한 job.join()이 완료되면 잡을 다시 시작하기 위해 start() 함수가 호출되고, 두 번째 join() 을 호출해서 두 번째 실행이 끝날 때까지 대기
전체 실행 시간을 측정하고 time 변수에 저장한다.

총 실행에는 약 2초가 걸렸으므로 잡이 한 번만 실행됐음을 보여줌
완료된 잡에서 start()를 호출해 다시 시작했다면 총 실행 시간은 약 4초가 될 것

이전에 기술한 일단 잡이 특정 상태에 도달하면 이전 상태로 되돌아 가지 않는다. 과 일치한다.
잡은 완료됨(Completed) 상태에 도달했으므로 start()를 호출해도 아무런 변화가 없다.

최종 상태의 주의 사항

  • 일부 잡의 상태는 최종 상태로 간주
    -> 최종 상태는 잡을 옮길 수 없는 상태
    -> 잡이 이전 상태로 되돌아가지 않을 것이라는 점을 고려하면 해당 상태는 취소됨(Cancelled)과 완료됨(Completed) 임

⭐정리

모니터링할 때 잡의 다양한 상태와 현재의 상태를 산출하는 방법을 아는 것이 중요

  • 잡(Job) 은 아무것도 반환하지 않는 백그라운드 작업에 사용

  • 디퍼드(Deferred) 는 백그라운드 작업이 수신하려는 것을 반활할 때 사용

  • 잡은 다양한 상태값을 갖음
    -> 생성, 활성, 취소 중, 취소됨 및 완료됨

  • 잡의 현재 상태를 파악하기 위해 isActive, isCancelled 및 is Completed 속성 사용

  • 디퍼드는 잡을 확장해 무언가를 반환할 가능성을 높임

  • 디퍼드가 가질 수 있는 상태는 잡의 상태와 같음

  • 잡의 상태는 앞으로만 이동할 수 있음. 이전 상태로 되돌릴 수 없음❌❌

  • 최종 상태는 잡이 이동할 수 없는 상태 중 하나

  • 잡의 최종 상태는 취소됨 및 완료됨

  • Join() 을 사용해 디퍼드가 대기된 경우, 예외가 전파되지 않도록 값을 읽기 전에 취소됐는지 여부를 확인해야 함

  • 항상 잡에 예외를 기록하거나 표시하자🔥

profile
개발자희망자

0개의 댓글