HandlerThread와 AsyncTask, 그리고 왜 우리는 Coroutine으로 왔을까

유진·2025년 12월 13일

Android

목록 보기
14/17
post-thumbnail

앞선 글에서 프로세스, 스레드, UI 스레드, 그리고 Handler와 Looper까지 살펴봤다.

사실 앞선 글과 이번 글은 안드로이드 프로그래밍 Next Step 3장을 읽다가 정리한 내용을 바탕으로 작성하였다.
앞선 글이 추가적으로 조사한 내용을 바탕으로 작성한 글이고
이 글이 3장의 내용을 정리한 것이다.

3장에서는 백그라운드 스레드를 주제로 다음 두 방식을 소개한다.

  • HandlerThread
  • AsyncTask

둘 다 “백그라운드 작업을 안전하게 처리하기 위한 시도”라는 공통점을 가진다.


HandlerThread: Looper가 붙은 백그라운드 스레드

HandlerThread는 이름 그대로 Thread를 상속받은 클래스다.
차이점은 단 하나, Looper가 자동으로 붙어 있다는 것이다.

앞에서 봤듯이,

  • UI 스레드는 기본적으로 Looper가 있다
  • 일반 Thread는 Looper가 없다

그래서 일반 Thread 안에서 Handler() 기본 생성자를 쓰면 문제가 생긴다.
Handler는 반드시 Looper가 있는 스레드에 붙어야 하기 때문이다.

이 문제를 해결하기 위해 등장한 것이 HandlerThread다.


왜 HandlerThread가 필요했을까

일반 Thread의 특징은 단순하다.

  • 시작하면 실행
  • 작업이 끝나면 종료

하지만 이런 요구가 생긴다.

  • 백그라운드에서
  • 계속 살아 있으면서
  • 요청이 들어올 때마다
  • 순차적으로 처리하고 싶다

이건 사실상 UI 스레드와 동일한 구조다.
단지 UI가 없을 뿐이다.

그래서 안드로이드는

  • Looper
  • MessageQueue

를 가진 백그라운드 전용 스레드를 제공했고,
그게 HandlerThread다.


HandlerThread의 핵심 포인트

  • 내부에 Looper가 있음
  • MessageQueue 기반
  • 작업을 순서대로 처리
  • 직접 스레드 관리 필요

여기서 중요한 문장이 하나 있다.

스레드는 기본적으로 실행 순서를 보장하지 않는다.


즐겨찾기 버튼을 마구 클릭하면 생기는 문제

예를 들어 즐겨찾기 버튼을 빠르게 클릭한다고 해보자.

  • 클릭할 때마다 Thread 생성
  • 네트워크 요청 시작
  • 완료 시점은 제각각

이 상태에서 스레드들이 서로 순서를 보장하지 않으면,

  • 마지막 클릭이 먼저 끝날 수도 있고
  • 이전 클릭이 나중에 반영될 수도 있다

이게 바로 레이스 컨디션이다.

그래서 필요한 게 큐(queue)다.

  • 요청은 순서대로 쌓고
  • 하나씩 처리

HandlerThread는 이 구조를 자연스럽게 제공한다.


HandlerThread의 한계

하지만 HandlerThread는 여전히 단점이 많다.

  • Looper.loop()는 무한 루프라 직접 종료해야 함
  • Looper.quit()는 다른 스레드에서 호출해야 함
  • 생명주기 관리가 번거로움
  • 실수하면 메모리 릭으로 직행

그래서 다음 단계로 등장한 것이 AsyncTask다.


AsyncTask: 안드로이드가 제공한 간편한 비동기 도구

AsyncTask는 Thread보다 안드로이드 개발자에게 친숙했다.

이유는 간단하다.

  • 안드로이드 API
  • 사용법이 쉬움
  • UI 스레드와의 연결이 자동

즉, “스레드 + Handler + UI 전환”을 한 번에 감싸준 도구였다.


하지만 AsyncTask의 치명적인 문제

가장 큰 문제는 액티비티 생명주기를 따라가지 않는다는 점이다.

  • Activity는 종료됨
  • AsyncTask는 계속 실행 중
  • Activity를 참조하고 있다면?
  • → 메모리 누수

이 문제를 막기 위해 isCancelled()를 체크하고
onDestroy()에서 cancel을 호출하는 방식이 등장했지만,

이건 개발자에게 책임을 떠넘긴 설계에 가깝다.


AsyncTask의 또 다른 문제: 예외 처리

AsyncTask에서 예외 처리는 까다롭다.

  • try-catch 범위가 불분명
  • 에러 전달 구조가 명확하지 않음

이 문제를 해결하기 위해 RxJava가 주목받기 시작했다.

RxJava는

  • onNext
  • onError
  • onComplete

처럼 에러를 구조적으로 다룰 수 있었기 때문이다.


AsyncTask의 문제 2: 실행 순서 보장 불가

AsyncTask는 병렬 실행이 가능하다.
하지만 병렬 실행은 항상 위험을 동반한다.

예를 들어,

  • 개요 API
  • 상세 API

이 두 개가 순서대로 실행되어야 한다면?

병렬 실행에서는

  • 어떤 요청이 먼저 끝날지 보장할 수 없다
  • 순서를 가정하는 순간 버그가 된다

이건 운의 영역이다.

그래서 이런 경우에는 차라리

  • 병렬이 아니라
  • 순차 실행이 더 안전하다

실행 순서를 조정하기 위해
CountDownLatch 같은 동기화 도구가 등장했지만,
코드는 점점 복잡해졌다.


그래서 결론은 Coroutine이다

여기까지의 흐름을 정리하면 명확하다.

  • Thread: 너무 저수준
  • HandlerThread: 관리가 어려움
  • AsyncTask: 생명주기와 어긋남
  • RxJava: 강력하지만 학습 비용 큼

그래서 안드로이드는 결국 Coroutine을 표준으로 선택했다.

Coroutine은

  • Handler + Looper 위에서 동작하지만
  • 생명주기와 자연스럽게 결합되고
  • 순차 코드처럼 읽히며
  • 취소와 예외 처리가 명확하다
viewModelScope.launch {
    val overview = loadOverview()
    val detail = loadDetail(overview.id)
    updateUi(detail)
}

이 코드는

  • 순서를 보장하고
  • UI 스레드를 안전하게 사용하며
  • Activity 종료 시 자동으로 취소된다

마무리하며

HandlerThread와 AsyncTask는
안드로이드가 비동기 문제를 해결하기 위해 지나온 과정이다.

지금 우리가 Coroutine을 쓰는 이유는 단순히 “새로워서”가 아니다.

  • 스레드 관리
  • 생명주기
  • 순서 보장
  • 예외 처리

이 모든 문제를 현실적으로 가장 잘 해결한 도구이기 때문이다.

profile
안드로이드... 좋아하세요?

0개의 댓글