Owl Life

Coroutines & Flows - 다섯가지 안티 패턴 본문

Android Dev/dev

Coroutines & Flows - 다섯가지 안티 패턴

Owl Life 2025. 6. 14. 20:32
반응형

 

 

 

1. UI 업데이트를 메인 스레드에서 하면 안 되는 이유

  • 여러분 앱의 화면(UI)은 마치 상점의 간판처럼 항상 손님(사용자)에게 최신 정보를 보여줘야 합니다.
  • 그런데 이 간판을 업데이트하는 작업이 너무 복잡하고 오래 걸리면(CPU를 많이 사용하면), 간판이 잠시 멈춰버리겠죠?
    • 이처럼 뷰 모델에서 UI 상태를 업데이트하는 복잡한 로직을 실행할 때, 기본적으로는 메인 스레드라는 중요한 길에서 작업하게 됩니다.
    • 이 길이 막히면 앱 화면 전체가 멈추는 것처럼 느껴져서 사용자가 불편함을 느낍니다.
  • 이 문제를 해결하려면 복잡한 UI 업데이트 작업은 Default Dispatcher라는 다른 작업자에게 맡겨야 합니다.
    • 이렇게 하면 메인 스레드가 막히지 않고, UI는 계속 부드럽게 움직일 수 있습니다.

 

 

2. 화면 이동 시 데이터 저장이 날아가 버리는 문제

  • 앱에서 화면을 이동할 때(예: 노트 작성 화면에서 목록 화면으로 돌아갈 때), 현재 화면과 연결된 View Model Scope에서 실행 중인 코루틴은 자동으로 취소됩니다.
  • 만약 이 코루틴안에서 데이터를 저장하거나 서버와 동기화하는 중요한 작업을 하고 있었다면, 화면을 이동하는 순간 그 작업이 중간에 멈춰버릴 수 있습니다.
    • 예를 들어, 작성 중인 노트를 저장 버튼을 눌러 저장하고 바로 이전 화면으로 돌아갔는데, View Model Scope가 취소되면서 서버에 저장되지 않는 문제가 발생할 수 있습니다.
  • 이 문제를 막으려면 두 가지 방법이 있습니다.
    • 첫째, 데이터 저장이 완료될 때까지 UI에게 기다리라고 알려주고, 저장이 끝난 후에 화면 이동을 시킵니다.
    • 둘째, View Model Scope 대신 앱이 살아있는 동안 계속 유지되는 Application Scope를 사용해서 중요한 백그라운드 작업을 실행합니다.

 

3. 병렬로 할 수 있는 일을 순서대로 시키는 문제

안티 패턴 문제점 올바른 해결 방법 예시
병렬 작업 순차 실행 여러 코루틴 작업을 병렬로 실행하여 시간을 단축할 수 있음에도 불구하고, 순서대로 실행하게 만듦. 모든 코루틴 작업을 먼저 시작(launch)시킨 후, 마지막에 모든 코루틴이 완료될 때까지 한꺼번에 기다림(join). 프로필 사진 업로드와 프로필 정보 업데이트를 동시에 진행.
`launch` 사용 후 바로 `join` 호출 코루틴을 시작하고 바로 완료될 때까지 기다리므로, 다음 작업이 시작되기 전에 현재 작업이 끝나야 함. 모든 `launch` 호출을 먼저 완료한 후, 마지막에 모든 `join` 호출을 수행. `launch { 작업1() }; launch { 작업2() }; 작업1.join(); 작업2.join()` (잘못된 방식) vs `launch { 작업1() }; launch { 작업2() }; joinAll()` (올바른 방식)
`async` 사용 후 바로 `await` 호출 비동기 작업을 시작하고 바로 결과를 기다리므로, 비동기 작업의 이점을 살리지 못하고 순차적으로 실행됨. 모든 `async` 호출을 먼저 완료한 후, 마지막에 모든 `await` 호출을 수행. `val 결과1 = async { 작업1() }.await(); val 결과2 = async { 작업2() }.await()` (잘못된 방식) vs `val 작업1 = async { 작업1() }; val 작업2 = async { 작업2() }; val 결과1 = 작업1.await(); val 결과2 = 작업2.await()` (올바른 방식)

 

  • 여러 코루틴작업을 동시에(병렬로) 실행하면 전체 작업 시간을 크게 줄일 수 있습니다. 예를 들어, 프로필 사진 업로드와 프로필 정보 업데이트는 서로 기다릴 필요 없이 동시에 진행될 수 있습니다.
  • 하지만 launch를 사용해서 코루틴을 시작하자마자 바로 join을 호출하거나, async를 사용하고 바로 await를 호출하면, 마치 한 사람씩 줄 서서 일을 처리하는 것처럼 순서대로 실행되게 만들어 버립니다.
    • launch는 코루틴을 시작하라는 명령이고, join은 그 코루틴이 끝날 때까지 기다리라는 명령입니다.
    • launch 다음에 바로 join을 쓰면, 첫 번째 작업이 끝날 때까지 기다렸다가 두 번째 작업을 시작하게 됩니다.
  • 올바르게 병렬 실행을 하려면, 모든 코루틴작업을 먼저 다 시작( launch)시켜 놓습니다.
    • 그리고 마지막에 모든 코루틴이 완료될 때까지 한꺼번에 기다리도록( join) 하면 됩니다. 이렇게 해야 비로소 여러 작업이 동시에 착착 진행됩니다.

 

4. while true 반복문에서 취소 요청이 무시되는 문제

  • 데이터를 주기적으로 가져오는 작업처럼 while true 무한 반복문 안에서 코루틴을 실행할 때 주의해야 할 점이 있습니다.
  • 만약 이 반복문 안에 try-catch 블록을 사용하여 일반적인 예외를 잡도록 코드를 작성하면 문제가 생길 수 있습니다.
    • 코루틴이 취소될 때 발생하는 취소 예외(Cancellation Exception)는 try-catch 블록에 의해 일반 예외처럼 잡혀버릴 수 있습니다.
    • 이렇게 되면 코루틴은 사실상 취소된 상태인데, 반복문은 취소 사실을 인지하지 못하고 계속 돌게 됩니다. 마치 "취소됐는데 취소 안 된 척"하며 무한 루프에 빠지는 거죠.
  • 이 문제를 해결하려면 try-catch 블록 안에서 예외를 잡은 후, coroutineContext.ensureActive()를 호출하여 현재 코루틴이 활성 상태인지 다시 확인해야 합니다.
    • 이 함수는 만약 코루틴이 취소된 상태라면 취소 예외를 다시 발생시켜서 반복문이 제대로 종료되도록 돕습니다.

 

5. Supervisor Job을 잘못 사용하는 문제

안티 패턴 문제점 올바른 사용법 설명
Supervisor Job을 launch 함수에 직접 전달 자식 코루틴들이 Supervisor Job의 독립적인 실패 처리 혜택을 받지 못하고 부모의 실패에 영향을 받음 `supervisorScope` 블록 안에서 코루틴 실행 `supervisorScope` 안에서 시작된 코루틴은 서로 독립적으로 실패 처리됨

 

  • supervisor job은 특정 코루틴이 실패하더라도 다른 자식 코루틴들이 영향받지 않고 계속 실행될 수 있도록 해주는 특별한 Job입니다.
  • 하지만 launch 함수에 Supervisor Job을 직접 인자로 전달하는 것은 올바른 사용법이 아닙니다.
    • 코루틴의 Job은 자식 코루틴에게 상속되지 않는 유일한 컨텍스트 요소입니다.
    • 그래서 launch에 Supervisor Job을 직접 넘겨주면, 코틀린 내부적으로 이상한 계층 구조가 만들어져서, 자식 코루틴들이 supervisor job의 혜택(실패 시 독립적인 실행)을 제대로 받지 못합니다. 즉, 따로 놀아야 할 자식들이 부모 때문에 같이 실패하게 되는 거죠.
  • supervisor job의 기능을 제대로 사용하려면 supervisorScope라는 특별한 함수 블록 안에서 코루틴을 실행해야 합니다.
    • supervisorScope 안에서 launch로 시작된 코루틴들은 서로 독립적으로 실패할 수 있게 됩니다. 마치 각자 알아서 책임지고 실패하는 자식들처럼 말이죠.

 

 

출처 : https://youtu.be/JyBq76N4Zc4?si=OEGCPCf9D0_37NTY

 

 

반응형
Comments