Job 객체를 활용한 코루틴 제어

이윤설·2024년 10월 20일

코루틴 연구소

목록 보기
3/10

1. 코루틴 순차 처리

코루틴에서 순차 처리가 필요한 이유

• ‘토큰 업데이트 후 네트워크 요청’, ‘이미지 변환 후 업로드 요청’ 같이 작업 간에 선후 관계(종속성) 있는 작업들이 있다. 이런 작업들이 만약 순차 처리가 안된다면, 문제가 생기기 때문에 꼭 순차 처리를 해주어야 한다.

순차 처리를 안한 경우

의도한 것은 토큰 업데이트 시작 -> 완료 -> 네트워크 요청인데, 의도한 대로 동작하지 않는다!
이것이 순차 처리를 안한 것의 결과이다.

만약 이 작업에 순차 처리가 안될 경우에는, 클라이언트는 서버에서 Unauthorized 오류를 응답받았을 것이다. 왜냐하면 토큰이 업데이트 되지 않은 상태로 네트워크 요청을 했기 때문이다.

Join 함수 사용해 순차 처리하기

Job 객체의 join 함수를 사용하면, 코루틴 순차 처리가 가능하다. 만약 A 코루틴이 완료된 후에 B 코루틴이 실행돼야 한다면, B 코루틴이 실행되기 전에 A 코루틴에 대해 join 함수를 호출하면 된다.

join 함수를 사용하면 join 함수의 대상이 된 코루틴이 완료될 때까지 join 함수를 호출한 코루틴이 일시 중단된다. 위 코드에선 updateTokenJob이 완료될 때까지 runBlocking 코루틴이 중단된다.

joinAll 함수를 사용한 복수의 코루틴 순차 처리


실제 개발 시에는 서로 독립적인 복수의 코루틴을 병렬 실행한 후 이들이 모두 완료됐을 때 다음 작업을
실행해야 하는 경우가 많다.
예) 복수의 이미지 변환이 모두 완료된 이후 업로드를 해야 하는 경우

joinAll은 아주 간단하다.
n개의 join문을 한번에 작성하는 것이라고 보면 된다.

참고로 joinAll은 list에 담아서 사용할 수도 있다.

2. CoroutineStart.LAZY 사용해 지연 코루틴 만들기


launch를 호출해 실행하면, 코루틴이 즉시 실행된다.

지연 코루틴


지연 코루틴이란, 즉시 실행 요청되지 않는 코루틴이다.
launch 함수의 start 인자로 CoroutineStart.LAZY를 넘기면 지연 코루틴이 생성된다.
단, 지연 코루틴을 실행하려면 무조건 start 나 join 함수를 호출해야 한다.

지연 코루틴을 사용해야 하는 특정상황은 언제일까??

  1. Splash Screen 후 화면 전환

  2. 자동 로그아웃 기능

  3. API 요청 후 재시도 로직

  4. 백그라운드에서 주기적으로 작업 실행해야 할 때 (ex. 데이터를 일정 시간마다 새로고침하는 경우)

3. 코루틴 취소

코루틴 실행 도중, 더이상 코루틴을 실행할 필요가 없어지면 즉시 취소해야 한다!
취소하지 않으면 코루틴이 스레드를 계속해서 사용하기 때문에 애플리케이션의 성능 저하로 이어지기 때문이다.
예) 사용자가 오래 걸리는 이미지 변환 작업을 요청한 후 취소한 경우

cancel


가장 기본적인 함수로는 cancel이 있다.
하지만 cancel은 순차성 관점에서의 단점이 커서 잘 쓰이지는 않는다.

cancel 함수는 코루틴을 곧바로 취소하지 않고, 취소 확인용 플래그를 '취소 요청됨'으로 바꾸는 역할만 한다. 이후 미래에 취소 확인용 플래그가 확인되는 시점에 코루틴이 취소된다.

예시

  • 영화관: 자동화된 티켓 예매 및 취소 시스템을 가진 영화관
  • 고객: 영화 티켓을 예매한 후 취소하려는 사람
  • 티켓 예매: 실행 중인 코루틴
  • 취소 요청: cancel() 함수 호출
  1. 티켓 예매 (코루틴 실행)

    • 고객이 영화 티켓을 예매한다 (코루틴 시작).
    • 예매 과정에는 여러 단계가 있다 (좌석 선택, 결제 등).
  2. 취소 요청 (cancel() 호출)

    • 고객이 콜센터에 전화해 취소를 요청한다.
    • 콜센터는 고객의 예매 정보에 '취소 요청됨' 플래그만 표시한다.
    • 이 시점에서는 아직 실제 취소가 이루어지지 않았다.
  3. 취소 처리 (코루틴의 취소 확인 및 실행)

    • 예매 시스템은 주기적으로 '취소 요청됨' 플래그를 확인한다.
    • 플래그가 확인되면, 그 시점에서 예매 과정을 중단하고 취소 절차를 시작한다.
    • 이미 완료된 단계(예: 결제)는 롤백이 필요할 수 있다.
  4. 취소 완료 (코루틴 종료)

    • 모든 취소 절차가 완료되면 예매가 최종적으로 취소된다.
    • 고객에게 취소 완료 메시지가 전송된다.

예제

fun main() = runBlocking<Unit> {
    val longJob: Job = launch(Dispatchers.Default) {
        Thread.sleep(1000L)
        println("longJob 코루틴의 동작")
    }
    longJob.cancel()
    executeAfterJobCancelled()
}

fun executeAfterJobCancelled() {
    println("하하")
}

executeAfterJobCancelled는 말그대로 cancel 된 후에 작동해야 하는데, 취소가 늦게 이루어졌음을 확인할 수 있다.

cancelAndJoin

cancelAndJoin = Cancel + Join : 취소 요청한 후 취소가 완료될 때까지 호출 코루틴 일시 중단
코루틴이 취소된 후 실행돼야 하는 코루틴이 있다면, 코루틴을 취소할 때 cancelAndJoin 함수를 사용하면 된다.

예제

fun main() = runBlocking<Unit> {
    val longJob: Job = launch(Dispatchers.Default) {
        Thread.sleep(1000L)
        println("longJob 코루틴의 동작")
    }
    longJob.cancelAndJoin()
    executeAfterJobCancelled()
}

4. 코루틴 취소 확인 시점

코루틴이 취소를 확인하는 시점은 다음 두가지이다.
1. 일시 중단 시점
2. 코루틴이 실행을 대기하는 시점

취소 확인 시점 만들기

코루틴에 취소 확인 시점을 만드는 세가지 방법
1. delay 함수를 사용해 취소 확인 시점 만들기
2. yield 함수를 사용해 취소 확인 시점 만들기
3. CoroutineScope.isActive를 사용한 취소 확인

delay 함수를 사용해 취소 확인 시점 만들기

fun main() = runBlocking<Unit> {
    val whileJob: Job = launch(Dispatchers.Default) {
        while (true) {
            println("작업 중")
            delay(1L)
        }
    }
    delay(100L)
    whileJob.cancel() // 코루틴 취소
}
  1. 비동기 실행:
    launch(Dispatchers.Default)를 사용해 새로운 코루틴을 시작합니다. 이 코루틴은 메인 스레드와는 별도로 실행됩니다. 따라서 while (true) 루프가 무한히 실행되더라도 메인 스레드를 차단하지 않습니다.

  2. 코루틴의 동시성:
    runBlocking은 여러 코루틴이 동시에 실행될 수 있는 코루틴 스코프를 만듭니다. 따라서 whileJob 코루틴과 메인 코루틴(delay(100L)와 whileJob.cancel()을 실행하는)이 동시에 실행됩니다.

  3. 실행 순서:

    • whileJob 코루틴이 시작되어 백그라운드에서 실행됩니다.
    • 동시에, 메인 코루틴은 delay(100L)를 실행합니다.
    • 100밀리초 후, 메인 코루틴은 whileJob.cancel()을 호출합니다.
  4. 취소 메커니즘:

    • delay(1L)는 중단 함수입니다. 이 함수는 코루틴의 취소 상태를 확인합니다.
    • whileJob.cancel()이 호출되면, 다음 delay(1L) 호출 시 취소 요청이 감지되고 코루틴이 종료됩니다.
  5. 취소 요청과 확인:

    • whileJob.cancel()은 코루틴에 취소 요청을 보냅니다.
    • delay(1L)는 취소를 확인하는 역할을 합니다.
  6. 동작 과정:

    • cancel()이 호출되면, 코루틴의 상태가 '취소 요청됨'으로 변경됩니다.
    • 다음 delay(1L) 호출 시, 이 상태를 확인하고 코루틴을 종료합니다.
  7. 거의 동시 종료:

    • delay(1L)의 지연 시간이 매우 짧기 때문에, cancel() 호출 후 거의 즉시 코루틴이 종료됩니다.
    • 실제로는 약간의 시간 차이가 있지만, 사용자 입장에서는 거의 동시에 일어나는 것처럼 보입니다.
  8. 협조적 취소:

    • 이는 코틀린 코루틴의 '협조적 취소' 메커니즘의 예입니다.
    • 코루틴은 취소 요청을 받았을 때 스스로 종료할 기회를 갖습니다.
  9. delay()의 역할:

    • delay()는 단순한 대기 함수가 아닙니다.
    • 코루틴의 취소 상태를 확인하는 중단점(suspension point) 역할도 합니다.

따라서, cancel()은 취소를 요청하고, delay(1L)는 그 취소를 확인하여 실제로 코루틴을 종료시키는 역할을 합니다. 이 두 과정이 매우 빠르게 연속해서 일어나기 때문에, 실질적으로는 거의 동시에 발생하는 것처럼 보이게 됩니다.

따라서 while (true)가 무한 루프이긴 하지만, 코루틴의 비동기 실행과 협조적 취소 메커니즘 덕분에 메인 스레드가 차단되지 않고 취소가 가능한 것입니다. delay(1L)가 주기적으로 실행되어 취소 요청을 확인하고 적절히 종료할 수 있게 해줍니다.

하지만 delay를 이용하여 취소를 확인하면 while 문이 반복될 때마다 일시중단 된다.
이는 불필요하게 작업을 지연시켜서 성능 저하가 발생한다.

yield 함수를 사용해 취소 확인 시점 만들기

fun main() = runBlocking<Unit> {
    val whileJob: Job = launch(Dispatchers.Default) {
        while (true) {
            println("작업 중")
            yield()
        }
    }
    delay(100L)
    whileJob.cancel() // 코루틴 취소
}

yield란 ‘양보’ 라는 뜻으로, yield 함수를 호출한 코루틴은 자신이 사용하던 스레드를 양보한다.
스레드를 양보한다는 것은 코루틴이 스레드 사용을 중단하고, 일시 중단 한다는 뜻이다.
스레드를 양보한 후 곧바로 재개 요청된다.

yield도 문제점이 있다. delay와 마찬가지로 모두 일시중단 후 재개 과정을 거친다.

재개 시에는 CoroutineDispatcher에 의해 다시 스레드로 보내지는 과정을 거치기 때문에 비효율적이다.

delay, yield 비교하기 (중요)

  1. delay(1L) 사용:

    • 각 반복마다 1밀리초 동안 코루틴을 일시 중단한다.
    • 실제 작업 시간(빨간색 막대)과 대기 시간(파란색 영역 사이의 공백) 사이에 명확한 간격이 있다.
    • 100ms 동안 약 100번의 작업을 수행할 수 있다.
  2. yield() 사용:

    • 다른 코루틴에게 실행을 양보하지만, 즉시 다시 실행을 재개한다.
    • 작업들 사이의 간격이 매우 좁아 거의 연속적으로 보인다.
    • 100ms 동안 훨씬 더 많은 작업을 수행할 수 있다.

주요 차이점:

  • delay(1L)는 실제로 코루틴을 일시 중단하여 다른 작업이 실행될 수 있는 시간을 제공한다.
  • yield()는 다른 코루틴에게 실행 기회를 주지만, 즉시 다시 실행을 재개하므로 더 많은 작업을 수행할 수 있다. 단, yield는 실행 시간은 정해지지 않는다.
    yield()는 매우 빠르게 실행되며, 일반적으로 마이크로초(µs) 단위 또는 그 이하의 시간이 걸린다.
    정확한 시간은 시스템 상태, 다른 실행 가능한 코루틴의 존재 여부 등에 따라 달라질 수있다.

중요한 점은 yield()가 delay(1L)처럼 고정된 시간 동안 대기하는 것이 아니라, 가능한 한 빨리 제어권을 다시 가져온다는 것이다. 이로 인해 yield()를 사용하는 루프는 delay(1L)를 사용하는 루프보다 훨씬 더 많은 반복을 수행할 수 있다.

결과적으로, yield()를 사용한 버전이 더 많은 "작업 중" 메시지를 출력할 수 있지만, CPU 사용량이 더 높을 수 있다. 반면 delay(1L)를 사용한 버전은 더 적은 메시지를 출력하지만, 다른 코루틴들이 실행될 수 있는 여유를 제공한다.

재밌는 예시

상황: 붐비는 은행에서 일하는 두 직원의 예

  1. delay(1L)를 사용하는 직원 A:

    • 고객을 응대한 후 항상 1분간 휴식을 취한다.
    • 휴식 시간 동안 다른 직원들이 고객을 응대할 수 있다.
    • 1분이 지나면 다음 고객을 부른다.
    • 하루 동안 상대적으로 적은 수의 고객을 응대하지만, 다른 직원들도 일할 기회가 많다.
  2. yield()를 사용하는 직원 B:

    • 고객을 응대한 후 "다른 직원 분, 고객 응대하실 분 계신가요?"라고 매우 빠르게 외친다.
    • 아무도 응답하지 않으면 즉시 다음 고객을 부른다.
    • 이 과정은 눈 깜짝할 사이에 일어나므로, 대부분의 경우 B가 연속해서 고객을 응대한다.
    • 하루 동안 매우 많은 고객을 응대하지만, 다른 직원들이 끼어들 기회는 거의 없다.

주요 차이점:

  • 직원 A(delay)는 규칙적으로 쉬면서 다른 직원들에게 확실한 기회를 준다.
  • 직원 B(yield)는 다른 직원에게 기회를 주는 척하지만, 실제로는 거의 쉬지 않고 계속 일한다.

실제 코루틴에서의 의미:

  • delay를 사용하면 코루틴이 일정 시간 동안 확실히 중단되어 다른 코루틴이 실행될 수 있다.
  • yield를 사용하면 코루틴이 잠깐 양보하지만 대부분 즉시 다시 실행된다. 따라서 더 많은 작업을 빠르게 처리할 수 있지만, 다른 코루틴들이 실행될 기회는 상대적으로 적다.

장단점 비교

  • yield()
    장점:
    매우 빠르게 실행되어 높은 처리량을 제공한다.
    CPU 바운드 작업에 유용할 수 있다.

    단점:
    다른 코루틴에게 실행 기회를 효과적으로 주지 못할 수 있다.
    CPU를 과도하게 사용할 수 있어 시스템 자원을 많이 소모할 수 있다.

  • delay()
    장점:
    명확하게 다른 코루틴에게 실행 기회를 준다.
    시스템 자원을 효율적으로 사용할 수 있다.
    I/O 바운드 작업이나 주기적인 작업에 적합하다.

    단점:
    지정된 시간 동안 항상 대기하므로 즉각적인 응답이 필요한 경우에는 부적합할 수 있다.

    그러니까 간단하게 요약하자면 보통 코루틴을 사용하려면 취소 기능도 넣어야 한다.
    이때 취소를 하려면 취소 확인 시점 -> 취소완료의 과정을 거치는데,
    취소 확인 시점에서 1. yield 또는 2. delay 할 것인지 수행중인 로직에 알맞게 결정하면 된다.

CoroutineScope.isActive를 사용한 취소 확인

fun main() = runBlocking<Unit> {
    val whileJob: Job = launch(Dispatchers.Default) {
        while (this.isActive) {
            println("작업 중")
        }
    }
    delay(100L)
    whileJob.cancel() // 코루틴 취소
}

isActive를 사용하면 코루틴이 취소 요청을 받았는지 수동으로 확인할 수 있어, 작업을 계속할지 멈출지 결정할 수 있다. 이 방식은 취소 가능한 함수(delay, yield)를 사용하지 않고도 직접 취소 상태를 체크할 수 있다. 코드에서 while(this.isActive)를 사용하여, 코루틴이 취소되기 전까지 계속 작업을 진행하고, 취소 요청이 오면 루프를 빠져나가게 만든다.

5. 코루틴의 상태와 Job 객체의 상태 변수

코루틴의 상태

코루틴의 상태는 위와 같다.
단, 실행 완료 중은 아직 배우지 않은 개념이 필요하므로 일단은 생략하겠다.

코루틴의 상태 출력하는 함수

코루틴의 상태를 출력하기 위해서는 Job 객체를 프린트하면 된다.

다른 상태도 똑같이 출력하면 된다.
세부 내용은 강의자료를 참고하겠다.

Job의 toString()을 사용해 출력하는 방식의 문제점


Job의 toString()을 사용해 출력을 하게 되면, Job 객체를 구현하는 JobSupport 객체에서 디버깅 용도로 만드는 문자열(toDebugString())을 사용하는 것이라서 직접 코드에서 사용하기 어렵다.

Job 객체의 상태 변수

Job 객체는 코루틴을 추상화한 객체여서 코루틴의 상태를 간접적으로 나타내는 세가지 상태 변수를 외부로 공개한다.

• isActive: 코루틴이 활성화 되어 있는지 여부. 코루틴이 ‘실행 중’ 상태일때 true이다.
• isCancelled: 코루틴에 취소가 요청됐는지 여부. cancel 함수가 호출되기만 하면 true를 반환하므로, 취소 중인 상태도 포함한다.
• isCompleted: 코루틴이 완료 되었는지 여부, 코루틴이 실행 완료 되거나 취소 완료되면 true 반환


Job 객체의 상태 변수 정리

요약

코루틴 순차 처리

  1. Job 객체의 join 함수를 사용해 코루틴 순차 처리를 할 수 있다.
  2. join 함수는 join 함수를 호출한 코루틴을 join의 대상이 된 코루틴이 완료될 때까지 일시 중단시킨다.
  3. joinAll 함수를 사용해 복수의 코루틴에 대한 순차 처리를 할 수 있다.
    CoroutineStart.LAZY 사용해 지연 코루틴 만들기
  4. launch의 start 인자에 CoroutineStart.LAZY 옵션을 넣으면 지연 코루틴을 만들 수 있다.
  5. 지연 코루틴은 Job 객체의 start나 join 함수가 호출될 때까지 실행 요청되지 않는다.

코루틴 취소

  1. cancel 함수를 사용해 코루틴을 취소할 수 있다.
  2. cancel 함수는 코루틴을 곧바로 취소하지 않고, 취소 확인용 플래그만 변경한다.
  3. cancelAndJoin 함수를 사용하면 함수를 호출한 코루틴을 취소가 완료될 때까지 일시 중단할 수 있다.

코루틴 취소 확인

  1. 코루틴은 취소 확인 시점이 있어야 취소될 수 있다. 취소 확인 시점은 일시 중단 시점과, 실행 대기 시점이다.
  2. delay 함수를 사용하면 일시 중단 시점을 만들 수 있지만, 일정 시간 동안 스레드를 양보하기 때문에 비효율적이다.
  3. yield 함수를 사용하면 일시 중단 시점을 만든 후 곧바로 재개 요청되기 때문에 delay 함수보다 효율적이다.
  4. delay 함수와 yield 함수는 모두 일시 중단 시점을 만들어 재개 되는 과정을 거치기 때문에 비효율적이다.
  5. CoroutineScope 객체의 isActive 확장 프로퍼티를 사용하면 코루틴을 일시 중단 시키지 않고 취소를 확인할 수 있다.

코루틴의 상태와 상태 변수

  1. Job 객체를 프린트 하면 코루틴의 상태를 디버그용 문자열 값으로 출력 할 수 있다.
    하지만 이 값은 코드에서는 사용하기 어렵다.
  2. Job 객체는 코루틴의 상태를 간접적으로 나타내는 세가지 상태 변수를 외부로 공개한다.
  3. isActive는 코루틴이 실행 중 상태일 때만 true이다. 취소 중 상태에서 코루틴은 실행되지만, isActive는 false이다.
  4. 취소가 요청되면 isCancelled는 true가 된다.
  5. 실행이 완료되던 취소가 완료되던, 코루틴의 실행이 완료되었을 때, isCompleted는 true가 된다.

강의자료 출처 및 참고한 강의:

코틀린 코루틴 완전 정복 by 조세영님
https://www.inflearn.com/course/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5/dashboard

profile
화려한 외면이 아닌 단단한 내면

0개의 댓글