에러(error)

android.osNetworkOnMainThreadException

[ 에러발생 Logcat 화면 ]

에러 상세(detail)

2019-09-12 22:21:09.669 31487-31487/com.example.joker E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.joker, PID: 31487
android.os.NetworkOnMainThreadException
at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1448)
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:108)
at java.net.SocketOutputStream.write(SocketOutputStream.java:153)
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
at sun.nio.cs.StreamEncoder.implFlush(StreamEncoder.java:295)
at sun.nio.cs.StreamEncoder.flush(StreamEncoder.java:141)
at java.io.OutputStreamWriter.flush(OutputStreamWriter.java:229)
at java.io.BufferedWriter.flush(BufferedWriter.java:254)
at com.example.joker.SubThread.send(SubThread.java:48)
at com.example.joker.MainActivity.onClick(MainActivity.java:93)
at android.view.View.performClick(View.java:6897)
at android.widget.TextView.performClick(TextView.java:12693)
at android.view.View$PerformClick.run(View.java:26104)
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:6944)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)

 

 

개발환경(environment)

OS : windows 10

IDE : android studio 3.5

Device : samsung SM-G930S (갤럭시 S7)

 

 

상황(situation)

소켓(Socket) 통신을 사용하는 안드로이드 앱을 개발 중에, 앱을 종료시켜버리는 Fatal Exception이 발생했다. 에러가 발생한 시점은 소켓에 연결된 곳으로 데이터를 쓰려고(writing) 시도할 때 발생한 것이다.

 

 

원인(cause)

소켓(Socket) 통신을 사용해서 데이터를 쓰기(write)와 읽기(read)를 할 때 작업을 안드로이드 메인 스레드(main thread)와 분리시켜서 실행하지 않아서 발생한 것이다.


안드로이드 앱을 개발할 때 주의할 점 중 하나는 모든 작업을 메인 스레드(main thread)에서 처리하면 안된다는 것이다. 시간이 걸리는 작업들은 메인 스레드에서 분리시켜서 실행시켜야 한다. 만약, 메인 스레드를 오랫동안 잡고 놓지 않을 만한 어떤 작업(ex, socket, http 통신의 reading, writing)이 있다면, 안드로이드는 위 처럼 에러가 발생시킨다. 참고로 위 에러는 컴파일(compile) 중에 발생하는 에러가 아닌, 런타임(runtime) 에러이기 때문에, 앱을 실행하는 도중 발생할 수 있는 에러이다.
※ 안드로이드에서 메인 스레드(Main thread)에서만 UI작업을 할 수 있도록 제한했기 때문에, 메인 스레드를 UI 스레드(UI thread)라고 부르기도 한다.


안드로이드에서 말하는 메인 스레드를 이해하기 위해 아래 그림을 추가했다. 그림과 함께 두 가지만 이해한다면, 메인스레드를 고려해서 코딩하는 것이 어렵지 않을 것이라고 생각된다.

 

[ 안드로이드 메인 스레드 이해 ]


첫째, 메인 스레드(Main thread)에서 통신을 하지 않는다.
안드로이드에서 말하는 메인 스레드는 개발자가 따로 추가하지 않아도 안드로이드에 의해 앱에게 알아서 할당시켜주는 스레드를 말한다. 쉽게 말해서, 우리가 onCreate()이나 onResume() 안에 작성하고 있던 코드들은 모두 메인 스레드에서 동작하는 것이다. 이런 영역에서는 오래걸리는 작업을 하면 안되기 때문에, 통신을 해야하는 코드가 있다면 반드시 별도의 스레드로 작업을 분리시켜주어야 한다. 예를 들어, 데이터를 읽고 쓰기 위해 소켓(Socket)통신이나 HTTP 통신을 한다면 분리시켜주어야 한다. 그렇지 않으면 위 에러를 보게 될 것이다.


둘째, 메인이 아닌 다른 스레드(Sub threads)에서 UI 작업을 하지 않는다.
안드로이드 개발 중에 화면의 정보를 변경해야하는 작업이 있다면, 반드시 메인 스레드를 통해서 작업을 해야한다. 안드로이드에서 동작하는 모든 UI는 하나의 스레드에서 관리되도록 만들어져 있다. 따라서, 앱을 개발하다보면 코드가 많아질텐데, 아무리 코드가 많아진다고 하더라도 화면 정보인 UI를 변경하는 코드들을 보면, 모두 메인 스레드에서 처리하도록 코딩되어 있는 것을 볼 수 있다. 만약에, UI 작업을 메인 스레드가 아닌 곳에서 수행하려한다면 역시 (위 에러와는 다른) 에러가 발생할 것이다.

 

 

해결(solution)

개발 중인 안드로이드 코드의 소켓(Socket) 통신 내용 중에 데이터를 전달하는 부분을 별도의 스레드로 처리하도록 변경하면, 에러는 발생하지 않는다. 안드로이드에서는 별도의 스레드로 작업을 수행할 때 보통 Handler, AsynkTask, Thread 이렇게 3가지를 사용하게 된다.


java.lang.Thread
android.os.Handler
android.os.AsyncTask


아래 코드는 현재 개발 중인 코드에서 이번 에러(NetworkOnMainThreadException)를 발생시킨 부분이다. 메인 스레드에서 실행시킨 코드를 별도의 스레드를 만들어서 처리하도록 수정했다. 참고로, 작성자는 Thread를 사용했지만, 상황에 따라 Handler나 AsynTask를 사용해도 상관없다.

 

기존 코드

public class MainActivity extends Activity implements OnClickListener {

	SubThread subThread;
	...
    
	@Override
	public void onClick(View v) {
		if(v.getId()==R.id.btn_send){
			subThread.send(editText.getText().toString());
		}
	}
}

public class SubThread extends Thread {

	BufferedWriter bw;
	...

	public void send(String data){
		...
   		bw.write(data);
		bw.flush();
		...
    }
}

 

수정 코드

public class MainActivity extends Activity implements OnClickListener {

	SubThread subThread;
	...
    
	@Override
	public void onClick(View v) {
		if(v.getId()==R.id.btn_send){
			new Thread() {
				public void run() {
					subThread.send(editText.getText().toString());
				}
			}.start();
		}
	}
}

public class SubThread extends Thread {

	BufferedWriter bw;
	...

	public void send(String data){
		...
		bw.write(data);
		bw.flush();
		...
	}
}