[android] 안드로이드 Handler탐구

OS간 경쟁이 치열해지면서 사용자의 경험을 향상시키지 않으면 외면받는 시대가 되었습니다(과거처럼 특정 OS밖에 없는게 아닌거죠)

이는 상대적으로 전환이 훨씬 쉬운 모바일 OS에서는 더욱 중요한 요소가 됩니다(어제까지 안드로이드 쓰다가 내일부터 아이폰을 쓴다던가 하는데 큰 전환비용을 느끼지 않는 사람도 많다는..)

심지어 윈도우즈 같은 운영체제도 타블렛을 지원하거나 머신파워가 약한 디바이스를 지원해가는 추세이므로 OS가 사용자 경험을 좋게 만들기 위한 노력은 계속될 뿐더러 범위도 점점 확장일로 입니다.

모던OS의 실행시간제한

그럼 OS에서 사용자 경험은 어떻게 향상될까요? 그것은 한마디로 답하기 어렵고 이 포스트의 주제에서도 벗어납니다만 한가지 짚어볼 사항은

사용자가 원할 때 작동하는 것도 중요하지만 일단 사용자의 반응에 즉각적인 변화가 일어나는 것도 중요하다

라는 것입니다. 인터렉션에 대한 응답성이라고도 합니다만 이를 확보하기 위한 OS의 전략은 일명 메인UI쓰레드에서만 뷰를 처리한다라는 정책입니다.

실은 이 정책은 브라우저에서는 매우 오래전부터 적용하던 원칙으로 코드상의 결함이 있든 없든 무조건 일정시간내에 실행되지 않으면 타임아웃으로 종료시켜버리는 기능입니다.

과거 브라우저에 이러한 자바스크립트 정책이 적용된데에는 서브시스템의 부하를 줄이고 머신파워를 아끼는 측면도 반영되어있었지만 현대에 와서는 일명 먹통이 되는 상태를 배제하기 위해 안전장치로서 역할을 수행하고 있는 셈입니다.

 

이러한 정책을 현대의 OS에서 충실히 반영하여 메인UI쓰레드-즉 뷰를 그리는 쓰레드에서 일정시간 이상 먹통을 만드는 상태가 지속되면 OS는 즉각 그 앱을 중지시켜버리는 과격한 정책을 내장하고 있으며 근본적으로 화면에 보이는 뷰의 컨트롤 자체를 다른 쓰레드에서는 못하도록 막고 있습니다.

 

위와 같은 제약조건에 의해 다음과 같은 결론이 도출됩니다.

  1. 메인UI쓰레드에서 실행시간이 긴 작업은 실행할 수 없으므로 별도의 쓰레드를 만들어 실행해야한다.
  2. 뷰를 메인UI쓰레드가 아닌 다른 쓰레드에서 직접 변경할 수 없다

1번이야 당연하다면 당연하고 쓰레드 만들어서 처리하면 된다치고 2번 문제는 어떻게 할까라고 생각하면 무조건 안된다라고 하면 개발을 할 수 없으니 별도의 방법을 열어준다가 답이겠죠.

이러한 구조를 OS별로 별도로 제공하고 있는데 안드로이드에서 제공하고 있는 시스템이 바로 Handler 시스템입니다. Looper나 Async는 핸들러를 래핑한 편의기능일뿐이고 핵심적인 코어는 핸들러인 셈이죠.

이 포스트는 핸들러의 사용법을 설명하는 것을 목표로하지 않습니다. 이미 핸들러를 쓸 줄 안다고 가정하고 그 의미를 조명하는데 의미를 두고 작성되었습니다.

핸들러의 일반적인 사용법은 구글문서에서 확인할 수 있습니다.

http://developer.android.com/reference/android/os/Handler.html

Producer, Consumer패턴

이러한 쓰레드 간 데이터 교환의 표준적인 방법은 생산자소비자 패턴을 사용하는 것입니다. 이 패턴은 잘 알려진 멀티쓰레드 패턴 중 하나로

  1. 핵심은 크리티컬섹션으로 보호되는 큐나 스택을 두고
  2. 생산자는 소비자나 소비자의 행위에 신경쓰지 않고 큐에 데이터를 쌓아주고
  3. 소비자는 생산자의 행위에 신경쓰지 않고 큐에 자료가 있으면 가져가는 형태의

패턴입니다. 1번에 보호되는 데이터임시보관 레이어를 보통 채널이라고 합니다. 그림으로 표현하면 다음과 같은 느낌이랄까..(요즘 만화그리고 싶은데 못그려서 이 포스팅에서 발산 중 ㅎㅎ)

[thumbnail src=”https://www.bsidesoft.com/wp-content/uploads/2014/10/ah2.jpg”]

이 패턴이 매우 쓸모 있는 이유는 다중 쓰레드 환경에서 안전하게 데이터를 생성, 교환할 수 있기 때문입니다. 자바로 표현하면 다음과 같습니다.

(흐름에 방해되는 try구문을 제거하고 읽기 쉬운 의사수준의 자바코드로 작성합니다)

//채널클래스는 동기화를 통해 _data의 접근을 제한한다.
class Channel{

	private Stack _data = new Stack();

	public syncronized void push( Object $v ){
		_data.push($v);
	}

	public syncronized Object pop(){
		return _data.pop();
	}
}

//생산자는 내리 쳐넣는다.
class Provider implements Runnable{

	private Channel channel;

	public Provider( Channel $channel ){
		channel = $channel;
	}

	public void run(){
		while(true){
			channel.push( new Object() );
			Thread.sleep(1000);
		}
	}
}

//소비자는 내 빼온다.
class Consumer implements Runnable{

	private Channel channel;

	public Consumer( Channel $channel ){
		channel = $channel;
	}

	public void run(){
		while(true){
			channel.pop();
			Thread.sleep(1000);
		}
	}
}

이런 구성 하에서 소비자와 생산자는 오직 채널만 바라보게 되고 멀티쓰레드 환경에서도 전혀 신경쓰지 않게 됩니다.

  • 이는 단지 한 개의 생산자, 소비자에 국한되는게 아니라
  • 다수의 생산자와 소비자를 등장시킬 수도 있고,
  • 또한 다채널구조를 가져갈 수도 있는 형태입니다.

핸들러와 메세지큐의 가상구현

이렇게 생각해보면 안드로이드OS가 최초에 실행해주는 메인UI쓰레드를 비롯하여 안드로이드OS위에서 생성된 쓰레드는 전부 메세지채널을 별도로 갖고 있다고 생각할 수 있습니다. 이를 코드로 표현해보면 다음과 같을 것입니다.

//가상의 OS전역 클래스가 있다고하자
class OS{

	//쓰레드를 키로 하는 메세지큐를 관리하는 해시맵을 만들자
	//채널구현이 귀찮으니 자바거 쓰자(ConcurrentLinkedQueue)
	static HashMap<Thread, ConcurrentLinkedQueue<Message>> messageQueue =
		new HashMap<Thread, ConcurrentLinkedQueue<Message>>();
}

(그렇습니다. 생산자소비자패턴의 채널부분은 태반 자바동시성패키지에 다 만들어져있습니다 ^^)

원래의 자바라면 쓰레드를 만들었다고 저런 메세지큐가 쌍으로 생길리가 없습니다만, 안드로이드상에서 실험해보면 아무 쓰레드나 만들어서 실행 중에 new Handler를 해보면 그 쓰레드 소속으로 send가 되는걸 확인할 수 있습니다. 그것은 곧 안드로이드의 Thread에는 메세지를 받을 수 있는 추가적인 장치가 들어있다고 생각해도 좋다는 것입니다(실제는 약간 다르지만 ^^)
핸들러는 현재 실행 중인 쓰레드의 메세지큐에 메세지 객체를 send하도록 디자인되어있으므로 상상력을 조금더 발휘해 Handler의 생성자를 구현해보면 다음과 같을 것입니다.

public class Handler{

	//메세지발송시 넣어줄 큐
	private ConcurrentLinkedQueue<Message> queue;

	public Handler(){
		//생성시점에 현재의 쓰레드를 이용해 메세지큐를 특정하자
		queue = OS.messageQueue.get(Thread.currentThread());
	}

	//메세지를 보낸다는것은 큐에 넣는다는 것이다.
	public void sendMessage( Message $msg ){
		queue.add($msg);
	}

	//나중에 메세지를 조사하면 핸들러를 알 수 있으니 그때의 콜백
	public void handleMessage( Message $msg ){}
}

생산자소비자패턴에 비추어보면 핸들러는 생산자의 역할을 수행하게 됩니다.
결국 이러한 메세지큐 채널을 갖고 있다면 OS차원에서 실행한 메인UI쓰레드는 다음과 같은 코드로 예상해볼 수 있습니다.

class MainUIThread extends Thread{

	//생성시점에 이미 메세지큐를 만들고
	private ConcurrentLinkedQueue<Message> queue =
		new ConcurrentLinkedQueue<Message>();

	public MainUIThread(){
		// 전역에도 등록한다.
		OS.messageQueue.put( this, queue );
	}

	public void run(){
		while(true){
			//뷰갱신, 라이프사이클갱신 등 원래 할 일들..

			//우선 큐에서 땡겨와보고
			Message msg = queue.poll();

			//null 이 아니라면
			if( msg != null ){

				//핸들러 얻어서 쏘자
				Handler hn = msg.getTarget();
				hn.handleMessage(msg);
			}
		}
	}
}

위의 구조에서 메인UI쓰레드는 지속적으로 큐를 뒤벼서 메세지가 있다면 해당 핸들러의 콜백(handleMessage)을 호출하게 됩니다.

이러한 가상의 구조는 실제로도 대동소이해서 핸들러를 통해 전달한 메세지는 라이프사이클과 무관하게 onCreate 에서 onFinish 사이클까지 살아만 있다면 지속적으로 비우고 실행하게 됩니다.

이렇게 되면 hn.handleMessage를 호출한게 메인UI쓰레드가 되어 뷰를 갱신하는 제약조건을 통과할 수 있게 되는 거죠.

쓰레드 당 핸들러 하나만 쓰기

위의 구조를 곰곰히 판단해보면 실행의 중심은 Message 입니다. 그 메세지를 받아서 처리하는 코드가 들어있는 쪽은 핸들러의 handleMessage 구상 쪽입니다.

여기까지가 일반적인 로직이지만 이를 더 깊이 생각해보면 그저 handleMessage 안에 알고리즘을 기술하기 위해서 다수의 핸들러를 만드는거지 실제로 알고리즘만 분리할 수 있다면 핸들러라는 객체는 쓰레드당 하나만 있어도 충분합니다. 그럼 핸들러에서 알고리즘을 처리하는 인터페이스를 간단히 분리하여 정의해보죠.

interface HandleAction{
	void act( Message $msg );
}

위의 구조를 이용하면 핸들러를 단일하게 두고 대신 HandleAction 을 늘려가는 방식을 선택할 수 있을 것입니다.
Message 에는 obj 속성이 있으므로 여기에 HandleAction 을 넣어줄 수 있습니다.

//유일한 핸들러를 만들자
Handler singleHn = new Handler(){
	public void handleMessage( Message $msg ){

		//실제 액터를 obj에서 꺼낸다
		HandleAction actor = (HandleAction)$msg.obj;

		//액터가 알고리즘을 처리한다.
		actor.act( $msg );
	}
}

위와 같이 핸들러를 구성하면 메세지를 보낼 때 obj에 액터를 넣어서 보내주면 됩니다.

Message msg = Message.obtain( singleHn );

//메세지에 액터를 넣어주자
msg.obj = new HandleAction(){
	public void act( Message $msg ){
		//여기에 알고리즘을 구축
	}
};

//쏴~
singleHn.sendMessage( msg );

이와 같이 어떤 경우에도 singleHn 하나만 갖고 통신을 실현하게 되고 대신 변화하는 각종 알고리즘은 HandleAction 이 가져가게 됩니다.

근데 여기서 매우 중요한 질문이 있죠.

짜피 여러개의 Handler를 만드나 HandleAction을 그 수만큼 만드나 같은거 아닌가요?

그쵸 같죠. 오히려 HandleAction 을 도입하면 관리해야하는 인터페이스와 singleHn 이 더 생기는 꼴입니다.

그러나 그럼에도 불구하고 저는 액티비티당 핸들러를 하나만 만드시길 강력히 권장합니다.

이유는…

핸들러의 취약한 메세지큐 바인딩

때문입니다.

앞에서 작성한 가상코드에서 핸들러는 핸들러의 생성자에서 현재 실행 중인 쓰레드를 이용해 메세지큐와 바인딩했습니다. 엄청 생략하고 이해를 돕기 위해 간단히 표현했지만 실제로 저와 비슷하게 바인딩됩니다. 즉 생성자에게 특정 쓰레드를 인자로 넘겨서 바인딩할 수도 없고 이를 실현해주는 상수같은 것도 없습니다.

오직 new할 때 그 코드를 실행한 쓰레드

의 큐에 바인딩됩니다. 따라서 코드는 그냥 코드지만 누가 실행했냐로 핸들러가 어떤 쓰레드의 메세지큐를 가리킬지 결정된다는 것입니다. 아래 핸들러를 생성해주는 코드를 생각해봅시다.

class Main extends Activity{

	public Handler getHandler(){
		return new Handler(){
			public void handleMessage( Message $msg ){}
		};
	}
}

그럼 Main.getHandler()를 통해 얻은 핸들러는 어떤 메세지큐를 가리키고 있을까요. 그건 전적으로 저 메서드를 호출하는 쓰레드가 누구냐에 달려있다는 것입니다.

같은 코드로 된 메서드 한 개가 반환하는 핸들러가 그때 그때 다릅니다. 이걸 감당하기란 무척 힘듭니다(핸들러 생성과정을 라이브러리화하는게 거의 불가능해지죠!)

따라서 안전하게 onCreate의 확정적으로 어떤 쓰레드인지 인지할 수 있는 시점에 단일한 핸들러를 생성해서 재활용하는게 다수의 메세지처리기를 구현하는데 훨씬 안전한 방법이라 생각됩니다(저만 그럴지도!)

결론

핸들러의 악몽은 여기서 끝나지 않습니다. UI를 처리하는 코드는 보통 대규모의 인자를 동원하거나 여러줄의 코드를 동원합니다.

이를 꼴랑 msg.obj에서 객체 하나 받은걸로 처리하려니 난처한게 이만저만이 아닌거죠. 예를들어 다음과 같습니다.

hn = new Handler(){
	public void handleMessage( Message $msg ){
		//레이아웃도 변경하고
		//이미지도 교체하고
		//텍스트도 바꾸고
		//버튼도 활성화하고..
		//....
	}
}

저런 끝없는 UI처리를 위해 $msg.obj 하나에 의존해서 처리하기란 여간 어렵습니다. 그러다보니 메세지를 여러개 만들고 여러개의 핸들러를 만들게 되죠.

이를 개선하려면 두가지 방법을 동시에 적용해야합니다.

  1. UI를 처리하는 함수군이 광범위하게 커맨드패턴화 된다.
  2. $msg.obj에 들어갈 객체는 인보커의 커맨드저장소 규모의 객체로 한다

즉 간단히 말해 커맨드패턴기반으로 많은 UI처리함수를 이전시키고 이러한 커맨드를 담는 그릇을 만들어 이를 obj에 태워보내면 handleMessage는 일종의 인보커로 작동하게 된다는 것입니다.
이는 앞에서 싱글핸들러와 맥략을 같이 합니다. 쓰레드당 인보커를 하나 두고 메세지 자체가 필요한 커맨드만 매번 갈아치우고 보내주면 인보커가 처리해주는 형태가 되는 것이죠.

이에 대한 자세한 예와 코드는…이만 체력이 딸려서 나중에 다른 글로 도전해보겠습니다.

(요즘 만화그리고 싶은데 못그려서 이 포스팅에서 발산 중 ㅎㅎ…2)
[thumbnail src=”https://www.bsidesoft.com/wp-content/uploads/2014/10/ah.jpg”]

%d 블로거가 이것을 좋아합니다: