Android에서 NetworkOnMainThreadException 해결: 메인 스레드 네트워크 작업의 근본 원인과 백그라운드 이동

검증 환경

본 절은 원문 사례·문서에 등장한 버전/도구를 정리한 것이다. 별도 실험실 재현이 명시되지 않은 항목은 일반화하지 않는다.

항목본문·원문에서 확인된 범위
런타임/도구Android API 30
문서 정리일2026-08-27
  • OS/CI 세부 값은 프로젝트마다 다르므로, 적용 전 로컬에서 동일 오류 메시지를 재확인한다.

1. 문제 정의

다음은 RSS 피드를 SAX 파서로 읽는 전형적인 Android 코드입니다. url.openStream() 으로 네트워크 스트림을 여는 순간 android.os.NetworkOnMainThreadException 이 발생합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 문제가 발생하는 코드 (메인 스레드에서 직접 네트워크 호출)
URL url = new URL(urlToRssFeed);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
XMLReader xmlReader = parser.getXMLReader();
RssHandler theRSSHandler = new RssHandler();
xmlReader.setContentHandler(theRSSHandler);

InputSource is = new InputSource(url.openStream()); // ← 여기서 예외 발생
xmlReader.parse(is);

return theRSSHandler.getFeed();

실제 예외 로그는 단순합니다.

1
android.os.NetworkOnMainThreadException

문제 요약

  • 발생 단계: 런타임, url.openStream() 이 실제 네트워크 I/O를 시작하는 순간
  • 발생 조건: 메인(UI) 스레드에서 네트워크 작업을 실행
  • 표면 증상: 앱이 예외를 즉시 던지고, 해당 기능이 동작하지 않음

2. 표면 증상

먼저 관찰되는 증상을 정리하면 다음과 같습니다.

표면 증상관찰 포인트예상 방향
NetworkOnMainThreadException 발생예외 스택에 openStream() 호출네트워크 호출 위치를 스레드 밖으로
메인 스레드에서 위 broad code 실행onCreate / 이벤트 핸들러 안에서 호출백그라운드로 이동
RSS 피드가 파싱되지 않음SAX 파서가 올바른 InputStream 을 못 얻음스트림 획득을 별도 스레드로 분리

표면적으로는 “네트워크 코드가 실행된다"는 것만 보이지만, 이 예외는 단순 오류가 아니라 Android 프레임워크가 메인 스레드의 blocking I/O를 의도적으로 차단한 것임을 알 필요가 있습니다.


3. 원인 탐구: 왜 메인 스레드에서 네트워크 작업이 금지되는가

Android 는 UI를 담당하는 메인(UI) 스레드와 백그라운드 스레드를 구조적으로 분리합니다. 네트워크 I/O처럼 대기가 걸릴 수 있는 작업을 메인 스레드에서 직접 수행하면, 해당 시간 동안 UI(터치, 애니메이션, 응답)가 멈추고 앱이 ‘응답 없음(ANR)’ 상태로 빠질 수 있습니다.

이를 막기 위해 Android 런타임은 URL.openStream() 처럼 네트워크 진입점을 메인 스레드에서 호출했을 때 NetworkOnMainThreadException 을 던지도록 동작합니다. 즉 이 예외는 방어 장치(guard) 의 일종으로, 개발자가 네트워크 작업을 백그라운드 스레드로 보내도록 강제하는 신호입니다.


4. 근본 원인 분석

이 오류의 근본 원인은 두 개가 결합된 것입니다.

1) 네트워크 I/O가 메인 스레드에서 실행되었다 가장 직접적인 원인입니다. 같은 네트워크 코드라도 백그라운드 스레드에서 실행하면 예외가 발생하지 않습니다.

2) INTERNET 권한을 매니페스트에 선언하지 않은 경우 실행 환경에 따라 네트워크 동작에 필요한 권한이 없으면 스트림 획득이 실패할 수 있습니다. 이는 원래 예외의 직접 원인은 아니지만, 함께 확인하는 것이 안전합니다.

즉, 이 문제의 본질은 “코드가 틀렸다"가 아니라 **“코드가 실행되는 스레드가 잘못되었다”**는 것입니다. 같은 코드를 백그라운드 스레드로 옮기는 것이 근본 해결입니다.


원인 후보 매트릭스

원인 후보확인 방법해당 경우의 증상해결 방향
메인 스레드에서 네트워크 직접 호출콜 스택에 메인 스레드 이름 표시NetworkOnMainThreadException 즉시 발생백그라운드 스레드로 이동
INTERNET 권한 누락AndroidManifest.xml 확인네트워크 접근 실패 / SecurityException권한 선언 추가
AsyncTask deprecated (API 30+)빌드 경고 / API 참고컴파일은 되나 권장 안 됨Executor / ThreadPoolExecutor 사용

5. 코드 해결책

단계 A — 네트워크 코드를 백그라운드 스레드로 이동

AsyncTask 를 상속한 클래스를 만들고, 네트워크 I/O 코드를 doInBackground() 안에 넣습니다. 결과는 onPostExecute() 에서 UI 로 받습니다.

 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
class RetrieveFeedTask extends AsyncTask<String, Void, RSSFeed> {
    private Exception exception;

    @Override
    protected RSSFeed doInBackground(String... urls) {
        try {
            URL url = new URL(urls[0]);
            SAXParserFactory factory = SAXParserFactory.newInstance();
            SAXParser parser = factory.newSAXParser();
            XMLReader xmlReader = parser.getXMLReader();
            RssHandler theRSSHandler = new RssHandler();
            xmlReader.setContentHandler(theRSSHandler);
            InputSource is = new InputSource(url.openStream());
            xmlReader.parse(is);
            return theRSSHandler.getFeed();
        } catch (Exception e) {
            this.exception = e;
            return null;
        }
    }

    @Override
    protected void onPostExecute(RSSFeed feed) {
        // this.exception 확인
        // feed 로 UI 갱신
    }
}

실행은 MainActivity 의 onCreate() 안에서 한 줄로 처리합니다.

1
new RetrieveFeedTask().execute(urlToRssFeed);

단계 — AndroidManifest.xml 에 INTERNET 권한 추가

1
<uses-permission android:name="android.permission.INTERNET" />

단계 — API 30 이상: AsyncTask 대신 Executor 사용

AsyncTask 는 Android API 30에서 deprecate되었으므로, 최신 프로젝트에서는 ExecutorService 를 사용하는 것이 좋습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ExecutorService executor = Executors.newSingleThreadExecutor();

Future<RSSFeed> future = executor.submit(() -> {
    URL url = new URL(urlToRssFeed);
    SAXParserFactory factory = SAXParserFactory.newInstance();
    SAXParser parser = factory.newSAXParser();
    XMLReader xmlReader = parser.getXMLReader();
    RssHandler theRSSHandler = new RssHandler();
    xmlReader.setContentHandler(theRSSHandler);
    InputSource is = new InputSource(url.openStream());
    xmlReader.parse(is);
    return theRSSHandler.getFeed();
});
// 결과는 future.get() 으로 수신하고 UI 스레드로 전달

6. 검증 명령

해결 후 성공 여부를 확인하는 방법입니다. 환경에 따라 다를 수 있으므로 일반적인 점검 기준입니다.

1
2
3
4
5
# Android 프로젝트 빌드
./gradlew clean build        # 또는 mvn -q clean package

# 네트워크 스트림이 정상인지 (표준 HTTP) — 서버 측 점검
curl -sL -I https://example.com/rss.xml | head -n 5

앱 실행 후 RSS 피드가 예외 없이 파싱되고 UI가 블록되지 않는지 확인하면 해결이 검증됩니다.


7. 향후 예방 조치

이 예외가 다시 발생하지 않도록 하는 체크 리스트입니다. 환경에 따라 다를 수 있고, 일반적인 점검 기준입니다.

  • 메인 스레드에 네트워크 I/O를 넣지 않는다. UI 처리와 블로킹 I/O를 같은 스레드에서 수행하지 않습니다.
  • 콜백/이벤트 핸들러 안에서 직접 openStream() / get() 을 호출하지 않는다. 항상 백그라운드 작업으로 이동합니다.
  • INTERNET 권한을 AndroidManifest.xml 에 유지 합니다.
  • API 30+ 는 AsyncTask 대신 Executor 계열을 사용합니다.
  • 팀 규칙/리뷰 체크리스트에 “네트워크 작업은 반드시 백그라운드 스레드” 항목을 넣습니다.

잘못된 해결책 (추천하지 않는 접근)

잘못된 시도왜 안 되는가
예외를 무시하고 계속 실행메인 UI 스레드가 여전히 멈춰 ANR 위험
같은 코드를 같은 스레드에서 재실행같은 위치라 예외 재발
API 30+ 프로젝트에서 AsyncTask 강제 사용deprecated; 경고와 비호환성 발생

DevTrace verdict

이 문제의 핵심은 네트워크 자원이 없다거나 코드 자체의 로직이 틀렸다가 아니라, 네트워크 작업이 ‘어느 스레드’에서 실행되었는가 입니다. — 메인 스레드에서는 안 되고, 백그라운드 스레드로 이동하면 같은 코드가 정상 동작합니다.


출처:

DevTrace 결론

이 문제의 핵심은 네트워크 작업 자체가 아니라 ‘어느 스레드에서 실행했는가’이며, 메인 스레드를 피해 백그라운드 스레드로 이동하면 해결됩니다.

원문 출처는 문제 발견의 단서이며, 위 판단과 점검 항목은 DevTrace의 독자 분석이다.