ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 안드로이드 비동기 처리 1 (Thread, Runnable, Executor Service)
    안드로이드 학습/Android 기술면접 대비 2024. 7. 18. 13:58

    안드로이드에서 비동기 작업을 하는 방법은 여러가지 있다.

    • Thread & Runnable
    • RxJava
    • Coroutine

    각각의 방법은 장단점이 있기 때문에 상황에 맞게 적절하게 사용되는 것이 바람직하다. 

    어느 타이밍에 써야 적합할지를 알기 위해서는 각각 방법을 학습해서 학습할 필요성을 느꼈다.

     

    그래서 먼저 가장 기본적인 Thread를 생성하고 사용하는 3가지 방식을 학습해보리고 한다. 

     

     Thread 좀 더 간단하게 사용하는 방식은 아래와 같다. 

    • 1) Thread를 직접 만들어서 사용하는 방식
    • 2) Runnable을 만든 후, Thread에 Runnable을 넘겨서 실행하도록 하는 방식
    • 3) Executor Service를 이용해서 Thread Pool을 만들거 Runnable 생성후 submit 하는 방식

     

    아래 코드의 모든 예제는 0부터 10초까지 숫자를 세주는 코드이다.

    1. Thread를 직접 만들어서 사용하는 방식 

    가장 기본적인 것은 Thread만 생성해서 사용하는 방법이다. 

     

    1-1 객체식(코틀린) / 익명객체(자바)으로 Thread 생성 방법

    private fun addThread() {
        val thread = object : Thread() {
            override fun run() {
                for (num in 0..10) {
                    handlerUtils.handlerPost {
                        binding?.tvMultithreadThreadTest?.text = "$num"
                    }
                    sleep(1000L)
                }
            }
        }
        thread.start()
    }

     

     

    메인 Thread가 아닌 새로 생성된 Thread에서는 UI 수정이 안되기 때문에 TextView 같은 것을 변경하려면 runOnUiThread 같은 Handler를 사용하는 것을 이용해서 UI 변경을 해야한다. 

     

    1-2 class로 Thread 상속하는 해서 사용하는 방법

    class ThreadExample : Thread() {
        override fun run() {
    
        }
    }
    

     

    • Thread를 상속해서 사용한다면 다른 class를 상속해서 사용할수는 없다. 그래서 유연성을 높이기 위한 방법으로는 Runnable을 사용하는 방식이다. 
    • 간단한 소스 같은 경우에는 쉽게 관리가 가능하지만 복잡해진다면 스레드를 사용자가 직접 관리하기 어렵다.

     

    2. Runnable Interface를 구현하는 방법

    2-1 객체식(코틀린) / 익명객체(자바)로 Runnable 사용하는 방법

    private fun useRunnable() {
        val runnable = Runnable {
            for (num in 0..10) {
                handlerUtils.runOnUiThread(activity) {
                    binding?.tvMultithreadRunnableTest?.text = "$num"
                }
                sleep(1000L)
            }
        }
        val runnableTestThread = Thread(runnable)
        runnableTestThread.start()
    }

     

    2-2 class로 Runnable을 상속하는 해서 사용하는 방법

    class RunnableExample : Runnable {
        override fun run() {
            
        }
    }
    • Runnable interface를 상속하기 때문에 다른 Class를 상속해서 사용하기 때문에 좀더 유연성 있게 사용이 가능하다.
    • Runnable을 implement 하는 방식 또한 간단한 소스에는 쉽게 관리가 가능하지만 복잡해진다면 스레드를 사용자가 직접 관리하기 어렵다.

     

    3. Executor Service를 이용해서 Thread Pool을 만들어 Runnable에 submit 하는 방식

    3-1) ExecutorService와 Thread Pool의 구조

     

    Executor Service는 Thread pool과 Queue로 구성되어 있다. 작업할 task들은 Queue에 들어가게 되고 순차적으로 스레드에 할당된다. 스레드가 만약 남아있지 않다면 Queue 안에서 대기하게 된다. 스레드를 생성하는 것은 비용이 큰 작업이기 때문에 이를 최소화하기 위해 미리 스레드 풀 안에 스레드를 생성해 놓고 관리한다. 

     

    3-2) Executor의 장점

    Executor는 Thread에 비해 여러가지 스레드 관리의 효율성코드의 가독성유지보수성에 장점이 있다.

    더보기

    1. 스레드 관리의 효율성

    • 스레드 풀 관리: Executor Service는 내부적으로 스레드 풀(thread pool)을 사용한다. 스레드 풀은 미리 생성된 스레드를 재사용하므로 새로운 스레드를 매번 생성하고 파괴하는 비용을 줄일 수 있습니다. 이는 성능 향상과 메모리 사용 감소에 기여한다.
    • 스레드 수 제한: 직접 스레드를 생성할 경우 무한정 스레드가 생성될 수 있는데, 이는 시스템 자원을 낭비하고 앱의 성능 저하를 초래할 수 있습니다. Executor Service를 사용하면 스레드 풀의 크기를 제한할 수 있어 과도한 스레드 생성을 방지할 수 있습니다.

    2. 작업 스케줄링과 큐잉

    • 작업 큐: Executor Service는 작업(러너블 또는 콜러블)을 큐에 넣고, 스레드 풀의 여유가 있을 때 작업을 실행합니다. 이를 통해 작업의 스케줄링 큐잉을 손쉽게 처리할 수 있습니다.
    • 비동기 작업 관리: Executor Service는 submit 메서드를 통해 비동기 작업을 제출하고, Future 객체를 사용하여 결과를 추적하거나 반환값을 받을 수 있습니다.

    3. 코드의 가독성과 유지보수성

    • 반복적인 코드 감소: 직접 스레드를 관리하려면 스레드 생성, 시작, 종료를 직접 코드로 작성해야 합니다. 반면, Executor Service를 사용하면 이러한 반복적인 작업을 줄일 수 있어 코드가 더 간결해집니다.
    • 작업 종료 및 타임아웃 처리: Executor Service는 작업 종료 및 타임아웃을 쉽게 관리할 수 있는 메서드를 제공합니다. 예를 들어, shutdown이나 shutdownNow 같은 메서드를 통해 스레드 풀을 안전하게 종료할 수 있습니다.

    4. 향상된 에러 처리

    • Executor Service를 사용하면 작업 중 발생하는 예외를 좀 더 효과적으로 관리할 수 있습니다. Future 객체의 get 메서드를 통해 예외를 잡아낼 수 있으며, 에러 발생 시 적절한 처리를 수행할 수 있습니다.

    요약

    • 스레드 관리가 더 효율적이고 작업 스케줄링이 용이합니다.
    • Future을 이용해서 결과를 추적하거나 반환값을 받을 수 있다. 
    • 스레드 풀을 사용하여 시스템 자원 낭비를 줄일 수 있습니다.
    • 반복 코드 감소와 함께 가독성  유지보수성이 향상됩니다.
    • 에러 처리와 작업 종료 관리가 편리합니다.

    이러한 이유로 Executor Service가 스레드를 직접 사용하는 것보다 더 권장되는 방법입니다.\

     

    3-3) 예제 코드

    Executor의 경우 Thread를 만들고 관리하는 것을 Executor에 위임한다. Thread를 사용하려는 개발자는 Runnable만 만들어서 넘겨주기만 하면 된다. 

     

    ExecutorService 객체 생성

    val executorService = Executors.newFixedThreadPool(2)

     

    Executors는 아래의 메서드들을 제공하여 스레드 풀의 개수 및 종류를 정할 수 있다.

    • newFixedThreadPool(int) : 인자 개수만큼의 고정된 스레드 풀을 생성한다.
    • newCachedThreadPool() : 필요할 때, 필요한 만큼의 스레드 풀을 생성한다.
    • newSingleThreadExecutor() : 스레드가 1개인 ExecutorService를 리턴한다. 싱글 스레드에서 동작해야 하는 작업을 처리할 때 사용한다.
    private fun useExecutor() {
        val executorService = Executors.newFixedThreadPool(2)
        val runnable = Runnable {
            for (num in 0..10) {
                handlerUtils.runOnUiThread(activity) {
                    binding?.tvMultithreadExecutorTest?.text = "$num"
                }
                sleep(1000L)
            }
        }
        executorService.submit(
            runnable
        )
    
        executorService.shutdown();
    }

     

    • Executors.newFixedThreadPool(2)  : 2개의 Thread를 사용 할 수 있다는 의미다.
    • 만약 작업이 스레드 숫자보다 많다면 작업을 바로 처리하지 못하고 Blocking Queue에 쌓아서 대기한 후에 앞의 작업이 다 끝난 후에 작업을 처리한다. 

     

    private fun testMoreExecutor() {
        val executorService = Executors.newFixedThreadPool(2) //Thread 2
        val addRunnable: (Int) -> Runnable = { idx ->
            Runnable {
                for(num in 0..10){
                    binding?.apply {
                        handlerUtils.runOnUiThread(activity) {
                            when(idx){
                                1 -> tvMultithreadExecutorTestMore1.text = "$num"
                                2 -> tvMultithreadExecutorTestMore2.text = "$num"
                                3 -> tvMultithreadExecutorTestMore3.text = "$num"
                                4 -> tvMultithreadExecutorTestMore4.text = "$num"
                            }
                        }
                    }
                    sleep(1000L)
                }
            }
        }
    
        executorService.submit(addRunnable(1))
        executorService.submit(addRunnable(2))
        executorService.submit(addRunnable(3))
        executorService.submit(addRunnable(4))
    
        executorService.shutdown()
    }

     

    ThreadPool을 2개 만들었지만 작업이 4개이기 때문에 2개씩 처리가 된다. 

     

    처음에 0 부터 10까지 2개만 처리되고 

    1 -> tvMultithreadExecutorTestMore1.text = "$num" 

    2 -> tvMultithreadExecutorTestMore2.text = "$num"

     

    처리가 다되면 아래 2개가 시작된다.

    3 -> tvMultithreadExecutorTestMore3.text = "$num" 

    4 -> tvMultithreadExecutorTestMore4.text = "$num"

     

     

    3가지 Thread 사용 방법을 알아봤다. 요즘은 비동기 작업에는 Thread 말고도 Rxjava, 코루틴을 많이 사용하기는 하지만 Thread만의 장점이 있다고 생각한다.

    • Learning Curve가 낮고 간단히 사용하기 편리하다. (Rxjava 같은 경우 초보자가 학습하기에는 데이터의 흐름이나 개념이 좀 어렵다. Coroutine 경우에도 개념의 이해와 사용방법의 학습이 필요하다.)
    • 간단한 비동기 작업에 적합하다. (다른 것들은 라이브러리까지 추가 되어야 하기 때문이다.)

     

    종합적으로 봤을때, 아래의 경우에 사용할 것 같다. 

    • 간단하게 비동기를 사용해야 할때나
    • Rxjava나 Coroutine에 대한 지식이 없는 상태에서 빠르게 비동기를 사용해야 할 경우

     

    하지만 앱 프로젝트가 커지면서 혹은 처음 설계에서 비동기 Task가 많아지고 복잡해 질 것을 예측한다면, 네트워크 통신 여러가지 메소드를 지원하는 Rxjava나 코루틴을 사용해보는 것을 고려해볼만하다.

Designed by Tistory.