티스토리 뷰

코루틴이 무엇인지에 대한 정의는 인터넷에 넘쳐나기 때문에, 다시 추가적으로 설명하지 않으려고 한다. 다만 최근 구글에서 안드로이드 플랫폼에서 Concurrent job을 처리하기 위한 공식적인 방법으로 권장하기 시작했다는 점을 언급하고 싶다. 만약 코틀린으로만 된 프로젝트라면 RxJava나 다른 솔루션보다는 코틀린에서 제공하고 구글이 공식적으로 인정한 코루틴을 사용하는 것이 자연스러운 선택이라고 할 수 있을 것 같다.

 

그럼, 코틀린은 어떤 모습이고, 왜 cocurrent job을 처리하는데 좋다고 할까?

먼저, 자바의 Thread에 대해 간략히 살펴보기로 하자. Thread 의 제약점을 통해 왜 코루틴이 유용한지 이해하는데 도움이 될 것이다.

fun main() {
    new Thread() {
        println("Thread started")
        Thread.sleep(2000L)
        println("Thread completed")
    }
    println("done")
}

실행결과는 다음과 같이 출력될 것이다.

2021-03-24 18:20:36.163 I/System.out: done
2021-03-24 18:20:36.163 I/System.out: Thread started
2021-03-24 18:20:38.165 I/System.out: Thread completed

예제에서는 Thread를 생성하여 2초간 정지한 후 코드를 종료했다. 그런데 실행결과는 보면 main이 제일 먼저 실행이 되고, 그 다음에 Thread  내부의 코드가 실행되는 것을 알 수 있다. 즉, 메인이 종료된 후에 Thread가 실행이 되고 있다. 대부분의 경우에는 Thread 가 끝날 때까지 Thread를 기다리고 싶을 것이다. 그렇게 하려면 아마도 다음과 같이 수정할 수 있을 것이다.

fun main() {
    new Thread() {
        println("Thread started")
        Thread.sleep(2000L)
        println("Thread completed")
    }
    Thread.sleep(2500L)
    println("done")
}

출력 결과:

2021-03-24 18:28:46.203 I/System.out: Thread started
2021-03-24 18:28:48.204 I/System.out: Thread completed
2021-03-24 18:28:48.704 I/System.out: done

원하던 결과를 얻었다. 그렇다면, 안드로이드로 돌아가서 위의 Thread를 실행한 후 Toast를 보여주려고 하면, 다음과 같이 코드를 추가할 수 있을 것이다. 

 

class MainActivity : AppCompatActivity {

    (다른 코드들은 생략)
    
    button1.setOnClickListener {
    	runTestThread()
    }
    
    private fun runTestThread() {
        new Thread() {
           println("Thread started")
           Thread.sleep(2000L)
           Toast.makeText(requireContext(), "Thread completed", Toast.LENGTH_LONG).show()
        }
        
        Thread.sleep(2500L)
        println("done")
    } 
}

불행히도, 다음과 같은 에러와 함께 앱이 크래쉬될 것이다.

2021-03-24 18:34:40.701 E/AndroidRuntime: FATAL EXCEPTION: Thread-2
    Process: com.mpark.coroutines, PID: 27489
    java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare()
        at android.widget.Toast$TN.<init>(Toast.java:390)
        at android.widget.Toast.<init>(Toast.java:114)
        at android.widget.Toast.makeText(Toast.java:277)
        at android.widget.Toast.makeText(Toast.java:267)
        at com.mpark.coroutines.exercises.exercise1.Exercise1Fragment$runThread$1.run(Exercise1Fragment.kt:62)
        at java.lang.Thread.run(Thread.java:764)
2021-03-24 18:34:41.197 I/System.out: done

 

Can't toast on a thread that has not called Looper.prepare()

 

에러의 원인은 백라운드 쓰레드에서 화면에 토스트를 보여주려고 했기 때문이다. 안드로이에서는 화면에 무언가를 그려주는 작업은 메인 Thread에서 처리해야 한다. 그렇지 않으면 같은 에러로 앱이 종료될 것이다. 이제 메인 쓰레드에서 Toast를 보여주자.

private fun runThread() {
        Thread {
            println("Thread started")
            Thread.sleep(2000L)

            Handler(Looper.getMainLooper()).post {
                Toast.makeText(requireContext(), "Thread completed", Toast.LENGTH_LONG).show()
            }
        }.start()

        Thread.sleep(3500L)
        println("done")
}

 

그럼, 다음 코드를 한번 보자.

private fun runThread() {
        Thread {
            println("Thread started")
            Thread.sleep(2000L)

            Handler(Looper.getMainLooper()).postDelayed ({
                Toast.makeText(requireContext(), "Thread completed", Toast.LENGTH_LONG).show()
            }, 500L)
        }.start()

        Thread.sleep(2000L)
        println("done")
        
        finish()
    }

다음과 같은 오류메세지와 함께 액티비티가 종료된다음, 앱 크래쉬가 발생할 것이다.

  Process: com.mpark.coroutines, PID: 29851
    java.lang.IllegalStateException: Fragment Exercise1Fragment{3c2e8d6 (0634ecd1-9cca-4623-86b8-abca4e985f97)} not attached to a context.
        at androidx.fragment.app.Fragment.requireContext(Fragment.java:774)
        at com.mpark.coroutines.exercises.exercise1.Exercise1Fragment$runThread$1$1.run(Exercise1Fragment.kt:65)
        at android.os.Handler.handleCallback(Handler.java:789)
        at android.os.Handler.dispatchMessage(Handler.java:98)
        at android.os.Looper.loop(Looper.java:164)
        at android.app.ActivityThread.main(ActivityThread.java:6541)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)

왜 그럴까?

백그라운드 쓰레드를 2000ms 대기하고 메인쓰레드를 2100ms 대기 후에 액티비티를 종료시킨 후 액티비티를 종료했다. 여기까지는 아무런 문제가 없다. 액티비티가 종료되기 전에  백그라운드 쓰레드를 실행했기 때문이다. 그런데 액티비티가 종료된 후 Toast를 보여주려고 할 때 requireContext()에 접근하고 있으나, 이미 액티비티가 종료된 상태이므로 참조할 Context없기 때문이다.

 

위에서 살펴 본 예제들을 대해 이미 너무 익숙한 경우이고,  해당 에러 케이스들을 핸들링하는데 전혀 문제가 없다면, 이 글을 무시하시기 바란다. 하지만, 만약 님의 머릿 속에 '어 생각보다 복잡하네' 라는 생각이 든다면, 아마도 코루틴을 배우는데 시간을 좀 더 할애하셔야할 가능성이 아주 높다.

 

다음 시간에는 코루틴에 대해 본격적으로 살펴보기로 하자.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함