※ HandlerThread

: HandlerThread에 대한 기본적인 내용은 해당 문장 URL을 참고하길 바란다. HandlerThread 클래스는 개별적인 Looper을 가지는 클래스 Thread를 상속받는다. Handler는 HandlerThread 외부에서 HandlerThread의 Looper와 연결한다. HandlerThread에 대한 사용법은 이 포스트 첫 문장 URL에 있다. 다음은 HandlerThread 내부 코드이다.

public class HandlerThread extends Thread{
	Looper mLooper;
	
	@Override 
	public void run(){
		Looper.prepare();
		synchronized (this) {
			mLooper = Looper.myLooper();
			notifyAll();
		}
		Looper.loop();
	}
	
	public Looper getLooper(){
		if(!isAlive()){
			return null;
		}
		synchronized(this){
			while(isAlive() && mLooper == null){
				try{
					wait();
				}catch(InterruptedException e){}
			}
		}
		return mLooper;
	}
	
	public boolean quit(){
		Looper looper = getLooper();
		if(looper != null){
			looper.quit();
			return true;
		}
		return false;
	}
}

먼저 getLooper 메서드를 보자. 우리는 HandlerThread의 Looper에 접근할 때 멤버 변수로 접근하지 않고 getLooper()를 통해 접근한다. 위의 quit() 메서드도 getLooper()을 사용했다. 왜냐하면 Looper에 접근할 때 추가적으로 해야할 처리가 있기 때문이다. getLooper의 처음을 보면 isAlive() 메서드로 HandlerThread.start()를 호출했는 지 확인하고 null을 반환한다. 따라서 우리는 getLooper()을 호출하기 전에 꼭 start()를 호출해야 한다. mLooper == null 부분은 위에 run() 메서드를 보면 알 수 있다.  run 메서드에서 Looper을 준비하고 Looper.loop를 통해 Looper가 동작하도록 한다. 또한 mLooper 변수에 Looper을 대입한다. 여기서 HandlerThread.start를 호출하고 나서 run이 언제 수행될 지 확실치 않다. 따라서 run에서 mLooper 변수를 언제 초기화될 지 모르므로 mLooper==null 조건문이 있다. 만약에 mLooper가 null 이라면 wait로 run() 메서드에서 mLooper가 초기화 될때까지 기다린다. run 메서드에서 mLooper의 초기화가 완료되면 notifyAll() 메서드로 기다리고 있는 getLooper 메서드를 깨운다.


- 순차적인 백그라운드 작업이 필요하다면 HandlerThread를 사용해라

: 예를 들어 게시물에 "좋아요" 버튼이 있다고 해보자. "좋아요" 체크 버튼을 선택/해제 할 때마다 스레드를 생성하거나 기존의 스레드를 이용해 DB에 반영한다고 가정해보자. 스레드는 start()한 순서대로 작업이 실행된다는 보장이 없으므로 선택->해제->선택 순서로 클릭을 해도 DB에 반영할 때 선택->선택->해제가 될 수도 있다. 즉, 결과가 바뀌는 것이다. 이 때 HandlerThread를 사용하는 걸 권장한다. HandlerThread는 순차적으로 작업을 하기 때문이다. 



※ 스레드 풀

: 백그라운드에서 해야할 작업이 많다면 스레드 풀을 사용하길 권장한다.


- ThreadPoolExecutor 클래스

ThreadPoolExector(int corePoolSize, int maximumPoolSize,
	long keepAliveTime, TimeUnit unit,
	BlockingQueue<Runnable> workQueue,
	RejectedExecutionHandler handler)

ThreadPoolExecutor의 생성자다. corePoolSize는 풀에서 스레드 기본 개수, maximumPoolSize는 스레드 최대 개수를 나타낸다. 스레드는 execute()나 submit() 메서드를 호출하는 순간 생긴다. 처음부터 corePoolSize 개수의 스레드를 유지하지 않는다. 그러다가 corePoolSize를 넘는 스레드가 생기면 스레드 작업이 끝날 시 스레드를 유지하지 않고 corePoolSize만큼의 스레드가 되도록 스레드를 버린다. keepAliveTime은 스레드의 태스크가 종료될 때 바로 제거하지 않고 대기하는 시간이다. unit은 keepAliveTime 변수의 시간 타입이다. 예로 TimeUnit.SECONDS나 TimeUnit.MINUTES가 있다. 보통 스레드 풀에서는 스레드를 corePoolSize 개수만큼 유지하다가 추가로 요청이 들어오면 workQueue에 쌓는다. 이는 workQueue 종류마다 동작방식이 다르다. workQueue 인자로 들어갈 수 있는 것은 3가지다. ArrayBlockingQueue는 큐 개수에 제한이 있고 요청이 들어오면 일단 큐에 쌓다가 큐가 꽉 차면 maximumPoolSize가 될 때까지 스레드를 추가한다. LinkedBlockingQueue는 큐 개수에 제한이 없지만 별도로 큐 개수를 생성자를 통해서 제한할 수도 있다. SynchronousQueue는 요청을 큐에 쌓지 않고 준비된 스레드로 처리한다. 모든 스레드가 작업 중이면 maximumPoolSize까지 스레드를 생성해서 처리한다. handler 매개변수는 ThreadPoolExecutor가 정지되거나 maximumPoolSize+workQueue 개수를 초과 할 때 거부하는 방식이다. 4가지 인자값이 있다. ThreadPoolExecutor.AbortPolicy 는 런타임 예외를 발생시킨다. ThreadPoolExecutor.CallerRunsPolicy는 스레드를 생성하지 않고 태스크를 호출하는 스레드에서 바로 실행한다. ThreadPoolExecutor.DiscardPolicy 는 태스크가 제거된다. ThreadPoolExecutor.DiscardOldestPolicy는 workQueue에서 가장 오래된 태스크를 제거한다. 가장 쓸모있는 handler는 DiscardOldestPolicy이다. 예로 ListView에 웹에서 가져온 사진을 로드한다고 가정하자. 사용자가 ListView를 스크롤할 시 여러 사진을 웹에서 불러오게 되는 데 만약 스레드 풀이 꽉 찰시 하나의 태스크를 제거해야 한다. 이 때 현재 보이는 화면보다는 이미 지나간 화면의  태스크를 취소하는 게 좋을 것이다. 이런 동작을 DiscardOldestPolicy로 설정해줌으로써 할 수 있다.


- ScheduledThreadPoolExecutor 클래스

: 지연/반복 작업을 한다. 물론 Handler을 통해서도 가능하다. 화면 갱신할 때는 Handler을 사용하는 게 좋으나 DB 작업이나 네트워크 작업에서 지연/반복이 필요한 경우 ScheduledThreadPoolExecutor을 사용하면 좋다.


- Executors 클래스

: ThreadPoolExecutor와 ScheduledThreadPoolExecutor 클래스의 팩토리 메서드로 사용된다. newFixedThreadPool(int nThreads)는 nThreads 개수까지 스레드를 생성하고 workQueue는 크기 제한이 없다. new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()) 이다. newCachedThreadPool()은 필요할 때 스레드를 생성하는데 스레드 개수 제한이 없다. keepAliveTime이 60초로 이 때문에 Cached라고 한다. new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()) 이다. newSingleThreadExecutor()은 단일 스레드를 사용해서 순차적으로 처리한다. workQueue는 크기 제한이 없다. newScheduledThreadPool(int corePoolSize)는 corePoolSize 개수의 ScheduledThreadPoolExecutor을 만든다.


※ AsyncTask 클래스

: AsyncTask의 기본적인 내용을 보길 원한다면 해당 문장에 링크된 포스트를 읽기를 추천한다. AsyncTask는 백그라운드 스레드의 작업 결과를 UI 스레드에 전달해야할 때 사용해라. 즉, onPostExecute()를 오버라이드해야 할 때 써라. 그렇지 않으면 그냥 스레드를 사용하는 걸 권장한다. AsyncTask는 제네릭 클래스로 Params, Progress, Result 파라미터가 있다. 만약 이들 파라미터 값으로 Void가 들어간다면 Handler을 이용하는 걸 추천한다. 더 코드가 간단해진다. 


- 액티비티와 AsyncTask 종료 시점 불일치로 인한 문제

: 액티비티에서 AsyncTask가 백그라운드 작업 중인데 Back 키를 눌러 액티비티가 종료되었다고 해보자. 액티비티가 종료되더라도 AsyncTask는 정상적으로 작업을 하고 onPostExecute() 메서드도 호출이 된다. 단, 액티비티가 화면에 보이지 않기 때문에 불필요한 작업이 된다. 또한 장치 구성이 변경될 때도 문제가 된다. 예로 화면 회전이 일어났다고 가정해보자. 그러면 기존 액티비티는 종료되고 새로운 액티비티가 생긴다. 하지만 기존 액티비티에 AsyncTask가 실행 중이다면 화면 회전이 일어나도 계속 실행이 되고 기존 액티비티는 AsyncTask 때문에 메모리에서 제거가 되지 않는다. 새로 생긴 액티비티에서도 AsyncTask가 실행이 된다. 다시 화면 회전이 일어났다고 해보자. 똑같은 상황이 발생된다. AsyncTask가 작업 중이기에 액티비티는 사라지지 않고 메모리에 유지되 결국에는 OutOfMemoryError의 원인이 될 수 있다. 참고로, Fragment에서도 AsyncTask로 인해 문제가 발생할 수 있다. Fragment는 Back키가 눌러졌을 시 액티비티와 분리되 getContext()와 getActivity() 메서드가 null을 리턴한다. AsyncTask의 onPostExecute()에서 Context를 사용하게 되면 NullPointException이 발생한다. 따라서 onPostExecute() 메서드 시작 부분에 getContext()와 getActivity()가 null을 리턴하는 지 확인하는 게 좋다. 어차피 null을 리턴하면 화면상에 보이지 않을 때므로 아무 처리를 하지 않아도 된다. 위에서 본 것처럼 액티비티와 AsyncTask 종료 시점 불일치를 해결하기 위해서 적절한 시점에 AsyncTask 작업 취소를 해야 한다. AsyncTask의 작업 취소에 대해서 알아보자.


- AsyncTask 취소

: AsyncTask는 cancel(boolean) 메서드를 호출하면 mCancelled 변수를 true로 만들고 onPostExecute() 대신 onCancelled 메서드가 호출된다. 액티비티 종료 시점과 근접하게 AsyncTask를 종료하는 방법은 doInBackground() 곳곳에 isCancelled() 리턴값을 체크하고 AsyncTask 멤버 변수를 유지해 onDestroy()에서 AsyncTask.cancel() 메서드를 호출하는 것이다. 추가로 cancel의 매개변수 mayInterruptIfRunning은 doInBackground를 실행하는 스레드에 interrupt()를 실행할 지 여부를 나타낸다. 만약 true를 전달하면 sleep(), join(), wait()가 실행 중이면 InterruptedException이 발생한다. 또한 AsyncTask에서 웹상 데이터를 읽을 때 자주 사용하는 InputStream의 read() 메서드에서도 인터럽트 여부를 체크한다. 그런데 인터럽트 체크를 킷캣 이상에서 지원해 isCancelled() 리턴 값도 사용해야 한다. 또한 AsyncTask의 doInBackground에서 개인이 구현한 클래스의 메서드를 호출하는 경우가 있다. 이럴 때 isCancelled 메서드를 불가피하게 사용 못 할수도 있는 데 이 때 isInterrupted() 메서드로 인터럽트를 체크하면 유용하다. 

따라서 AsyncTask를 종료하는 가장 좋은 방법은 cancel(true)를 사용해 인터럽트 예외를 활용하되 주기적으로 isCancelled()로 취소 여부를 확인해야 한다. 물론 cancel(false)도 유용할 때가 있다. 백그라운드 작업을 계속 유지해 앱에서 반드시 필요한 데이터를 획득하고 싶을 때이다. 


- AsyncTask 예외 처리

: AsyncTask는 예외를 처리할 방법이 없다. 이에 대한 대안을 제시하려고 한다. 첫 번째 대안은 예외가 발생했을 시 null 값을 리턴하는 것이다. onPostExecute 메서드에서는 결과값이 null 인지 확인을 하고 null이면 예외 관련 메시지를 보여준다. 하지만 예외를 발생하지 않았지만 결과값으로 null을 리턴할 때는 문제가 생긴다. 이에 대한 대안으로 doInBackground에서 Boolean.TRUE나 Boolean.FALSE를 리턴한다. 예외가 발생하면 FALSE를 리턴한다. 작업 결과는 AsyncTask 클래스 내에 멤버 변수를 선언해 직접 접근한다. 


- AsyncTask 병렬 실행 시 순서 보장 방법

: AsyncTask는 버전이 올라가면서 병렬 실행에서 순차 실행으로 변경됬다. 하지만 대부분 병렬 실행을 기본으로 해서 개발한다. 병렬 실행으로 작업 결과를 순서에 따라 UI에 적용할 때가 많다. 예로 어떤 게시물을 보여줄 때 상세 데이터와 개요 데이터가 있는 데 이들의 요청 URL이 달라서 별도의 스레드를 생성해서 데이터를 웹에서 가져와야 한다고 하자. 그러면 상세 데이터 보다는 개요 데이터를 먼저 UI에 적용하는 게 맞을 것이다. 이를 구현하기 위해선 CountDownLatch를 사용해야 한다. CountDownLatch는 해당 문장에 연결된 URL 포스트의 뒷 부분에 정리되 있다. AsyncTask에서 CountDownLatch를 사용한 코드는 다음과 같다.

private CountDownLatch latch = new CountDownLatch(1);

private class AsyncTaskA extends AsyncTask<Void, Void, Void>{
	
	....
	
	@Override
	protected void onPostExecute(result){
		try{
			
		}catch(){
			
		}finally{
			latch.countDown();
		}
	}
}

private class AsyncTaskB extends AsyncTask<Void, Void, Void>{
	
	....
	
	@Override
	protected void doInBackground(parama){
		try{
			
		}catch(){
			
		}finally{
			try{
				latch.await();	
			}catch(){
				
			}
		}
	}
}

위 코드는 AsyncTaskA의 결과가 먼저 UI에 적용되고 AsyncTaskB의 결과가 적용된다.

: 앱이 실행되면, 즉 앱 프로세스가 시작되면 메인스레드가 생성된다. 메인 스레드에서는 액티비티/프래그먼트의 UI 변경만 하지 않는다. 액티비티/서비스/브로드캐스트 리시버/Application 의 생명 주기와 메서드 호출을 메인 스레드에서 실행한다. 안드로이드 애플리케이션에서 메인 스레드는 ActivityThread 클래스의 main 메서드이다. 이 메서드가 앱의 시작 지점이다. ActivityThread는 Thread를 상속하지 않은 클래스이며 모든 컴포넌트와 관련이 있다. ActivityThread의 main 메서드는 다음과 같다.

public static void main(String[] args){ .... Looper.prepareMainLooper(); // 메인 Loop를 준비한다. ActivityThread thread = new ActivityThread(); thread.attach(false); ... Looper.loop(); // loop 메서드 안에서 무한 반복문이 있어 main은 종료되지 않는다. ... }

위의 코드를 이해하기 위해 Looper에 대해서 간략히 보면(Looper에 대해서 안다고 가정한다. 모르면 이 문장의 URL 포스트를 읽는 걸 추천한다.) Looper.prepare()로 스레드 별로 다른 Looper를 생성한다. 메인 스레드의 메인 Looper의 경우 Looper.prepareMainLooper() 메서드를 통해 가져온다. 접근할 때는 Looper.getMainLooper()을 사용하면 된다. 이 메서드를 사용하면 어디서든지 메인 Looper에 접근 가능하다. Looper는 별도의 MessageQueue를 가진다. 메시지 큐는 ArrayBlockingQueue 보다는 LinkedBlockingQueue에 가깝다. 전자에 비해 후자는 개수 제한이 없고 삽입 속도가 빠르다. 대신 전자는 랜덤 접근이 가능하고 후자는 순차 접근을 해야 한다. 메시지큐는 후자에 속해 순차적으로 메시지가 실행된다. Looper.loop() 메서드 코드를 보자.

public static void loop(){
	final Looper me = myLooper();
	if(me==null){
		throw new RuntimeException("...");
	}
	final MessageQueue queue = me.Queue;
	for(;;){
		Message msg = queue.next();
		if(msg==null){
			return;
		}
		msg.target.dispatchMessage(msg);
		msg.recycle();
	}
}

public void quit(){
	mQueue.quit(false);
}

public void quitSafely(){
	mQueue.quit(true);
}

: queue.next()를 통해 메시지큐에서 메시지를 꺼낸다. 만약 메시지가 null 일시 무한루프를 멈춘다. 즉 이 때는 Looper가 종료될 때이다. msg.target.dispatchMessage를 통해서 메시지를 타겟 핸들러 인스턴스에 보낸다. quit() 메서드는 호출될 시 큐상에 아직 처리되지 않은 메시지를 다 제거한다. quitSafely()는 현재 시간보다 늦게 처리될 예정인 메시지를 제고하고 그 앞에 있는 메시지는 처리하고 종료한다. 단, API 레벨 18이상에서 사용 가능하다. 메시지 클래스는 what, arg1, arg2, obj, replyTo public 변수와 target, callback 등의 패키지 프라이빗 변수가 있다. 메시지를 생성할 때는 Message.obtain이나 Hander의 obtainMessage 메서드 사용을 추천한다. 오브젝트 풀에서 메세지 객체를 가져오기 때문이다. new Message()로 객체를 생성하면 자원이 낭비된다. 


※ Handler

: Handler는 메시지를 메시지 큐에 넣는 기능과 메시지 큐에서 전달된 메시지 객체를 처리하는 기능을 한다. Handler는 다음의 생성자들을 가진다.

Handler()
Handler(Handler.Callback callback)
Handler(Looper looper)
Handler(Looper looper, Handler.Callback callback)

위에서 1~3번째 생성자들은 내부에서 4번째 생성자를 호출한다. 따라서 핸들러는 무조건 Looper을 필요로 한다. 메인 스레드에서 핸들러를 생성할 시 자동으로 ActivityThread에서 생성한 메인 Looper가 연결된다. 그렇다면 백그라운드 스레드에서 핸들러를 생성한다면 Looper가 없을 시 RuntimeException이 발생한다. 이를 해결하려면 핸들러를 생성하기 전에 백그라운드 스레드에서 Looper.prepare()을 실행해서 Looper을 준비해야 한다. 

class LooperThread extends Thread{
	public Handler handler;
	
	public void run(){
		Looper.prepare();
		handler = new Handler(){
		public void HandleMessage(Message msg){
			//Message 처리
		};

		Looper.loop();
	}
}

위와 같이 백그라운드 스레드에서 핸들러를 사용하면 된다. Looper.loop() 내에는 무한 반복문이 있기에 스레드는 종료되지 않는다. 참고로 UI를 변경하기 위해서는 메인 스레드에 접근해야 하는데 이 때 메인 Looper와 연결된 핸들러에 접근해야 한다. 백그라운드에서 메인 Looper에 접근하는 좋은 방법은 Looper.getMainLooper() 메서드를 이용하는 것이다.

public void backThreadFunc(){
	new Handler(Looper.getMainLooper()).post(new Runnable(){
		public void run(){
			....
		}
	});
}

Looper.loop() 메서드 안에서 msg.target.dispatchMessage(msg)를 사용해서 메시지 처리를 하였다. dispatchMessage 메서드는 다음과 같다.

public void dispatchMessage(Message msg){
	if(msg.callback!=null){
		handleCallback(msg);
	}else{
		if(mCallback!=null){
			if(mCallback.handleMessage(msg)){
				return;
			}
		}
		handleMessage(msg);
	}
}

private static void handleCallback(Message message){
	message.callback.run();
}

callback은 Runnable 객체를 가리킨다. Runnable 객체가 없다면 handleMessage()를 호출해 메시지를 처리한다.


- Handler 용도

1. 백그라운드 스레드에서 네트워크나 DB 작업 후 UI 업데이트를 할 때 사용한다. AsyncTask의 onPostExecute 메서드도 내부적으로 Handler을 사용한다.

2. 메인 스레드 작업 예약할 때 사용된다. 간혹 Activity의 onCreate에서 하지 못하는 일이 있다. 대표적으로 소프트 키보드를 띄우는 것이다. 이 때 Handler을 사용해 Message를 보내면 된다. onCreate에서 onResume까지의 작업이 메인 Looper에서 메시지 하나를 꺼내면 이어서 실행되기 때문에 onCreate에서 보낸 메시지는 onResume 이후에 실행된다.

3. 반복해서 UI를 갱신할 때 사용한다. 핸들러의 Runnable객체 안에서 또는 handleMessage 메서드 안에서 postDelayed나 sendMessageDelayed를 호출하면 된다.

4. 시간 제한을 할 때 핸들러를 사용할 수 있다. 안드로이드 ANR을 판단할 때 핸들러를 사용한다. 다음은 몇 초 내에 백 키를 반복해서 누를 때만 앱이 종료되도록 하는 코드이다.

private boolean isBackOnce = false;

@Override
public void onBackPressed(){
	if(isBackOne){
		super.onBackPressed();
	}else{
		isBackPressedOnce = true;
		hander.postDelayed(timerTask, 5000);
	}
}	

private final Runnable timerTask = new Runnable(){
	@Override
	public void run(){
		isBackOne = false;
	}
};



+ Recent posts