Android threading

안드로이드 스레드와 백그라운드 태스크 이해하기: 실제 앱 예제와 함께

안드로이드에서는 메인 스레드를 차단하는 작업을 하면 안된다는 건 모두 아는 사실입니다. 하지만 이게 정말 무슨 뜻일까요? Bay Area Android Dev Group의 강연에서 Ari Lacenski가 오랫동안 작업하면서 복잡한 태스크를 처리하는 안드로이드 애플리케이션을 개발할 때 주의해야 할 점에 대해 알려드립니다. 실제 애플리케이션의 요구 사항을 살펴보고, AsyncTask, Activity, Service에 대해 논의한 뒤 보다 유지 관리가 용이한 솔루션을 구축하는 방법에 대해 말씀드립니다.


안드로이드의 스레드 (0:46)

스레드에 대해 논의할 때, 안드로이드 애플리케이션이 적어도 하나의 메인 스레드를 가지고 있다는 것을 모르는 분은 없을 겁니다. 이 스레드는 안드로이드 앱을 위한 Application 클래스가 생성될 때 함께 만들어집니다. 메인 스레드의 책임은 주로 UI를 그리는 것으로, 사용자의 상호 작용을 처리하고 화면에서 픽셀을 그리며 액티비티를 시작하기 위한 것입니다. Activity에 추가되는 코드는 UI 스레드가 끝난 이후 남아있는 스레드로 실행되므로, 앱은 항상 사용자의 활동에 반응할 수 있습니다.

아마 Activity 코드에 약간의 추가적인 코드를 넣는 것이 별 일 아니라고 생각할지 모릅니다. 예를 들어 리스트뷰가 있고, 리스트를 만들고, 새 아이템을 넣을 수 있죠. 리스트를 정렬해서 리스트 어댑터에 넘길 수도 있습니다. 프로세서는 여유 리소스를 사용해서 리스트를 그려줄 겁니다.

하지만 Activity에 추가하는 코드 중 일부는 너무 길어서 플랫폼이 UI 스레드에서 실행하도록 허용하지 않습니다. 일반적인 예로는 애플리케이션에서 네트워크 호출을 하는 경우입니다. 기본 URLConnection 인스턴스를 얻고 Activity 내에서 바로 네트워크 호출을 실행하려고 할 수도 있습니다. 이런 경우 정상적으로 컴파일되긴 하지만, 런타임에서 NetworkOnMainThreadException이 발생합니다. 하지만 사실 플랫폼은 UI 스레드에서 해야하는 일 이상으로 코드를 짜도록 허용하고 있습니다.

이보다 덜 극단적인 예를 들어 볼까요? 리스트뷰 상황으로 돌아가서 정적 목록을 만드는 대신 로컬 데이터베이스에 쿼리를 보내고 그 결과를 리스트에 넣는다고 가정해보겠습니다. Realm을 사용한다면 UI 스레드에서 이런 쿼리를 보내도 상관없지만, SQLite를 사용하는 경우라면 어떨까요? 쿼리를 만들기 위한 기나긴 코드가 필요하고, 데이터베이스 엔진에 접근해서 결과를 받고 이를 정렬해서 ArrayAdapter에 넣어야겠죠. 이 모든 것이 Activity에서 실행된다면 UI 인터렉션은 정말로 느려질 수 있습니다.

UI 스레드의 shared preferences를 한 번 읽어보고 문제가 없을거라 생각할지도 모릅니다. 어떤 일이 일어날 수 있을까요? 문제 자체가 복잡해집니다. 작업을 하면 할수록 UI 스레드를 인터럽트하거나 UI 스레드의 시간을 잡아먹게 됩니다. 애플리케이션이 작업을 건너뛰거나 지연되는 경향이 많아지며 애니메이션이 제대로 업데이트되지 않을 수도 있습니다. 사용자는 이 앱이 대체 뭐가 잘못된건지 궁금해할 것이고 최악의 경우 한참을 기다린 사용자에게 “애플리케이션이 응답하지 않습니다. 닫으시겠습니까?”라는 에러 메시지가 나타날지도 모릅니다. 개발자라면 큼직하게 “저를 때려주세요.” 라는 종이를 등에 붙이고 다니는 상황과 비슷하게 느낄 수도 있겠네요. 사용자는 앱이 신뢰할 수 없는건지, 아니면 자원을 많이 먹는건지 궁금해하면서 떠나버릴 겁니다.

”각 스레드는 [스레드] 실행 중에 메서드 로컬 변수와 매개 변수를 저장하는데 주로 사용되는 독립용 메모리 영역을 할당합니다. 독립용 메모리 영역은 스레드가 생성될 때 할당되고, 스레드가 종료되면 할당이 해제됩니다.”

  • Anders Göransson, Efficient Android Threading

제가 정말 좋아하는 인용문으로, 스레드가 무엇이며 어떤 일을 하는지, 또한 어떻게 메인 스레드 내 실행 대신 사용할 수 있는지에 대한 Efficient Android Threading이라는 책에 나오는 대목입니다. 또한 사용자가 만드는 스레드의 생명주기에 대한 책임을 환기시킵니다. 안드로이드는 이를 해결할 구조적 옵션을 많이 제공하지만, UI 스레드에서 작업하지 않더라도 애플리케이션의 어떤 스레드가 활성 상태인지 알아아합니다.

이런 개발 뉴스를 더 만나보세요

멀티스레딩을 사용하려면 스레드가 다른 스레드로 실행이 전환될 때마다 약간의 지연 시간이 발생한다는 것도 염두에 둬야 합니다. 이는 컨텍스트 스위칭이라고 불리며 더 복잡한 태스크 작업을 연결하고 개발할 때 살펴보게 됩니다.

AsyncTask (5:51)

이제 안드로이드 SDK가 자신의 스레드를 관리하는 일을 처리하는 도구 몇 가지에 대해 알아보겠습니다. AsyncTask부터 보는 것이 좋겠네요. Activity에서 사용하기 정말 편리하고 비동기 작업을 할 때 테스트하기도 좋습니다. AsyncTask 인스턴스를 만든 Activity에서 바로 사용할 수 있습니다. 이렇게 사용 할 수 있긴 하지만, 조금 후에 어떻게 하는 편이 나은지 다시 말씀드리겠습니다.

AsyncTask에는 두 가지 중요한 메서드가 있는데, 첫 번째는 doInBackground입니다. 이 메서드를 오버라이드해서 백그라운드 스레드에서 수행하고자하는 작업을 구현할 수 있습니다. 두 번째는 UI 스레드에서 다시 실행되는 onPostExecute로, doInBackground에서 수행한 결과를 다시 받을 수 있습니다. 조건이 왕복되는 것을 알려면 백그라운드에서 뭔가를 실행하고 이를 onPostExecute에 전달한 뒤 실행을 계속할 수 있습니다.

AsyncTask가 정말로 하는 일은 무엇일까요? AsyncTask의 코드를 보려면 클래스 이름을 컨트롤-클릭 합니다. 안드로이드 스레드 모델이 하는 일을 파악하는데 많은 도움이 되므로 AsyncTask 코드를 실제로 읽어보세요.

// AsyncTask.java
private static class InternalHandler extends Handler {
		@Override
		public void handleMessage(Message msg) {
			AsyncTaskResult result = (AsyncTaskResult) msg.obj;
			switch (msg.what) {
				case MESSAGE_POST_RESULT:
					// There is only one result
					result.mTask.finish(result.mData[0]);
					break;
				case MESSAGE_POST_PROGRESS:
					result.mTask.onProgressUpdate(result.mData);
					break;
			}
		}
}

이 클래스에서 가장 흥미로운 점은 핸들러의 인스턴스가 있다는 것입니다. 이전에 핸들러를 보지 못한 분도 계실텐데, 그 이유는 UI 코드에서 볼 일이 전혀 없기 때문입니다. 어떤 메서드를 작성하고 모든 메서드가 태스크를 완료하기 위해 서로 호출하는 것을 기대하는 코드를 작성하는 복잡함 대신 핸들러를 사용할 수 있습니다. 핸들러를 사용하면 handleMessage 메서드 내의 switch 문에서 참조된 코드를 가질 수 있습니다. 해당 핸들러 객체에 메시지를 보내면 해당 부분을 수행하도록 지시합니다.

AsyncTaskexecute가 호출될 때 스레드를 시작하는데, 개발자가 직접 해당 스레드와 상호작용할 필요 없이 doInBackground에 쓴 모든 것이 실행됩니다. 겉으로는 드러나지 않은 핸들러에 MESSAGE_POST_RESULT 메시지가 전송됩니다. 이 신호를 통해 AsyncTaskonPostExecute 메서드로 옮겨갑니다. 정말 편리하게 느껴지지만, 명심할 점은 매우 단순한 작업을 위한 것으로 작업이 시간이 지날수록 복잡해지면 안 된다는 것입니다.

방금 설명한 상황이 여러분이 필요로 하는 것과는 좀 다를 때를 생각해 보겠습니다. 순차적으로 실행해야할 두 개의 비동기 태스크가 있다고 해볼까요? 이미 AsyncTask로 백그라운드 스레드를 실행하고 있기 때문에 첫 번재 태스크를 시작한 다음 doInBackground에서 두 번째 태스크를 호출할 수 있습니다. 하지만 안드로이드 스레드 모델에서는 이런 형태를 허용하지 않기 때문에, 첫 번째 태스크가 완료될 때 두 번째 태스크를 큐에 넣는 방법을 시도해볼 수 있습니다. JavaScript에서 이런 콜백 체이닝을 자주 볼 수 있는데요. 아래와 같은 모습입니다.

new AsyncTask<Void, Void, Boolean>() {
	protected Boolean doInBackground(Void... params) {
		doOneThing();
		return null;
	}
	protected void onPostExecute(Boolean result) {
		AsyncTask doAnotherThing =
			new AsyncTask<Void, Void, Boolean>() {
			protected Boolean doInBackground(Void... params) {
				doYetAnotherThing();
				return null;
			}
			private void doYetAnotherThing() {}
		};
		doAnotherThing.execute();
	}
	private void doOneThing() {}
}.execute();

아직도 살짝 어리둥절한 상태로 이 코드가 읽기 어렵다고 생각하신다면, 그 느낌이 맞습니다. 정말 끔찍한 코드죠. 새 스레드를 시작하고 AsyncTask의 executer에게 첫 번째 태스크 실행을 위해 전체 스레드 생명 주기를 진행하도록 요청합니다. 바로 이 시점에서 다른 백그라운드 시작이 아닌 이유로 UI 스레드를 다시 차단하게 됩니다. UI와 스레드 풀 executer의 책임을 교차 배치하고 있지만 사실 그렇게 할 이유가 없습니다. 앞서 말한대로 이 때마다 연산 지연이 발생합니다. 게다가 코드를 유지하기 어렵게 만드는 부분이기도 하므로 이런 설계를 피해만 합니다.

이처럼 태스크가 복잡한 상황이라면 다른 해결 방법이 있습니다. 안드로이드 SDK는 여러 옵션을 제공하는데 이런 옵션을 사용하면 보통 직접 스레드를 시작하거나 유지할 필요가 없습니다.

IntentService (11:57)

IntentService를 사용하면 다른 스레드가 실행되고 안드로이드 환경을 유지 관리합니다. UI 스레드와 독립적으로 실행되며 심지어 애플리케이션이 맨 앞이 아닐 때도 실행될 수 있습니다. 특히 해당 스레드를 시작하고 닫는 것을 신경쓰지 않아도 됩니다.

모든 서비스는 액티비티에서 시작됩니다. AsyncTask 보다 더 무겁고 작업하기 어렵기 때문에 설정이 더 많이 필요하고 Manifest도 변경해야 합니다. 하지만 Intent를 시작하는 것의 장점은 Intent Bundle을 통해 데이터를 전달할 수 있다는 점입니다. 이 사실을 바탕으로 Activity와 백그라운드에서 실행하려고 하는 태스크 사이에 정보를 전달하는 방법을 말씀드리겠습니다.

액티비티와 서비스 사이에 신호 교환 (13:29)

android-threading-signal

실제 애플리케이션에서 이 방법을 배우려면 CodePath 가이드를 읽어보는 것이 좋습니다. 요약하자면 ActivityService를 시작합니다. ActivityService가 언제 완료되는지 알 수 있도록 Service와 통신할 수 있습니다. 그런 다음 피드백을 표시하고 ResultReceiver 객체를 만들고, 이를 Service에 넘겨서 Intent Bundle을 통해 시작할 수 있도록 준비합니다. 이로써 Service가 UI 코드를 실행할 객체의 참조를 가지게 됩니다. Service는 맡은 작업을 수행합니다. 이 과정의 가장 마지막 명령어는 onHandleIntent가 끝나고 스레드를 종료하기 전에 호출하는 rr.send입니다. 이 작업으로 RESULT 코드와 데이터 번들을 보낼 수 있으므로 rr.send를 호출하는 시점에서 Activity가 이를 받습니다.

// Activity
ResultReceiver rr = new
ResultReceiver() {
	@Override
	onReceiveResult() {
		// do stuff!
	}
}
// add rr to extras
startService(myServiceIntent);

// Service
onHandleIntent(Intent i) {
	ResultReceiver rr =
	i.getExtras().get(RR_KEY)
	(assigned from bundle)

	rr.send(Activity.RESULT_OK
	successData);
}

Task 통신 설계하기 (14:57)

점점 더 복잡한 태스크 설계에 대해 말씀드릴텐데요. 제가 Mango Health에서 했던 로그인 솔루션 작업을 예를 들어 보겠습니다. 저희 앱은 로그인하지 않고도 앱을 사용할 수 있습니다. 이후 사용자가 로컬 데이터베이스를 원격 데이터베이스와 동기화하고 싶을 때 로그인할 수 있는 옵션을 제공합니다. 원격으로 데이터를 동기화하는 기능을 지원하기 위해 로그인 프로세스가 상당히 무거워졌죠. 이런 로그인 프로세스 설계를 위해 다섯 가지 원칙을 정리했습니다.

  1. 모든 작업을 백그라운드 스레드에서 실행하는 것을 가장 중요하게 여겼습니다. UI에서 작업하는 것을 지양하고 UI의 책임을 단지 로딩 시의 스피너 표시와 사용자가 로그인했다고 알려주는 정도로 줄였습니다.
  2. 태스크는 로그인 성공이나 실패를 알려야 했습니다.
  3. 특정 순서로 실행하고 완료하기 전에 해야할 여러 가지 태스크가 있으므로 복잡할 것을 예상했습니다.
  4. 데이터베이스 싱크와 같은 몇몇 태스크는 비동기식이었습니다. 이 단계는 다른 모든 것들과 독립적으로 진행되는 것이 아니라 과정에 맞게 진행돼야 했습니다.
  5. 마지막으로 코드 작성을 마친 이후에도 가독성이 있어야 했습니다.

Login 태스크 구현하기 (16:45)

로그인 태스크를 만드는 것은 여러 단계가 필요했고 처음에는 작업을 실행하는 순서를 파악하기가 어려웠습니다. 의도된 피드백과 이들이 따르는 동기적 혹은 비동기적 패턴을 구분하는 것이 도움이 됐습니다. 다음 프로세스는 동기화여야 했기 때문에 아래 단계에 집중했습니다.

  • 네트워크 요청으로 인증 토근 받기
  • 네트워크 요청으로 사용자 계정 가져 오기
  • 원격 DB와 새 로컬 DB 동기화(push)
  • 새 데이터베이스 동기화(push)

이 네 가지 태스크는 플로우 프로세스에 적합해야 합니다. 또한 설계 1원칙에 따라 동기식인 몇몇 다른 태스크가 UI 스레드를 막지 않도록 해야 했습니다. 따라서 작업을 분리하고 작업을 메서드로 분리할 수 있는 것들을 자연스럽게 분리했습니다. 단, 어떤 태스크가 다른 태스크의 결과이거나, 다음 태스크에 필수적인 경우에는 분리하지 않았습니다. 마지막으로 특정 태스크가 Activity로 신호를 다시 보내는 것이 얼마나 중요한지 고려했습니다. 결과적으로 한 태스크가 프로세스 시작을 실패해서 사용자가 지속할 수 없거나, 모든 단계를 잘 완료하고 로그인 스피너가 돌거나 할테죠.

전체 태스크 목록은 두 가지 주요 원칙으로 분류됩니다. 첫째는 잠재적으로 완료되는 태스크를 더 작은 태스크로 분류하는 방법을 찾는 것입니다. 둘째는 작업 실패와 작업 성공을 다시 Activity로 전달하는 방법을 찾는 것입니다.

로그인된 상태 설정하기 (19:03)

android-threading-logged-in

이는 콜백 체이닝의 한 예일 뿐입니다. 한 세트의 연산을 서로 독립적으로 실행할 수 있기 때문에 콜백 체이닝은 한 세트 이후 다른 세트를 실행합니다. 하지만 한 태스크가 다음 태스크로 직접 이어지는 여러 개의 비동기적 태스크가 있는 경우 첫 번째를 먼저 시작해야 합니다. 예시의 경우 먼저 로그인 토큰을 서버로부터 얻는 것이 가장 먼저 실행돼야 합니다. 실패하면 바로 끝내야 하고요. 성공한다면 다음 비동기 태스크를 진행해야 합니다. 따라서 이 작업 체인을 끝내려면 순차대로, 하지만 비동기식으로 실행해야 하죠.

이 문제를 해결한 방법에 대해 말씀드리겠습니다. Service로부터 시작하지만 모듈 형식으로 실행될 수 있는 방식으로 구현하려고 했습니다. 서로 다른 일을 하고 있는 메서드가 잔뜩 있는 클래스를 만들었죠. 잘 동작하긴 했지만 코드가 너무 길고 방대해집니다. 이렇게 작성했을 때의 문제점은 어떻게 코드의 모든 부분이 협업하는지 기억하는 것이 너무나도 어렵다는 점이었습니다. 언제 이 메서드가 호출되고 왜 호출했는지 기억할 수가 없었습니다.

만약 제가 이런 코드를 이전 개발자에게 받았다면 그 클래스가 뭘하는지 짐작하기도 힘들었을겁니다. 로그인 매니저가 있지만 어떤 시점에서 로그인이 성공하는지 알 수가 없겠죠. 이러한 비동기 프로세스에 대한 콜백에서 다른 메서드에 대한 호출을 하는 다른 메서드가 또 있기 때문입니다.

메시징 핸들러 (21:58)

이제 더 나은 솔루션을 소개드리겠습니다. 다행히도 앞서 말씀드린대로 클래스를 만들 필요 없이 다른 옵션이 있습니다. 앞서 AsyncTask 실행을 돕기 위해 사용한 핸들러를 잠깐 봤었는데요. 핸들러를 사용하는 것이 이런 상황에서 더 좋은 솔루션이 될 수 있습니다. 핸들러로 이런 작업을 구현하려면 다시 세분화를 해야 합니다. handleMessage를 구현해서 각 상황을 다루도록 합니다. 코드의 한 부분이 끝나면 그 클래스에서 알고 있는 핸들러에 대한 참조를 가지고 다음 코드 부분을 실행하라는 메시지를 보냅니다.

// MyTaskModule.java
private static class LoginHandler extends Handler {
		MyCallback callback;

		@Override
		public void handleMessage(Message msg) {
				switch (msg.what) {
					case QUIT:
						callback.onFailure();
						break;
					case STARTED:
						doStuff();
						send(obtainMessage(NEXT_STEP));
						break;
					case NEXT_STEP:
						callback.onSuccess();
						break;
				}
			}
		}

프로세스 시작을 위해 핸들러에 시작 메시지를 보냅니다. 핸들러가 해당 코드를 실행하고 나면 NEXT_STEP가 호출돼고 해당 케이스로 이동됩니다. 또한 QUIT이라는 메시지를 직접 호출해서 끝낼 수도 있습니다. 이를 통해 핸들러 내부에서 벗어나 모든 것을 닫는 코드를 호출할 수 있습니다.

android-threading-mango-handler1

동작 과정을 설명하기 위해 MangoLoginHandler를 만들었습니다. 핸들러의 서브클래스인데요.

android-threading-mango-handler2

모든 작업은 handleMessage 안에서 수행됩니다. 원한다면 코드를 추가적으로 분리할 수도 있습니다.

android-threading-mango-handler3

코드의 맨 아래에 public 메서드 모듈이 있는데, 이 메서드는 LOOPER_STARTED 메시지를 보내서 Service가 코드를 실행하도록 합니다. 이 프로세스 작업을 위해 필요한 모든 데이터를 가져올 수 있습니다. 핸들러에 대한 참조를 얻고 post를 호출하면 핸들러는 설정한 스레드에서 시작됩니다. 여기서는 코드를 IntentService에서 시작하도록 했기 때문에 메인 스레드가 아닙니다. 모든 것이 잘 캡슐화돼서 백그라운드 스레드에서 동작하죠.

코드가 길어지면 길어질 수록, 각 메서드로 잘 분리하는 것이 중요합니다. 구현한 핸들러의 태그를 읽는 것만으로도 어떤 코드가 실제로 하고 있는 작업을 잘 이해할 수 있을 것 같지 않으신가요?

액티비티에서 서비스에서 모듈로 (25:57)

android-threading-module

앞서 말씀드린대로 ActivityService 사이의 통신을 만들고 태스크 모듈을 생성해서 핸들러를 내부에 넣은 후, Service에서 해당 모듈을 시작할 수 있게 됐습니다. 하지만 핸들러로부터 ServiceActivity로 다시 통신할 수 있는 방법을 아직 말씀드리지 않았습니다. 실제 작업은 모두 여기서 일어나게 됩니다. 이런 통신 방법이 필요하므로 실패나 성공 여부를 알 수 있어야 합니다.

태스크 모듈에 아주 간단한 인터페이스를 만들어서 이를 정의할 수 있습니다. 내부 인터페이스여도 되긴 하지만 꼭 public이어야 합니다. 해당 인터페이스의 인스턴스를 만들고 핸들러 오퍼레이션을 시작하기 위해 필요한 필요 사항이나 필요 인자 등을 만듭니다. 마지막으로 핸들러를 시작한 Service를 얻거나 태스크 모듈을 시작해서 콜백 인터페이스를 구현할 수 있습니다. 그 결과로 onSuccessonFailure 메서드를 가진 서비스를 만들게 되죠. 이 메서드는 해당 서비스에서 실행되는 태스크 모듈에서만 사용되도록 만들었습니다.

// MyTaskModule
public interface MyCallback() {
	public void onSuccess();
	public void onFailure();
}

// rest of task implementation
public void start(MyCallback callback) {
	// call onSuccess or
	// onFailure here!
}

Activity로부터 Service를 시작하고 ResultReceiver를 번들에 넘깁니다. Service 내에서는 먼저 해당 객체의 인스턴스가 되도록 콜백 인터페이스를 구현합니다. 태스크 모듈을 시작하면 Service를 전달해서 모듈에서 사용할 수 있도록 합니다. 모듈 안에서는 핸들러가 각 단계에 맞게 잘 실행된 경우 onSuccess를 호출합니다. onSuccess를 호출한 시점에서 Activity로부터 ResultReceiver를 사용해서 Service에 돌아옵니다. onSuccess가 실행되면 Activity에 OK라는 결과를 보낼 수 있습니다. 이제 로딩 스피너가 도는 것을 멈추고 로그인됩니다.

요약 (28:10)

android-threading-summary

여기까지 기본적인 네 객체를 만들었습니다. 먼저 사용자의 관점에서 제어를 관리하는 Activity가 있습니다. 또한 태스크를 관리하기 위해 설정한 Service에는 Activity와 커뮤니케이션하는 코드가 있습니다. 이런 코드를 Service 밖으로 가져와서 재사용이 가능하도록 별도의 모듈도 만들었습니다. 태스크 모듈에는 전체 프로시저를 처리할 수 있고, 사용자가 응답할 수 있도록 Activity에서 알아야할 것들을 전달하는 핸들러가 있습니다.

더 알아보기 (28:55)

이 주제를 공부하면서 유용했던 자료를 공유하겠습니다. 앞서 언급했던 CodePath 가이드(영문)를 다시 한 번 말씀드리고 싶고요. Efficient Android Threading이라는 책도 좋았습니다. 마지막으로 안드로이드 문서 중 프로세스 및 스레드 부분과 멀티플 스레드(영문)를 추천합니다.

Q&A (29:30)

Q: 비슷한 코드를 안드로이드 계정 관리자를 사용해서 만들 수 있나요?

Ari: 앞서 보여드린 코드에서 명확하지 않았는데, 이 질문을 받게 돼서 좋네요. 실제로 코드에서 Google 계정 관리자도 사용하고 있습니다. Google 계정처럼 기기에 사용자 계정을 등록하는 것이 제가 보여드린 코드가 하는 역할입니다. 실제로 계정 관리자 사용을 대체하려는 의도로 만든 코드는 아니지만 이 전체 프로세스의 일부로 계정 데이터를 계정 관리 시스템 부분에 작성할 수도 있습니다.

Q: Google 프리젠테이션에서 Sync Adapter를 사용해서 대기열에 넣고 책임을 처리하도록 하라는 내용을 봤는데 어떻게 고려해야 하나요?

Ari: 맞습니다. Sync Adapter를 사용할 때는 수행할 다른 작업에 적합하게 구현해야 한다고 생각합니다. 실제로 다른 작업을 수행할 필요가 없다면 Sync Adapter가 해야할 일을 네트워크 라이브러리를 사용해서 대신할 필요가 없습니다. Google 계정이나 기기를 원격에 동기화하는 작업처럼 Sync Adapter 호출로 할 일을 다 마칠 수 있는 경우라면 직접 호출을 할 것인지 래퍼를 사용할 것인지와 같은 설계 측면의 결정을 내려야 할 수 있습니다. 저의 경우 아주 간단한 래퍼를 사용하는 것을 좋아하는데요. Google 계정 데이터를 로컬에서 원격으로 보내는 작업이라면 이 방법이 적합하리라고 생각합니다,

Stephan: Sync Adapter 부분을 추가로 말씀드리자면 내부에 직접 컨텐트 프로바이더를 만들어야 하는 등 틀에 박힌 코드가 늘어나는 경우가 많습니다. 하지만 배터리 사용량을 줄이는데는 좋습니다. 할 일이 많아지긴 하지만 이 점에서는 좋을 수 있죠.

Q: UI 스레드로 신호를 다시 보내는 것을 좀 더 자세히 설명해 줄 수 있나요? 락을 건다던지 해야 하나요? 콜백이 어떻게 실제 액티비티를 업데이트하나요?

Ari: 제 코드의 경우 락을 사용하지 않았습니다. 기본적으로 해당 객체를 수정하는 것이 ResultReceiver에서 문제가 되지 않도록 했습니다. 두 가지를 확실히 했는데, 첫 번째는 프로그레스 스피너에서 상태를 변경했는데 사실 이는 스레드 세이프하지 않지만 실행이 UI 스레드로 넘겨지기 때문에 괜찮았습니다. Activity의 UI 스레드 메서드에 해당 변경을 요청하는 방식이었죠. 다음은 로그인 액티비티를 끝내는 것이었는데, 이는 UI 스레드에서 해도 안전했습니다. 해당 메서드에서 많은 것을 하지 않는 방법으로 문제가 일어나는 것을 막았죠. 아주 많은 것을 동기적으로 업데이트해야 한다면 이 방식이 걱정스러웠겠지만, 제 요구 사항을 최소화하는 방식으로 해결했습니다.

Q: EventBus를 사용해서 Activity를 시작한 구성 요소로부터 Service를 분리하는 방법을 고려했나요?

Ari: 솔직히 그러지 않았습니다. 다음 기회에 더 얘기할 수 있으면 좋겠네요.

Q: 왜 안드로이드는 Service, Sync Adapter, AsyncTask와 핸들러를 이런 방식으로 설계했을까요?

Stephan: 특히 휴대폰에는 팬이 없기 때문에 배터리가 주요 원인이었을 겁니다. 뭔가 실행하는 동안 휴대폰을 주머니에 넣으면 화상을 입을 수도 있겠죠. Linux나 PC에서는 메모리가 매우 저렴합니다. RAM도 풍부하고 환기팬도 있는데다 공간도 많으니 원하는 것은 마음대로 해도 되죠. 안드로이드라면 이럴 수가 없으니 많은 것이 배터리 위주로 돌아갑니다.

Suyash: 한 가지 재미있는 사실을 추가하자면 제가 Android를 시작했을 때 모든 것을 메인 스레드에서 하려고 했었습니다. Java나 JavaScript에서 서버 부문 프로그래밍을 해봤다면 멀티스레딩에 대해 그다지 고민한 적이 없었을지도 모릅니다. 하지만 메인 스레드에서 작업하지 않으려면 새 스레드를 다루는 방법을 배워야만 하죠. 안드로이드는 Services, AsyncTask, 핸들러와 함께 이런 시스템을 갖추고 있습니다. 기본적으로는 성능 상의 이유로 UI 스레드에서 모든 일이 발생하지 않아야 합니다. iOS에는 없는 AsyncTask라는 개념은 정말 독창적이고 놀랍다고 생각합니다.

Ari: 두 분의 답변으로 제 대답에도 도움이 되는 것 같습니다. 제 경험으로는 성능 문제가 발생하기 시작할 때 직관적이고 수동적인 학습 과정이 필요합니다. Java는 개발자를 돕는 많은 도구를 제공하고 있습니다. Stephan의 배터리 사용량과 성능 답변에 좀 더 추가하자면, 다른 스레드에서 작업을 실행하는 경우 새 스레드를 만드는 것은 아주 쉽습니다. 하지만 락 문제나 스레드에서 안전하지 않은 문제가 발생할 수 있죠. 이 프로세스의 생명 주기를 직접 관리하지 않는다면, 직접 지우기 전까지 안드로이드의 런타임에서 하염없이 돌아갈 겁니다. 따라서 이러한 개념을 이해하는 것이 어려운만큼 직접 어려움을 겪으면서 해결해보는 것이 바람직하다고 생각합니다.

Q: 사용자가 로그인하는 동안 기다려야 하는데요. 프로세스 실행을 처리하면서 사용자에게 응답할 방법이 있나요?

Ari: Mango 앱의 경우 이 문제를 툴바의 맨 위에 있는 가상 뒤로가기 버튼을 통해 해결했습니다. 사용자에게 대화 메시지가 나타나 뷰에서 좀 더 기다릴 것인지 물어보죠. 다른 대안은 백 버튼을 허용하면서 Activity를 삽입 상태로 하는 것입니다. 그런 다음 사용자에게 해당 액티비티로 이후에 다시 돌아갈 수 있음을 알립니다. 사용자는 이전 화면을 볼 수도 있지만 아직 전체 앱을 볼 수는 없겠죠. 모든 프로세스가 끝나면, 프로세스가 끝났고 몇 분 더 기다릴 수 있다는 피드백을 사용자에게 줍니다. 사용자가 홈 버튼을 누른다면 홈 화면으로 이동되지만 로그인 프로세스는 계속 작동합니다.

로그인 프로세스가 계속 동작하기를 원한다면 이런 절차들이 중요합니다. Service 모델의 장점은 이런 작업이 가능하다는 것입니다. 태스크의 우선 순위는 낮아지겠지만 끝나지는 않죠. 이 가능성 때문에 UI 스레드에서 나중에 수행할 수 있는 작업은 조심히 다뤄야 합니다. 광범위하지 않고 액티비티의 존재를 가정하지 않습니다. 또한 프로그레스 스피너가 아직 있으면 보여주는 작업과 같이 ResultReceiver에서 뭔가 할 수도 있습니다. 사실 좀 편법인 것 같으므로 보다 나은 솔루션을 찾아보고 싶긴 합니다.


다음: Realm Java를 사용하면 안드로이드 앱 모델 레이어를 효과적으로 작성할 수 있습니다.

General link arrow white

컨텐츠에 대하여

이 컨텐츠는 저자의 허가 하에 이곳에서 공유합니다.

Ari Laceski

4 design patterns for a RESTless mobile integration »

close