들어가기 전
MockWebServer로 API 테스트를 하면서 NewtworkOnMainThreadException이 발생하였다.
이번 포스팅에서 Android Thread에 대해 살펴보면서, NetworkOnMainThreadException가 무엇인지, 왜 발생한 것인지 등 새롭게 알게된 내용들을 정리하도록 하겠다.
NetworkOnMainThreadException 이란?
android.os.NetworkOnMainThreadException
해당 Exception은 Network 작업을 Main Thread에서 실행했을 때 발생하는 Exception이다.
그렇다면 안드로이드에서는 왜 Network 작업을 Main Thread에서 해서는 안될까?
질문에 답을 하기 위해서 Thread의 개념부터 Android의 Main Thread에 대해 설명하겠다.
Thread란?
Thread는 동시에 여러 작업을 수행하기 위해 사용되는 개념이다.
간략하게 프로그램이 실행되는 과정을 설명하면
OS가 프로그램을 메모리에 올리게 되고, 순차적으로 프로세스를 실행하게 된다.
- 이 때 Thread는 프로세스 내에 실행되는 각각의 독립적인 실행흐름으로 프로세스가 할당 받은 자원(힙 공간 등)을 이용하는 실행의 단위이다.
- 프로세스 내에는 두 개 이상의 스레드가 동작할 수 있고, 한 프로세스의 두 개 이상의 스레드가 동작하는 것을 멀티 스레딩 이라고 한다.
- 각 Thread들은 별도의 레지스터와 스택을 갖고 있다.
Android Thread
Thread에 대한 개념을 바탕으로 Android의 Thread에 대해 알아보자
안드로이드는 기본적으로 Single Thread 이다. Main Thread를 기본으로 동작한다고 볼 수 있다.
하지만 필요에 따라 Worker Thread를 생성해 동작할 수 있다.
Worker Thread
JVM(Java virtual machine)에서는 하나의 프로세스에 여러개 스레드를 허용하고 있다.
따라서 Main Thread 외에도 다른 Thread를 추가할 수 있고 이를 Worker Thread라고 한다.
Main Thread
Main Thread가 실행되는 시점은 어디일까?
이 Intent Filter로 Main, LAUNCHER로 설정한 부분부터 앱의 시작점을 만들고, 메인스레드가 만들어진다.
MainThread에는 2가지 규칙이 있다.
- UI 스레드를 차단하지 않습니다.
- UI 스레드 외부에서 Android UI 도구 키트에 액세스하지 마세요.
안드로이드에서 UI 작업은 Main Thread에서만 하고 있으며, UI 작업을 Main이 아닌 Worker Thread에서 진행할 시 에러가 발생한다.
Q. 왜 UI 작업을 Main Thread에서만 해야 할까?
결론부터 말하자면 Thread의 동기화 때문이다.
사진과 같은 UI가 있다고 가정해보자.
만약 UI 작업이 Main Thread에서만 이루어지지 않고, 여러 Thread에서 이루어진다고 한다면
버튼1을 만드는 A Thread
버튼2를 만드는 B Thread
회색 바탕을 만드는 C Thread
이렇게 동작할 수 있다.
C → B → A
순서로 순차적으로 Thread가 실행된다면 원하는 UI를 기대할 수 있지만.
만약 A → B → C
의 순서로 Thread가 실행되었다면?
버튼이 생성된 이후에 회색 배경이 생성되면서, 버튼이 보이지 않는 경우가 발생한다.
이렇게 되면 원하던 UI가 만들어지지 않으면서 사용자 경험을 해칠 수 있다.
또한 여러 개별 쓰레드에서 동시에 UI를 변경하려고 할 경우 (race condition) 충돌을 유발할 수 있다.
따라서 UI 작업을 Main Thread에서만 할 수 있도록 강제화하여 순차적인 상태 업데이트와, 일관성을 보장하도록 한다. → 그러므로 Worker Thread에서 UI 작업 시 에러가 발생하는 것이다.
UI 작업을 Main Thread에서만 진행하게 되면서 Android 에서는 Main Thread에서 긴 시간이 걸리는 작업을 지양하라고 명시해두었다.
Q. Main Thread에서 긴 시간이 걸리는 작업을 진행하게 된다면?
앞선 설명처럼 모든 UI 작업은 Main Thread에서 진행된다.
단일 Thread로 UI 작업을 순차적으로 진행하고 있다.
네트워크 작업과 같은 시간이 오래 걸리는 작업이 Main Thread에서 진행된다고 가정해보자.
Main Thread는 들어오는 작업들을 순차적으로 처리하고 있으므로
UI를 그리는 작업 이후 네트워크 작업이 존재하게되면 네트워크 작업이 끝날 때까지 다음 작업을 하지 못하게 된다.
즉 네트워크 작업이 Main Thread를 점유해 사용자의 입력 이벤트 등의 UI 작업을 전달하지 못하는 경우가 생기면 ANR( '애플리케이션이 제대로 작동하지 않음' 응답’)이 발생하게 된다.
그 외에도 서비스에서 20초 이상의 Main Thread를 점유하는 경우, Receiver에서 오래 걸리는 작업을 수행하는 경우 등 UI가 아닌 오랜 시간이 걸리는 작업으로 Main Thread를 점유하면 ANR이 발생한다.
이런 상황을 방지하기 위해서 Main Thread에서 긴 시간이 걸리는 작업을 금지하고 있고,
Network, DB 트랜잭션 (I/O 작업) 등 긴 시간이 걸리는 작업은 별도의 Thread에서 하도록 하고있다.
따라서 위 같은 이유로 금지시킨 Network 작업을 Main Thread에서 동작하려고 했기 때문에 NetworkOnMainThread Exception이 발생되었다.
문제 코드
@Provides
@Singleton
fun provideMockWebServer(): MockWebServer =
MockWebServer().apply {
enqueue(
MockResponse()
.setBody(NoticeRemote.notices)
.setResponseCode(200)
.setHeader("Content-Type", "application/json")
)
}
@Provides
@Singleton
fun provideRetrofit(moshi: Moshi, mockWebServer: MockWebServer): Retrofit = Retrofit.Builder()
.baseUrl(mockWebServer.url("/").toString())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
문제는 NetworkModule 클래스에서 발생하였다.
코드를 위처럼 작성하게 되면 NetworkOnMainThread Exception이 발생하였다.
그 이유는 MockWebServer의 start() 메서드가 Main Thread에서 실행되었기 때문이다.
MockWebServer는 임의 서버의 역할을 하기 때문에 @Provides로 제공되어서는 안된다.
프로덕션 실행 환경에서 MockWebServer가 실행되면서 메인스레드에서 네트워크 작업을 하게되는 것이다.
본래 MockWebServer는 테스트 환경에서 주로 사용하므로, 지금 같은 상황에서 실행하고 싶다면 Main Thread가 아닌 곳에서 실행되도록 해야한다.
수정 코드
@Provides
@Singleton
fun provideRetrofit(moshi: Moshi, mockWebServer: MockWebServer): Retrofit =
**runBlocking(Dispatchers.IO)** {
Retrofit.Builder()
.baseUrl(mockWebServer.url("/").toString())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
}
runBlocking(Dispatchers.IO)
를 통해서 Main Thread가 아닌 I/O Thread에서 MockWebServer의 start()가 실행되도록 하였다.
runBlocking(Dispatchers.IO)는 무엇을 의미하는걸까?
Dispatchers 란?
코틀린 코루틴에서 Dispatcher란 코루틴이 어떤 스레드나 스레드 풀에서 실행될지를 결정하는 것이다. 즉 어느 스레드에서 어떻게 실행될지를 관리하는 역할을 한다.
코루틴에서 제공하는 기본 디스패처들은 아래와 같다.
- Dispatchers.Main
- 안드로이드와 같은 UI 스레드에서 사용된다.
- Dispatchers.IO
- I/O 작업에 최적화된 디스패처이다.
- 많은 동시 작업을 지원할 수 있도록 스레드 풀 크기를 유동적으로 조절할 수 있다.
- Dispatchers.Default
- CPU 집중 작업에 최적화된 디스패처이다.
- 병렬 처리 성능이 중요한 논리 작업이나, 계산 수행할 때 적합하다.
- Dispatchers.Unconfined
- 어떤 특정한 스레드에서 실행될지 정하지 않고 유동적으로 결정하는 디스패처이다.
runBlocking 이란?
runBlocking은 코루틴을 생성하고, 해당 코루틴이 완료될 때까지 현재 스레드를 차단하는 함수이다.
블로킹 메서드이므로 UI 코드를 포함한 메인 스레드에서는 사용하지 않아야 한다.
runBlocking(Dispatchers.IO)
Dispatchers.IO로 I/O 작업을 위한 디스패처에서 현재 스레드를 차단하고 코루틴을 실행하겠다는 의미이다.
Main Thread에서 작업을 수행하는 것이 아닌 네트워크 작업을 별도의 스레드에서 작업하기 위해서 사용한 메서드이다.
이렇게 코드를 수정하면 더이상 네트워크 작업이 MainThread에서 실행되지 않아 Exception을 해결할 수 있다.
본 Exception을 해결하기 위해서 모호하게 알고있었던 지식들을 조금이나마 정리하게 되었고,
Thread 관리의 중요성을 알게 되었다.
Coroutine, Thread 를 확실하게 정리 할 필요성을 느껴 다음 게시물에서는 Coroutine과 Thread 비교, 그리고 Coroutine에 대해 자세히 남겨보도록 하겠다.
'Android > 트러블슈팅' 카테고리의 다른 글
[Android/트러블 슈팅] 푸시알림 매끄럽게 수신하기 : onNewIntent() (0) | 2024.10.08 |
---|---|
[Android/트러블슈팅] Kotlin 1.9.0 & Hilt Version Error (2) | 2024.09.27 |
[Android] WorkManager를 활용한 주기적인 백그라운드 작업 실행 (less than 15min ver) (0) | 2024.07.27 |
[Android] WorkManager를 활용해 주기적인 백그라운드 작업 실행하기 (15min ver) (0) | 2024.07.23 |
[Android/트러블슈팅] 구글 플레이스토어 키스토어 분실 재설정 방법 (2) | 2024.01.13 |