※ 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의 결과가 적용된다.

+ Recent posts