(원문: http://developer.android.com/guide/components/bound-services.html)

(위치: Develop > API Guides > App Components > Services 
> Bound Services)

(전체 번역 글 목록으로 이동하기)


서비스 바인드하기
(Bound Services)


바인드 된 서비스는 서버-클라이언트 구조에서 서버에 해당합니다. 서비스는 액티비티와 같은 컴포넌트들이 바인드 한 후, 요청을 보내고, 응답을 받고, 프로세스간 통신(IPC)를 할 수 있도록 만들 수 있습니다. 바인드 된 서비스는 보통 다른 앱컴포넌트들에게 바인드 되어 있는 동안에만 살아있으므로, 후면(background)에서 계속 실행되는 것은 아닙니다.

본 문서에서는 바인드 되는 서비스를 만드는 방법과 다른 앱컴포넌트가 서비스를 어떻게 바인드하는지에 대해 학습할 것입니다. 서비스에 대한 전반적인 내용과 서비스에서 알림을 발송하는 방법, 그리고 서비스를 전면(foreground)에서 실행되도록 하는 방법 등은 서비스에 대하여에서 학습하실 수 있습니다.


기본 개념

바인드 되는 서비스는 Service 클래스를 상속받아 만들며, 다른 앱컴포넌트들이 그것을 바인드하여 상호작용할 수 있는 서비스를 말합니다. 서비스가 바인드 되기 위해서는 onBind() 콜백 메소드를 구현해야 합니다. 이 메소드는 클라이언트들이 서비스와 상호작용할 수 있는 인터페이스를 정의한 IBinder 객체를 리턴해야 합니다. 

클라이언트는 bindService()를 호출하여 서비스에 바인드할 수 있습니다. bindService()를 호출할 때 인자로 ServiceConnection의 구현객체를 전달하는데, 이것은 서비스와의 연결상태를 모니터링하는 역할을 합니다. bindService()를 호출하면 결과값 없이 바로 리턴되지만, 안드로이드 시스템에 의해 클라이언트와 서비스가 연결되면 ServiceConnection 구현객체의 onServiceConnected() 메소드가 호출되며, 여기에서 서비스와 통신할 수 있는 IBinder 객체를 전달 받습니다. 

여러개의 클라이언트들이 동시에 하나의 서비스를 바인드 할 수 있습니다. 하지만 시스템은 IBinder를 구하기 위해 처음에만 onBind() 콜백 메소드를 호출하며, 두번째 바인드 요청부터는 onBind()를 호출하지 않고 처음에 얻은 IBinder를 전달해 줍니다.

모든 클라이언트가 서비스에서 언바인드(unbind)되면, 시스템은 (startService()로 시작된 서비스가 아닌 경우에 한하여) 서비스를 종료합니다. 

바인드 되는 서비스를 구현할 때 가장 중요한 부분은 onBind() 콜백 메소드의 구현입니다. IBinder 인터페이스를 정의하는 방법은 몇가지 있는데, 이어지는 섹션들에서 그 각각의 방법들을 학습할 것입니다.


바인드 되는 서비스 만들기

바인드 되는 서비스를 만들기 위해서는, 클라이언트가 서비스와 상호작용할 수 있도록 해주는 IBinder 인터페이스를 서비스가 제공해줘야 합니다. 여기에 세가지 방법이 있습니다:

Binder 클래스 확장하기

내 앱 내에서만 사용하고, 같은 프로세스 내에 있는 클라이언트에게만 바인드 되는 서비스라면, onBind()에서 Binder 클래스를 확장한 객체를 리턴해주면 됩니다. 클라이언트는 그 Binder 객체를 받아서, Binder에서 구현한 public 메소드나 서비스의 public 메소드를 호출할 수 있습니다. 
만약 서비스가 내 앱만을 위한 것이라면 이 방법을 사용하는 것이 좋습니다. 만약 다른 앱이나 다른 프로세스에서 서비스를 사용하려고 한다면, 이 방법을 사용하면 안됩니다.

Messenger 사용하기

서로 다른 프로세스간에 통신을 해야하는 상황이라면, Messenger를 이용하여 서비스를 위한 인터페이스를 만들 수 있습니다. 이 방법에 따르면, 서비스는 몇가지 종류의 Messenger 객체에 응답하는 Handler를 정의합니다. 이 Handler 안에서 Messenger는 클라이언트와 IBinder 객체를 공유하고, 클라이언트가 Message 객체를 이용하여 서비스에게 명령을 보낼 수 있도록 해줍니다. 게다가, 클라이언트는 자신의 Messenger 객체를 정의할 수 있고, 그것을 서비스로부터 돌려받을 수 있습니다.
이 방법은 프로세스간 통신(IPC)를 하는 가장 간단한 방법입니다. 왜냐하면, Messenger는 하나의 쓰레드에서 모든 요청을 큐(queue)에 쌓아 차례대로 처리하기 때문에, 멀티쓰레드 문제(thread-safe)를 신경쓰지 않아도 되기 때문입니다.

AIDL 사용하기

AIDL (Android Interface Definition Language)은 객체들을 원시 데이터들(primitives)로 분리하는 일을 하는데, 이것은 OS가 IPC를 하기 위해 객체를 마샬링(marshall them)할 때 그 원시 데이터들을 사용하기 때문입니다. 위의 Messenger 사용하기 방법도 실제로는 AIDL에 기반하고 있습니다. 위에서 언급했듯이 Messenger는 하나의 쓰레드를 사용하기 때문에 서비스는 한번에 하나의 요청을 차례대로 처리합니다. 하지만 한번에 여러개의 요청을 동시에 처리하기를 원한다면, AIDL을 직접 사용하여 구현해야 할 것입니다. 이러한 경우, 서비스는 멀티쓰레드 문제(thread-safe)를 신경써야 합니다.
AIDL을 직접 사용하기 위해서는, 프로그래밍 인터페이스를 정의하는 파일인 .aidl 파일을 만들어야 합니다. 안드로이드 SDK 툴은 이 파일을 이용하여, 파일 안에 정의된 인터페이스를 구현하고 IPC를 다루는 추상 클래스를 하나 만들어내는데, 서비스 클래스 안에서 이것을 확장하여 실제 동작을 구현해야 합니다.

메모: 대부분의 경우, 바인드 되는 서비스를 만들기 위해 AIDL을 직접 사용하지 않는 것이 좋습니다. 왜냐하면, 멀티쓰레드 문제를 신경써야 하고, 구현하기도 더 복잡하기 때문입니다. 따라서 권장하는 방법이 아니기 때문에 본 문서에서는 AIDL의 사용방법에 대해 구체적으로 설명하지는 않겠습니다. AIDL에 대한 자세한 내용은 따로 AIDL 문서에서 학습하실 수 있습니다.


Binder 클래스 확장하기

만약 서비스가 내 앱 내에서만 사용되고 다른 프로세스에서 바인드 될 일이 없다면, 서비스를 위한 Binder 클래스를 구현하여 그 객체를 onBind()에서 리턴해주면 됩니다. 그 Binder 객체는 서비스의 public 메소드에 직접 접근할 수 있게 해줍니다.

메모: 이 방법은 클라이언트와 서비스가 같은 앱 및 같은 프로세스 내에 있을 경우에만 동작하는데, 아마도 대부분의 경우가 그러할 것입니다. 예를 들면, 후면(background)에서 음악을 재생하는 서비스와 그것을 바인드하는 액티비티로 구성된 음악앱의 경우에 이 방법을 사용할 수 있을 것입니다.

구현방법은 아래와 같습니다:

1. 서비스 안에, Binder 클래스의 구현 객체를 만듭니다.(아래 방법 중 한 가지 적용):

  • 클라이언트가 호출할 수 있는 public 메소드를 포함한 Binder 객체를 만듭니다.
  • 클라이언트가 호출할 수 있는 public 메소드를 가지고 있는 Service 객체를 리턴합니다.
  • 클라이언트가 호출할 수 있는 public 메소드를 가지고 있는, (서비스 내에 있는) 다른 객체를 리턴합니다.

2. onBind() 콜백 메소드에서 Binder 객체를 리턴해줍니다.

3. 클라이언트의 onServiceConnected()에서 Binder 객체를 받아서, 바인드 된 서비스와 통신할 수 있습니다.

메모: 서비스와 클라이언트가 같은 앱에 있어야 하는 이유는, 클라이언트가 전달받은 객체를 앱에서 구현한 Binder 클래스로 타입 캐스팅해야하기 때문입니다. 그리고 서비스와 클라이언트가 같은 프로세스에 있어야 하는 이유는, 이 방법이 객체 전달시 마샬링을 하지 않기 때문에 다른 프로세스로는 객체를 전달할 수 없기 때문입니다.

이 방법을 적용한 예제 코드는 아래와 같습니다:

public class LocalService extends Service {
   
// Binder given to clients
   
private final IBinder mBinder = new LocalBinder();
   
// Random number generator
   
private final Random mGenerator = new Random();

   
/**
     * Class used for the client Binder.  Because we know this service always
     * runs in the same process as its clients, we don't need to deal with IPC.
     */

   
public class LocalBinder extends Binder {
       
LocalService getService() {
           
// Return this instance of LocalService so clients can call public methods
           
return LocalService.this;
       
}
   
}

   
@Override
   
public IBinder onBind(Intent intent) {
       
return mBinder;
   
}

   
/** method for clients */
   
public int getRandomNumber() {
     
return mGenerator.nextInt(100);
   
}
}

LocalBinder는 클라이언트에게 서비스 객체를 주기 위해 getService() 메소드를 제공합니다. 클라이언트는 서비스 객체를 가져와서 그것의 public 메소드를 호출할 수 있습니다. 예를 들면, 클라이언트는 서비스의 getRandomNumber() 메소드를 호출할 수 있는 것입니다.

아래 예제 코드는 LocalService를 바인드하는 액티비티이며, 버튼을 클릭했을 때 getRandomNumber()를 호출하는 것을 보여줍니다:

public class BindingActivity extends Activity {
   
LocalService mService;
   
boolean mBound = false;

   
@Override
   
protected void onCreate(Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);
        setContentView
(R.layout.main);
   
}

   
@Override
   
protected void onStart() {
       
super.onStart();
       
// Bind to LocalService
       
Intent intent = new Intent(this, LocalService.class);
        bindService
(intent, mConnection, Context.BIND_AUTO_CREATE);
   
}

   
@Override
   
protected void onStop() {
       
super.onStop();
       
// Unbind from the service
       
if (mBound) {
            unbindService
(mConnection);
            mBound
= false;
       
}
   
}

   
/** Called when a button is clicked (the button in the layout file attaches to
      * this method with the android:onClick attribute) */

   
public void onButtonClick(View v) {
       
if (mBound) {
           
// Call a method from the LocalService.
           
// However, if this call were something that might hang, then this request should
           
// occur in a separate thread to avoid slowing down the activity performance.
           
int num = mService.getRandomNumber();
           
Toast.makeText(this, "number: " + num, Toast.LENGTH_SHORT).show();
       
}
   
}

   
/** Defines callbacks for service binding, passed to bindService() */
   
private ServiceConnection mConnection = new ServiceConnection() {

       
@Override
       
public void onServiceConnected(ComponentName className,
               
IBinder service) {
           
// We've bound to LocalService, cast the IBinder and get LocalService instance
           
LocalBinder binder = (LocalBinder) service;
            mService
= binder.getService();
            mBound
= true;
       
}

       
@Override
       
public void onServiceDisconnected(ComponentName arg0) {
            mBound
= false;
       
}
   
};
}

위의 예제는 클라이언트가 서비스를 바인드하기 위해 ServiceConnectiononServiceConnected() 콜백 메소드를 구현하는 방법을 보여줍니다. 

이와 관련된 다른 샘플 코드는 ApiDemos 샘플앱의 LocalServiceActivities.java 파일을 참고하시기 바랍니다.


Messenger 사용하기

서비스를 다른 프로세스와 통신할 수 있도록 만들기 위해서는, 서비스를 위한 인터페이스를 제공하는 Messenger를 사용할 수 있습니다. 이 기술은 AIDL을 직접 사용하지 않고도 프로세스간 통신(IPC)을 할 수 있도록 해줍니다.

Messenger를 사용하는 방법은 아래와 같습니다:

  • 서비스는 클라이언트의 호출 발생시 콜백을 수신하는 Handler를 구현합니다.
  • 이 Handler는 Messenger 객체를 생성할 때 생성자의 인자로 사용됩니다.
  • 서비스의 onBind()에서, Messenger 객체는 IBinder 객체를 생성하여 리턴합니다.
  • 클라이언트는 전달받은 IBinder 객체를 이용하여 Messenger 객체를 생성하며, 이 Messenger 객체를 사용하여 서비스에 Message 객체를 보낼 수 있습니다.
  • 서비스는 Handler의 handleMessage()를 통해 Message 객체를 전달받습니다.

이 방법에 따르면, 서비스는 클라이언트가 직접 호출할 "메소드"를 제공해주지 않습니다. 대신에, 클라이언트는 Message 객체들을 Messanger 객체를 통해 서비스에 전달하며, 서비스는 Handler를 통해 그것들을 받습니다.

아래는 Messenger 인터페이스를 사용하는 서비스의 예제 코드입니다:

public class MessengerService extends Service {
   
/** Command to the service to display a message */
   
static final int MSG_SAY_HELLO = 1;

   
/**
     * Handler of incoming messages from clients.
     */

   
class IncomingHandler extends Handler {
       
@Override
       
public void handleMessage(Message msg) {
           
switch (msg.what) {
               
case MSG_SAY_HELLO:
                   
Toast.makeText(getApplicationContext(), "hello!", Toast.LENGTH_SHORT).show();
                   
break;
               
default:
                   
super.handleMessage(msg);
           
}
       
}
   
}

   
/**
     * Target we publish for clients to send messages to IncomingHandler.
     */

   
final Messenger mMessenger = new Messenger(new IncomingHandler());

   
/**
     * When binding to the service, we return an interface to our messenger
     * for sending messages to the service.
     */

   
@Override
   
public IBinder onBind(Intent intent) {
       
Toast.makeText(getApplicationContext(), "binding", Toast.LENGTH_SHORT).show();
       
return mMessenger.getBinder();
   
}
}

위의 예제 코드에서 HandlerhandleMessage() 메소드가 클라이언트로부터 Message 객체들을 전달받는 곳이며, Message 객체의 what 멤버변수를 통해 어떤 요청인지 구분해야 한다는 것을 알아야 합니다.

클라이언트는 서비스로부터 전달받은 IBinder 객체를 이용하여 Messenger 객체를 생성하고, Messenger 객체의 send() 메소드를 호출하여 Message 객체를 전달할 수 있습니다. 아래 예제에서는 서비스를 바인드하고, MSG_SAY_HELLO 메시지를 전달하는 것을 보여줍니다:

public class ActivityMessenger extends Activity {
   
/** Messenger for communicating with the service. */
   
Messenger mService = null;

   
/** Flag indicating whether we have called bind on the service. */
   
boolean mBound;

   
/**
     * Class for interacting with the main interface of the service.
     */

   
private ServiceConnection mConnection = new ServiceConnection() {
       
public void onServiceConnected(ComponentName className, IBinder service) {
           
// This is called when the connection with the service has been
           
// established, giving us the object we can use to
           
// interact with the service.  We are communicating with the
           
// service using a Messenger, so here we get a client-side
           
// representation of that from the raw IBinder object.
            mService
= new Messenger(service);
            mBound
= true;
       
}

       
public void onServiceDisconnected(ComponentName className) {
           
// This is called when the connection with the service has been
           
// unexpectedly disconnected -- that is, its process crashed.
            mService
= null;
            mBound
= false;
       
}
   
};

   
public void sayHello(View v) {
       
if (!mBound) return;
       
// Create and send a message to the service, using a supported 'what' value
       
Message msg = Message.obtain(null, MessengerService.MSG_SAY_HELLO, 0, 0);
       
try {
            mService
.send(msg);
       
} catch (RemoteException e) {
            e
.printStackTrace();
       
}
   
}

   
@Override
   
protected void onCreate(Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);
        setContentView
(R.layout.main);
   
}

   
@Override
   
protected void onStart() {
       
super.onStart();
       
// Bind to the service
        bindService
(new Intent(this, MessengerService.class), mConnection,
           
Context.BIND_AUTO_CREATE);
   
}

   
@Override
   
protected void onStop() {
       
super.onStop();
       
// Unbind from the service
       
if (mBound) {
            unbindService
(mConnection);
            mBound
= false;
       
}
   
}
}

위의 예제는 서비스가 클라이언트에게 응답하는 방법을 보여주지는 않습니다. 서비스가 클라이언트에게 응답하도록 하기 위해서는, 클라이언트에서도 서비스에서처럼 Messenger 객체를 생성할 필요가 있습니다. 그런 다음에, 클라이언트의 onServiceConnected() 콜백 메소드가 호출 되었을 때, 서비스에게 보낼 Message 객체의 replyTo 멤버변수에 클라이언트의 Messenger 객체를 담아 보내고, 서비스에서는 전달받은 Message 객체의 replyTo가 클라이언트의 Messenger 객체이므로 이것의 send() 메소드를 호출함으로써 클라이언트에게 Message를 보낼 수 있습니다. 

양방향 메시징의 샘플 코드는 ApiDemos 샘플앱의 MessengerService.java(service)와 MessengerServiceActivities.java(client)를 참고하시기 바랍니다.


서비스를 바인드하기

앱컴포넌트들(클라이언트들)은 bindService()를 호출함으로써 서비스를 바인드할 수 있습니다. bindService()가 호출되면, 안드로이드 시스템은 서비스의 onBind() 메소드를 호출하며, 여기에서 서비스와 상호작용할 수 있는 IBinder 객체를 리턴합니다. 

바인딩 작업은 비동기적으로 동작합니다. bindService()는 메소드 호출 성공여부(boolean)를 즉시 리턴해주며, 클라이언트에게 IBinder 객체를 리턴해주지 않습니다. IBinder 객체를 받기 위해서는, 클라이언트가 ServiceConnection 구현 객체를 만들어서, bindService()를 호출할 때 인자로 넘겨줘야 합니다. 시스템은 ServiceConnection의 onServiceConnected() 콜백 메소드를 통해 IBinder 객체를 전달해 줍니다.

메모: 액티비티, 서비스, 컨텐트 프로바이더만이 서비스를 바인드할 수 있으며, 브로드캐스트 리시버는 서비스를 바인드할 수 없습니다.

따라서, 클라이언트가 서비스를 바인드하기 위해서는 아래와 같이 해야합니다:

1. ServiceConnection을 구현합니다.
   아래 두개의 콜백 메소드를 구현해야 합니다.
   onServiceConnected()
       시스템은 서비스의 onBind()에서 리턴한 IBinder를 전달해주기 
       위해 이 메소드를 호출합니다.
   onServiceDisconnected()
       안드로이드 시스템은 서비스가 크래쉬 되었거나 갑자기 종료되었을
       때 이 메소드를 호출하며, 클라이언트가 언바인드 했을 때는 호출
       하지 않습니다.

2. bindService()를 호출하며, 이때 ServiceConnection 구현 객체를 
    인자로 넘깁니다.

3. 시스템에 의해 onServiceConnected()가 호출되었을 때, 전달받은
    인터페이스에 정의된 메소드들을 이용하여 서비스의 기능을 사용할
    수 있습니다. 

4. 서비스와의 연결을 끊으려면, unbindService()를 호출합니다.
    클라이언트가 종료되면 자연히 서비스와 언바인드 될 것입니다.
    그러나, 클라이언트가 더이상 서비스를 호출할 일이 없거나,
    액티비티가 정지상태가 되는 경우와 같이 서비스가 더이상 사용되지
    않을때는 언바인드 해주는 것이 좋습니다.(바인드 및 언바인드 하는 
    적절한 위치에 대해서는 좀더 아래 부분에서 학습할 것입니다.)

아래 예제 코드는 위의 Binder 클래스 확장하기에서 학습한 코드의 일부입니다. onServiceConnected()에서는 전달받은 IBinder를 이용하여 LocalService 객체를 얻어냅니다:

LocalService mService;
private ServiceConnection mConnection = new ServiceConnection() {
   
// Called when the connection with the service is established
   
public void onServiceConnected(ComponentName className, IBinder service) {
       
// Because we have bound to an explicit
       
// service that is running in our own process, we can
       
// cast its IBinder to a concrete class and directly access it.
       
LocalBinder binder = (LocalBinder) service;
        mService
= binder.getService();
        mBound
= true;
   
}

   
// Called when the connection with the service disconnects unexpectedly
   
public void onServiceDisconnected(ComponentName className) {
       
Log.e(TAG, "onServiceDisconnected");
        mBound
= false;
   
}
};

클라이언트가 서비스를 바인드하기 위해 bindService()를 호출할 때, 위의 ServiceConnection 구현 객체를 인자로 넘깁니다. 예제 코드:

Intent intent = new Intent(this, LocalService.class);
bindService
(intent, mConnection, Context.BIND_AUTO_CREATE);
  • bindService()의 첫번째 인자는 바인드할 서비스를 가리키는 인텐트입니다.(명시적 인텐트 및 암묵적 인텐트 모두 사용 가능합니다.)
  • 두번째 인자는 ServiceConnection의 구현 객체입니다.
  • 세번째 인자는 바인딩 옵션 값입니다. 보통은 서비스가 생성되어 있지 않을때 자동으로 생성해 주도록 BIND_AUTO_CREATE를 전달합니다. 전달 가능한 다른 값들로는, BIND_DEBUG_UNBINDBIND_NOT_FOREGROUND, 그리고 0(옵션 없음)이 있습니다.


추가 사항들

여기에 서비스 바인딩에 대한 몇가지 중요한 사항들을 추가합니다:

  • 다른 프로세스에 있는 서비스와의 연결이 끊어졌을 때 DeadObjectException 예외가 발생할 수 있습니다. 이는 원격 메소드 호출시에만 발생할 수 있는 예외입니다.
  • 객체들은 프로세스를 가로질러 레퍼런스 카운트됩니다.
  • 바인딩과 언바인딩은 보통 짝을 이루며, 클라이언트의 생명주기와 관계가 있습니다. 예를 들면:
    • 만약 액티비티가 화면에 보여질 때만 상호작용하게 하려면, 액티비티의 onStart()에서 바인드하고, onStop()에서 언바인드하면 됩니다.
    • 만약 액티비티가 정지되어 후면(background)에 있는 상태에서도 상호작용하게 하려면, 액티비티의 onCreate()에서 바인드하고, onDestroy()에서 언바인드하면 됩니다. 하지만 이렇게 하면 액티비티가 종료될 때까지 계속 서비스를 사용하게 된다는 점에 대해 주의해야 합니다. 

메모: 액티비티의 onResume()onPause()에서 바인드 및 언바인드하는 것은 피하는 것이 좋습니다. 이것들은 액티비티의 생명주기에서 가장 자주 호출되는 콜백 메소드인데, 서비스의 바인드 및 언바인드는 가능한한 최소한으로 하는 것이 좋기 때문입니다. 만약 여러개의 액티비티가 하나의 서비스를 바인드하고 있는 상황에서, stopped 상태에 있던 액티비티가 현재 화면에 보여지고 있던 액티비티를 제치고 resumed 상태가 되는 경우를 예로 들어보겠습니다. stopped 상태였던 액티비티가 resumed 상태로 되면서 서비스를 바인드하기 전에, 현재의 액티비티가 paused 상태로 되면서 언바인드 한다면, 서비스는 종료되었다가 다시 생성될 것입니다. (액티비티의 생명주기 변화에 대한 자세한 내용은 액티비티에 대하여에서 학습하실 수 있습니다.)  


서비스를 바인드하는 방법에 대한 샘플코드는 ApiDemos 샘플앱이 RemoteService.java 파일을 참고하시기 바랍니다.


바인드 되는 서비스의 생명주기 관리하기

서비스가 모든 클라이언트들로부터 언바인드 되면, (onStartCommand()가 호출되면서 시작된 서비스가 아닌 경우에 한하여) 안드로이드 시스템은 그 서비스를 종료시킵니다. 따라서 순전히 바인드만 된 서비스라면, 서비스의 생명주기를 관리할 필요가 없습니다. 안드로이드 시스템이 서비스의 바인드 여부를 판단하여 알아서 관리해주기 때문입니다. 

하지만 다른 앱컴포넌트에서 startService()를 호출하여, 서비스의 onStartCommand()가 호출된 경우, 즉 시작된(started) 서비스의 경우에는, 명시적으로 서비스를 종료시켜줘야 합니다. 이러한 경우에는 서비스가 클라이언트들에게 바인드 되었는지 여부와 상관없이, 서비스가 자체적으로 stopSelf()를 호출하거나 다른 앱컴포넌트가 stopService()를 호출할 때까지, 서비스는 실행됩니다. 

그리고, 서비스가 시작 및 바인드 되었다가 언바인드 되면서 onUnbind()가 호출되었을때, 여기서 true를 리턴하면, 다음번에 서비스가 다시 클라이언트에게 바인드될 때 onBind()가 호출되지 않고 onRebind()가 호출됩니다. onRebind()는 결과값을 리턴하지 않는 메소드지만, 클라이언트는 onServiceConnected()에서 (기존의) IBinder객체를 전달 받습니다. 아래의 그림1은 이러한 생명주기 흐름을 나타냅니다.


그림 1. 시작되고 바인드 되는 서비스의 생명주기


시작되는(started) 서비스의 생명주기에 대한 자세한 내용은 서비스에 대하여에서 학습하실 수 있습니다.


Posted by 개발자 김태우
,

(원문: 

http://developer.android.com/guide/components/services.html)

(위치: Develop > API Guides > App Components > Services)

(전체 번역 글 목록으로 이동하기)


서비스에 대하여 (Services)


서비스(Service)는 사용자 인터페이스(UI)를 제공하지 않고, 백그라운드에서 주로 시간이 오래 걸리는 작업들을 처리하는 앱 컴포넌트입니다. 다른 컴포넌트가 서비스를 실행할 수 있고, 사용자가 내 앱에서 벗어나 다른 앱으로 이동하더라도 서비스는 백그라운드에서 하던 일을 마저 할 것입니다. 그리고, 컴포넌트는 서비스와 상호작용을 하기 위해 바인드할 수 있고, 심지어 프로세스간 통신(IPC)을 할 수 있습니다. 서비스를 이용해서 할 수 있는 일들을 예로 들면, 네트워크를 통한 데이터 전송 및 수신, 음악 재생, 파일 I/O, 컨텐트 프로바이더와의 상호작용 등을 백그라운드에서 처리할 수 있습니다. 

서비스는 실행되는 방식에 따라 2가지 상태로 나뉩니다:

Started
(액티비티와 같은) 앱 컴포넌트가 startService()를 호출하면, 서비스는 "started" 상태가 됩니다. 서비스가 실행되면(started), 그 서비스를 실행한 컴포넌트가 종료되더라도 (할 일을 모두 마칠 때까지) 서비스는 종료되지 않습니다. 서비스는 별도로 실행되기 때문에 서비스를 실행한 컴포넌트에게 실행한 것에 대한 결과값을 리턴해 주지 않습니다. 네트워크 상에서 파일을 다운로드하거나 업로드 하는 경우, 작업이 끝나면 서비스는 스스로 종료되어야 합니다.

Bound
앱 컴포넌트가 bindService()를 호출하면, 서비스는 "bound" 상태(바인드 된 상태)가 됩니다. 바인드 된 서비스는, 앱 컴포넌트들이 서비스와 상호작용(요청 및 응답, 프로세스간 통신(IPC)) 할 수 있도록 해주는 클라이언트-서버 인터페이스를 제공해 줍니다. 서비스는 서비스가 아닌 다른 앱 컴포넌트에 의해 바인드 된 경우에 bound 상태로 동작합니다. 한번에 여러개의 컴포넌트가 바인드 될 수 있고, 바인드된 모든 컴포넌트가 언바인드 되면 서비스도 종료됩니다.

서비스의 상태를 위와 같이 둘로 구분해서 설명하고 있기는 하지만, 서비스는 Started 상태이면서 Bound 상태가 될 수 있습니다. Started 상태가 되는 경우 onStartCommand() 콜백 메소드가 호출되고, Bound 상태가 되는 경우 onBind() 콜백 메소드가 호출됩니다.

내 앱이 실행중인지 여부나, 서비스와 바인드 되어 있는지 여부와 상관없이, 내 앱 및 다른 앱의 컴포넌트에서 인텐트를 사용하여 서비스를 실행할 수 있습니다. 이는 액티비티가 내 앱 및 다른 앱의 모든 컴포넌트에서 인텐트를 통해 실행가능하다는 것과 비슷한 상황입니다. 하지만 매니페스트 파일에 서비스를 선언할 때, 다른 앱에서 서비스를 사용하지 못하게 선언할 수 있습니다. 이에 대해 자세한 내용은 아래의 매니페스트에서 서비스 선언하기 섹션에서 학습하실 수 있습니다.

주의사항: 서비스는 기본적으로 앱이 구동되는 프로세스의 메인 쓰레드에서 실행됩니다. 다시 말해서, 서비스는 별도의 프로세스나 쓰레드에서 실행되는 것이 아닙니다. 이것은, 서비스에서 CPU 사용량이 많은 작업, 또는 mp3 재생이나 네크워킹과 같은 블락킹(blocking) 작업을 수행해야 하는 경우, 서비스에서 별도의 쓰레드를 생성하여 그 안에서 작업이 수행되도록 구현해야 한다는 것을 의미합니다. 이렇게 별도의 쓰레드를 사용함으로써, 응답 지연 문제(ANR)를 예방하고, 앱의 메인 스레드가 사용자와의 상호작용에 집중할 수 있도록 해줍니다.  


기본 개념

서비스를 만들기 위해서는 Service 클래스(또는 Service의 서브클래스)를 상속받아 서브클래스를 만들면 됩니다. 서비스의 생명주기에 따른 동작을 위해 생명주기 콜백 메소드를 구현해야 하고, 서비스를 바인드한 컴포넌트에게 제공하는 메소드들을 구현할 수도 있습니다. 주요 콜백 메소드들은 아래와 같습니다:

onStartCommand()
다른 컴포넌트(액티비티 등)가 startService()를 호출함으로써 서비스 실행을 요청할 때, 시스템이 호출해주는 콜백 메소드입니다. 이 메소드가 실행되면 서비스는 started 상태가 되며, 후면(background)에서 작업을 수행합니다. 만약 이 메소드를 구현한다면, 서비스가 할 작업을 모두 마쳤을 때 서비스를 중지하기 위해 stopSelf()stopService()를 호출하는 부분도 구현해야 합니다. (만약 서비스를 바인드만 해서 사용한다면, 이 메소드를 구현하지 않아도 됩니다.)

onBind()
다른 컴포넌트가 (RPC를 하기 위해) bindService()를 호출함으로써 서비스를 바인드할 때, 시스템이 호출해주는 콜백 메소드입니다. 이 메소드에서는, 다른 컴포넌트가 서비스의 메소드들을 사용할 수 있도록 IBinder라는 인터페이스를 리턴해줘야 합니다. 다른 컴포넌트에 의해 바인드 되어 사용될 서비스라면 이 콜백 메소드를 반드시 구현해야 하지만, 바인드 되는 서비스가 아니라면 구현할 필요가 없습니다(기본적으로는 IBinder가 null로 리턴됩니다).

onCreate()
서비스가 생성될 때 호출되는 콜백 메소드이며, 여기에서는 (액티비티의 onCreate()와 마찬가지로) 서비스가 살아있는 동안 사용할 멤버 변수들을 셋팅하는 일을 합니다. 이 메소드는 onStartCommand()나 onBind()가 호출되기 전에 호출되며, 서비스가 실행되고 있는 중이라면 호출되지 않습니다.

onDestroy()
서비스가 더이상 사용되지 않아 종료될 때 호출되는 콜백 메소드입니다. 이 메소드는 서비스의 생명주기에서 가장 마지막에 호출되는 콜백 메소드이기 때문에, 여기에서는 서비스에서 사용하던 리소스들(쓰레드, 등록된 리스너, 리시버 등)을 모두 정리해줘야(clean up) 합니다. 

만약 어떤 컴포넌트가 startService()를 호출하여 서비스를 실행했다면(이 경우 서비스의 onStartCommand()가 호출됩니다), 그 서비스는 스스로 stopSelf()를 호출하거나 다른 컴포넌트가 stopService()를 호출할 때까지 started 상태로 유지됩니다. 

만약 어떤 컴포넌트가 bindService()를 호출하여 서비스를 생성했다면(이 경우 서비스의 onStartCommand()가 호출되지 않습니다), 그 서비스는 바인드되어 있는 동안에만 실행됩니다. 만약 서비스를 바인드한 컴포넌트들이 모두 언바인드(unbind)한다면, 시스템은 그 서비스를 종료시킵니다.

안드로이드 시스템은, 사용자에게 보여줄 액티비티 실행을 위한 메모리가 부족할 경우 서비스를 강제로 종료할 수 있습니다. 하지만 만약 서비스가 현재 사용자에게 보여지고 있는 액티비티에 바인드 되어 있다면, 다른 평범한 서비스보다는 중요하다고 판단하여 다른 서비스를 먼저 종료하도록 합니다. 그리고 서비스가 전면(foreground)에서 실행되고 있다면, 종료시키지 않습니다. 반면에, 서비스가 실행되어 꽤 오랜 시간이 흘렀다면, 시스템은 그 서비스를 후면(background)에 있는 컴포넌트들 중에서도 중요도가 낮은 쪽으로 분류할 것이고, 따라서 종료될 확률도 높아지게 됩니다. 그래서 서비스는 시스템에 의해 종료 및 재시작되는 경우에 대비하여 설계되어야 합니다. 시스템이 어떤 서비스를 종료시켰다 하더라도, 사용할 수 있는 환경이 된다면 시스템은 다시 그 서비스를 재시작해줄 수 있습니다(onStartCommand()가 리턴해주는 값에 따라 다르게 동작합니다). 시스템이 서비스를 종료하는 것과 관련하여 더 자세한 내용은 프로세스와 쓰레드에서 학습할 수 있습니다.

이어지는 섹션들에서는, 서비스를 만드는 방법과 다른 컴포넌트에서 서비스를 사용하는 방법에 대하여 학습합니다.  


매니페스트에서 서비스 선언하기

다른 컴포넌트들과 마찬가지로, 서비스도 앱의 매니페스트 파일에 선언해야 합니다. 

서비스를 선언하기 위해서는 아래 예제와 같이 <application> 요소의 자식 요소로 <service> 요소를 추가하면 됩니다:

<manifest ... >
  ...
 
<application ... >
     
<service android:name=".ExampleService" />
      ...
 
</application>
</manifest>

<service> 요소에는 서비스 실행시 요구되는 퍼미션과 어떤 프로세스에서 실행할지 등을 속성값으로 지정할 수도 있습니다. 하지만 android:name 만이 필수입력 사항이며, 여기에는 서비스의 클래스명을 넣습니다. 앱이 일단 출시되고 나면, 이 이름은 되도록 바꾸는 일이 없어야 합니다. 왜냐하면, 다른 앱에서 이 이름을 이용한 명시적 인텐트로 서비스를 실행하고 있을 수도 있기 때문입니다. 

앱의 보안을 강화하고자 한다면, 서비스를 시작하거나 바인드할때 명시적 인텐트를 사용하고, 매니페스트 파일에 서비스를 위한 인텐트 필터를 선언하지 않아야 합니다. 만약 인텐트 필터를 선언하고 서비스명이 없는 암묵적 인텐트로 서비스를 실행하는 상황에서, 어떤 서비스가 실행될지를 더 명확하게 하고 싶다면, 인텐트의 setPackage()를 호출함으로써 지정된 패키지 안에 있는 서비스가 실행되도록 제한할 수 있습니다.

추가적으로, android:exported"false"로 지정함으로써 서비스가 오직 내 앱 내에서만 실행되도록 할 수 있습니다. 이 방법은 다른 앱이 내 앱의 컴포넌트를 실행하지 못하도록 막아줍니다.


Started 상태의 서비스 생성하기

started 상태의 서비스란, 다른 컴포넌트의 startService() 호출에 의해 onStartCommand() 콜백 메소드가 호출된 상태의 서비스를 말합니다. 

서비스는 started 상태가 되면, 자신을 실행한 컴포넌트에 독립적인 생명주기를 갖고 후면(background)에서 실행되며, 자신을 실행한 컴포넌트가 종료되더라도 그와 상관없이 꿋꿋하게 자신이 해야할 일을 합니다. 따라서 서비스가 할일을 다하여 종료시키고 싶다면, 서비스 내에서 stopSelf()를 호출하여 스스로 종료되도록 하거나, 다른 컴포넌트에서 stopService()를 호출하여 종료시켜줘야 합니다.

액티비티 등의 앱 컴포넌트는 startService()를 호출함으로써 서비스를 실행할 수 있고, 이때 어떤 서비스를 실행할지에 대한 정보와 그 외에 서비스에 전달해야할 데이터를 담고 있는 인텐트를 인자로 넘길 수 있습니다. 그리고 서비스의 onStartCommand() 콜백 메소드에서 매개변수로 그 인텐트를 받게 됩니다.

예를 들어, 액티비티가 어떤 데이터를 온라인 데이터베이스에 저장해야 하는 상황을 가정해 보겠습니다. 액티비티는 startService()를 호출하여 서비스를 실행하며, 이때 넘겨주는 인텐트에 저장할 데이터를 담습니다. 서비스는 onStartCommand()가 호출될 때 매개변수로 그 인텐트를 넘겨받고, 인터넷을 통해 온라인 데이터베이스에 연결하여 데이터를 저장하는 작업을 수행합니다. 그리고 작업이 끝나면, stopSelf()를 호출하여 정지(stop) 및 종료(destroy) 합니다.

주의사항: 서비스는 기본적으로 앱의 다른 컴포넌트들과 같은 프로세스에서 실행되며, 메인 쓰레드(UI 쓰레드)에서 실행됩니다. 따라서 액티비티가 실행되고 있는 상태에서, 서비스가 CPU 사용량이 많거나 실행 흐름을 막는 작업(blocking operation)을 하게 되면 액티비티의 동작이 느려질 수 있습니다. 그러므로 이러한 문제를 피하기 위해서는, 서비스 내에서 쓰레드를 생성하여 사용하도록 구현해야 합니다.

일반적으로, 서비스를 만들때 아래 2개의 클래스 중 하나를 상속받습니다:

Service
가장 기본이 되는 추상 클래스이며, 모든 서비스 클래스는 이 클래스를 상속 받습니다. 서비스는 기본적으로 앱의 메인 쓰레드에서 실행되기 때문에 같은 프로세스에서 실행되는 액티비티의 퍼포먼스를 낮출 수 있습니다. 따라서 이 클래스를 상속받아 확장할 때, 쓰레드를 생성하여 서비스가 할 일을 생성된 쓰레드에서 하도록 만드는 것이 중요합니다.

IntentService
이 클래스는 Service 클래스의 서브 클래스이며, 하나의 워커 쓰레드를 만들어서 요청들을 한번에 하나씩 처리하도록 합니다. 만약 여러개의 요청을 동시에 처리해야만 하는 상황이 아니라면, 이 클래스를 사용하는 것이 가장 좋은 선택일 것입니다. 이 클래스를 상속받아 확장할 때는 onHandleIntent()만 구현하면 되는데, 이 콜백 메소드는 워커 쓰레드에서 실행되기 때문에, 매개변수로 인텐트를 받아서 그에 해당하는 요청을 처리하는 것만 구현해 주면 됩니다. 

이어지는 섹션들에서는 IntentServiceService를 상속받아 확장하는 방법을 설명합니다.


IntentService 클래스 확장하기

서비스에서 동시에 여러개의 요청을 처리해야만 하는 경우는 거의 없기 때문에, 아마도 IntentService 클래스를 상속받아 서비스 클래스를 만드는 것이 가장 좋은 선택일 것입니다. 

IntentService는 아래와 같은 일을 합니다:

  • 워커 쓰레드를 만든 후, onStartCommand()를 통해 전달받은 인텐트들을 (메인 쓰레드가 아닌) 워커 쓰레드에서 차근차근 실행합니다.
  • 작업큐를 만들어서 전달받은 인텐트들을 넣어놨다가 한번에 하나씩 onHandleIntent()에게 넘겨주기 때문에, 멀티쓰레드 구현에 대한 고민을 하지 않아도 됩니다.
  • 모든 요청에 대한 처리가 끝나면 종료되도록 이미 구현되어 있기 때문에, 확장한 클래스에서 stopSelf()를 호출할 필요가 없습니다.
  • onBind() 콜백 메소드는 기본적으로 null을 리턴하도록 되어 있습니다.
  • onStartCommand() 콜백 메소드는 기본적으로 전달 받은 인텐트를 작업큐에 넣는 일을 합니다.

이러한 사실들에 비춰봤을때, 확장 클래스에서는 onHandleIntent()만 구현하면 됩니다.(필요에 따라 생성자를 만들어야 할 수는 있습니다.)

IntentService의 구현 예제:

public class HelloIntentService extends IntentService {

 
/**
   * A constructor is required, and must call the super
IntentService(String)
   * constructor with a name for the worker thread.
   */

 
public HelloIntentService() {
     
super("HelloIntentService");
 
}

 
/**
   * The IntentService calls this method from the default worker thread with
   * the intent that started the service. When this method returns, IntentService
   * stops the service, as appropriate.
   */

 
@Override
 
protected void onHandleIntent(Intent intent) {
     
// Normally we would do some work here, like download a file.
     
// For our sample, we just sleep for 5 seconds.
     
long endTime = System.currentTimeMillis() + 5*1000;
     
while (System.currentTimeMillis() < endTime) {
         
synchronized (this) {
             
try {
                  wait
(endTime - System.currentTimeMillis());
             
} catch (Exception e) {
             
}
         
}
     
}
 
}
}

만약 onCreate(), onStartCommand(), onDestroy() 등의 콜백 메소드를 오버라이드 해야한다면, super.XXX()를 꼭 호출해줘야 합니다. 그래야 워커 쓰레드가 정상적으로 동작하기 때문입니다.

예를 들어, 오버라이드한 onStartCommand() 콜백 메소드는 아래와 같이 super.onStartCommand()를 리턴합니다:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
   
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();
   
return super.onStartCommand(intent,flags,startId);
}

onHandleIntent()와 더불어 onBind()에서도 super.XXX()를 호출할 필요가 없습니다(그러나 바인드 되어 사용되는 서비스라면 onBind()만 구현하면 됩니다).

다음 섹션에서는, Service 클래스를 확장하여, 위에서 구현한 서비스와 같은 일을 하는 서비스를 만드는 방법을 설명합니다. 이것은 훨씬 더 많이 코딩해야 하지만, 동시에 여러개의 요청들을 처리해야 한다면 이 방법을 사용해야 할 것입니다.


Service 클래스 확장하기

이전 섹션에서 학습한 바에 따르면, IntentService를 확장하여 서비스를 만드는 것은 매우 간단합니다. 하지만 만약 여러 작업들을 작업큐에 담아 차례대로 처리하는 대신에 멀티쓰레드 방식으로 서비스가 동작해야 한다면, Service를 확장하고 각각의 인텐트를 처리하도록 직접 구현해야할 것입니다.

아래 예제는 Service를 확장하여 만든 서비스 클래스이고, 비교를 위하여 위의 IntentService를 확장하여 만든 클래스와 똑같은 동작을 하도록 만들었습니다. 이것은, 각 요청에 대하여 한번에 하나씩 처리하도록 하기 위해 워커 쓰레드를 사용합니다.

public class HelloService extends Service {
 
private Looper mServiceLooper;
 
private ServiceHandler mServiceHandler;

 
// Handler that receives messages from the thread
 
private final class ServiceHandler extends Handler {
     
public ServiceHandler(Looper looper) {
         
super(looper);
     
}
     
@Override
     
public void handleMessage(Message msg) {
         
// Normally we would do some work here, like download a file.
         
// For our sample, we just sleep for 5 seconds.
         
long endTime = System.currentTimeMillis() + 5*1000;
         
while (System.currentTimeMillis() < endTime) {
             
synchronized (this) {
                 
try {
                      wait
(endTime - System.currentTimeMillis());
                 
} catch (Exception e) {
                 
}
             
}
         
}
         
// Stop the service using the startId, so that we don't stop
         
// the service in the middle of handling another job
          stopSelf
(msg.arg1);
     
}
 
}

 
@Override
 
public void onCreate() {
   
// Start up the thread running the service.  Note that we create a
   
// separate thread because the service normally runs in the process's
   
// main thread, which we don't want to block.  We also make it
   
// background priority so CPU-intensive work will not disrupt our UI.
   
HandlerThread thread = new HandlerThread("ServiceStartArguments",
           
Process.THREAD_PRIORITY_BACKGROUND);
    thread
.start();

   
// Get the HandlerThread's Looper and use it for our Handler
    mServiceLooper
= thread.getLooper();
    mServiceHandler
= new ServiceHandler(mServiceLooper);
 
}

 
@Override
 
public int onStartCommand(Intent intent, int flags, int startId) {
     
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();

     
// For each start request, send a message to start a job and deliver the
     
// start ID so we know which request we're stopping when we finish the job
     
Message msg = mServiceHandler.obtainMessage();
      msg
.arg1 = startId;
      mServiceHandler
.sendMessage(msg);

     
// If we get killed, after returning from here, restart
     
return START_STICKY;
 
}

 
@Override
 
public IBinder onBind(Intent intent) {
     
// We don't provide binding, so return null
     
return null;
 
}

 
@Override
 
public void onDestroy() {
   
Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
 
}
}

보시다시피 IntentService를 사용할 때보다 훨씬 더 일이 많습니다. 

하지만 서비스가 실행 요청을 받으면 각각의 요청에 대하여 onStartCommand()가 호출되기 때문에, 동시에 여러개의 요청을 처리할 수 있습니다. 위의 예제에서는 동시에 여러개의 요청을 처리하도록 구현되어 있지 않지만, 만약 그렇게 구현하고 싶다면, 각 요청에 대한 쓰레드를 생성하여, 이전 작업이 끝날때까지 기다릴 필요 없이 바로 실행하면 됩니다. 

onStartCommand()정수형의 값을 리턴해야 합니다. 그 값은, 시스템이 메모리가 부족하여 서비스를 종료한 이후에, 다시 여유가 생겼을 때 서비스를 이어서 실행할 것인지 여부를 나타내며, 아래의 3가지 중 한가지가 됩니다.

START_NOT_STICKY
onStartCommand()에서 이 값이 리턴된 후, 시스템이 서비스를 종료시켰다면, 다시 서비스가 실행될 수 있는 여건이 되더라도 서비스를 다시 생성하지 않습니다. 이것은 불필요하게 서비스가 실행되는 것을 막는 가장 안전한 방법이지만, 완료되지 못한 작업들 또한 더이상 수행되지 않습니다.

START_STICKY
onStartCommand()에서 이 값이 리턴된 후, 시스템이 서비스를 종료시켰다면, 다시 서비스가 실행될 수 있는 여건이 되었을 때, 서비스를 생성하고 onStartCommand()를 호출해 줍니다(하지만 종료전의 마지막 인텐트를 다시 전달해 주지는 않습니다). 이것은 미디어 플레이어처럼 계속 실행되길 원하는 경우에 적합합니다.

START_REDELIVER_INTENT
onStartCommand()에서 이 값이 리턴된 후, 시스템이 서비스를 종료시켰다면, 다시 서비스가 실행될 수 있는 여건이 되었을 때, 서비스를 생성하고 onStartCommand()를 호출하며 종료전의 마지막 인텐트를 인자로 넘겨줍니다. 이것은 파일 다운로드처럼 반드시 완료가 되어야 하는 경우에 적합합니다.


위의 값들에 대한 자세한 내용은 Service 클래스의 레퍼런스 문서에서 학습할 수 있습니다.


서비스 실행하기

startService() 호출시 실행할 서비스 정보를 가지고 있는 인텐트를 넘김으로써, 액티비티를 비롯한 앱 컴포넌트들에서 서비스를 실행할 수 있습니다. 안드로이드 시스템은 서비스를 실행시킬때 onStartCommand()를 호출해주면서 여기에 그 인텐트를 전달해 줍니다.(onStartCommand()를 직접 호출하지 않도록 합니다.)

아래 예제는, 이전 섹션에서 학습한 HelloService를 액티비티에서 명시적 인텐트로 실행하는 코드입니다.

Intent intent = new Intent(this, HelloService.class);
startService
(intent);

startService() 메소드는 호출 즉시 리턴되며, 그 뒤에 안드로이드 시스템이 서비스의 onStartCommand()를 호출합니다. 만약 서비스가 실행중인 상태가 아니었다면, 시스템은 먼저 onCreate()를 호출한 후 그 다음으로 onStartCommand()를 호출합니다.

서비스가 바인딩을 지원하지 않는다면, startService()에 넘겨주는 인텐트가 startService()를 호출하는 앱 컴포넌트와 호출되는 서비스의 유일한 소통 수단이 됩니다. 하지만, 만약 서비스로부터 결과값을 돌려받기를 원한다면, 서비스에서 결과값을 담은 인텐트로 sendBroadcast()하고, 결과를 받을 앱 컴포넌트에는 브로드캐스트 리시버가 등록되어 있으면 됩니다. (이 부분은 원문과 다릅니다. 원문에서는 PendingIntent가 언급되어 있는데 PendingIntent는 실행할 인텐트를 담아놓고 대기시켜 놨다가 원하는 적절한 시점에 실행할 수 있도록 해주는 것입니다. 원문대로라면 startService()할 때 PendingIntent를 넘기고 서비스에서 그 PendingIntent를 이용하여 브로드캐스트할 수 있어야 하는데, 그러한 메소드는 없으며 굳이 필요하지도 않습니다.)

서비스를 실행하기 위해 요청을 여러번 보내면 그때마다 서비스의 onStartCommand()가 호출되어 각 요청을 처리할 수 있습니다. 하지만 요청을 여러번 하더라도 서비스 객체는 하나이기 때문에 서비스를 정지시키기 위해서는 stopSelf()stopService()를 한번만 호출하면 됩니다.


서비스 정지시키기

서비스가 일단 시작되고 할일을 모두 마쳤다면, 자신 또는 다른 앱 컴포넌트에 의해 종료되어야 합니다. 시스템은, 메모리가 부족하여 어쩔 수 없이 서비스를 종료시키는 경우가 아니라면, 서비스를 종료시키지 않기 때문입니다. 서비스는 스스로 stopSelf()를 호출함으로써 종료되거나, 다른 앱 컴포넌트가 stopService()를 호출함으로써 종료될 수 있습니다.

stopSelf()나 stopService()를 호출하고나면, 시스템은 가능한한 빨리 서비스를 정지시킵니다. 하지만 서비스가 동시에 여러개의 요청을 받아 onStartCommand()가 요청들을 처리하고 있는 상태에서, 어느 하나의 요청이 완료되었다고 하여 서비스가 정지된다면, 다른 요청들이 처리되지 못하는 문제가 있을 수 있겠죠. 이러한 문제를 해결하기 위해서는 stopSelf(int startId)를 호출해야 합니다. 이것은 서비스가 가장 마지막에 받은 요청을 처리한 후에 종료될 수 있도록 합니다. 여기서 startId는 onStartCommand()에서 전달받은 값이며, 서비스가 요청을 받을 때마다 값이 증가합니다. 예를 들어, 서비스가 요청을 처리하고 나면 stopSelf(int)를 호출하도록 구현되어 있고, 연속적으로 두 개의 요청이 발생한 경우를 가정해 봅시다. 첫번째 요청시 onStartCommand()로부터 받은 startId가 1이고, 요청을 처리하고 있는 중에 두번째 요청이 들어와서 또 onStartCommand()가 호출되었고 startId는 2이며 해당 요청을 처리합니다. 그런데 두번째 요청이 모두 처리되기 전에 첫번째 요청에 대한 처리가 완료되어 stopSelf(1)가 호출되었다면, 마지막 요청에 대한 startId가 2이고 정지 요청시 전달한 startId가 1로서 서로 다르기 때문에 서비스를 정지시키지 않는 것입니다. 

주의사항: 서비스가 할 일을 마치고 나면 종료되도록 개발하는 것은 시스템 리소스의 낭비를 막고 배터리 수명을 늘리기 위해서 중요합니다. 필요하다면, 다른 앱 컴포넌트에서 stopService()를 호출하여 서비스를 정지시킬 수 있습니다. 바인드가 되는 서비스라 하더라도, onStartCommand()에서 실행한 작업이 완료되면 서비스를 정지시켜줘야 합니다. 

서비스의 생명주기에 관한 자세한 내용은, 아래 섹션의 서비스의 생명주기 관리하기에서 학습하실 수 있습니다.


바인드 되는 서비스 만들기

앱 컴포넌트들은 서비스와 연결상태를 계속 유지하기 위해 bindService()를 호출함으로써 (바인드 되는 서비스라면) 서비스와 바인드할 수 있습니다. 

서비스가 내 앱 내의 액티비티를 비롯한 다른 컴포넌트들과 상호작용을 하도록 하거나, 프로세스간 통신(IPC)를 통해 서비스의 일부 기능을 다른 앱에게 제공할 수 있도록 하기 위해서는 바인드 되는 서비스를 만들어야 합니다. 

바인드 되는 서비스를 만들기 위해서는, 서비스의 onBind() 콜백 메소드에서 서비스와 통신할 수 있는 인터페이스인 IBinder를 리턴해줘야 합니다. 그러면 다른 앱 컴포넌트에서 bindService()를 호출함으로써 그 인터페이스를 얻은 후에 서비스와 통신할 수 있습니다. 바인드 된 서비스는 자신을 바인드 한 앱컴포넌트에게 기능을 제공하기 위해 존재하며, 바인드 한 앱컴포넌트가 없으면 시스템에 의해 종료됩니다(onStartCommand()가 호출되어 시작된 서비스는 직접 종료시켜줘야 하지만, 바인드 된 서비스는 그럴 필요가 없습니다).

바인드 되는 서비스를 만들기 위해 가장 먼저 할 일은, 서비스와 통신하기 위한 인터페이스를 정의하는 것입니다. 서비스와 클라이언트 사이의 인터페이스는 IBinder를 구현한 것이어야 하며, 이것은 onBind()에서 리턴해 줍니다. 클라이언트가 일단 IBinder를 받고 나면, 그것을 통해 서비스와 상호작용할 수 있는 것입니다.

서비스는 한번에 여러개의 클라이언트에게 바인드 될 수 있습니다. 각각의 클라이언트는 서비스에 대한 용건을 마친후 unbindService()를 호출하여 바인드를 끊을 수 있으며, 모든 클라이언트가 바인드를 끊어 더이상 서비스를 바인드한 클라이언트가 없을 경우 시스템은 그 서비스를 종료시킵니다.

바인드 되는 서비스를 구현하는 방법은 몇가지가 있으며, 서비스를 시작시키는 것보다 더 복잡하기 때문에, 자세한 내용은 서비스 바인드하기에서 학습하시기 바랍니다.


사용자에게 알림 보내기

실행되고 있는 서비스는 사용자에게 토스트 알림이나 상태바 알림을 보내줄 수 있습니다. 

토스트 알림은 현재 화면에서 메시지가 잠깐 보였다 사라지는 방식입니다. 반면에 상태바 알림은 상태바에 아이콘과 메시지를 보여주고, 상태바를 열었을때 알림 목록을 볼 수 있으며, 그것을 눌러서 액티비티를 실행하는 등의 동작을 할 수 있습니다.

상태바 알림은 보통 후면(background)에서의 작업이 완료되었을 때 사용자에게 알리기 위해 사용하는 가장 좋은 방법입니다. 파일을 다운로드하는 경우를 예로 들어보겠습니다. 후면에서 파일 다운로드가 완료되어 사용자에게 상태바 알림을 보내주면, 사용자는 그 알림을 보고 상태바를 열어서 해당 항목을 누를 것이고, 그러면 다운로드한 파일을 보여주는 액티비티가 실행될 수 있을 것입니다.

더 자세한 내용은 토스트 알림상태바 알림에서 학습하실 수 있습니다.


전면(foreground)에서 서비스 실행하기

전면(foreground)에 있는 서비스는 현재 뭔가 하고 있다는 것을 사용자가 인지하고 있는 서비스로서 메모리 부족시 시스템에 의한 종료 대상에서 제외됩니다. 전면에 있는 서비스는 "진행중(Ongoing)"인 상태바 알림을 제공해야 하며, 그것은 서비스가 정지되거나 전면에서 제외되지 않으면 상태바 알림 목록에서 제거되지 않는다는 것을 의미합니다.

예를 들면, 서비스에서 음악을 재생중인 음악 플레이어는, 사용자가 음악 재생중이라는 것을 알고 있기 때문에 그 서비스를 전면에 두기 위해 상태바 알림을 제공합니다. 그 상태바 알림은 아마도 현재곡을 표시해 줄 것이며, 눌렀을 때 음악 플레이어 액티비티를 실행해 줄 것입니다.

서비스를 전면에 두기 위해서는 startForeground()를 호출합니다. 이 메소드에는 두 개의 인자를 넘기는데, 첫번째는 알림을 식별하기 위한 유니크한 정수형 ID이며, 두번째는 Notification 객체입니다. 예제 코드:

Notification notification = new Notification(R.drawable.icon, getText(R.string.ticker_text),
       
System.currentTimeMillis());
Intent notificationIntent = new Intent(this, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
notification
.setLatestEventInfo(this, getText(R.string.notification_title),
        getText
(R.string.notification_message), pendingIntent);
startForeground
(ONGOING_NOTIFICATION_ID, notification);

주의사항: startForeground()의 첫번째 인자인 ID에 0을 넣으면 안됩니다.

서비스를 전면에서 제외시키기 위해서는 stopForeground()를 호출합니다. 이 메소드는 boolean형의 인자를 받는데 이것은 상태바에 있는 알림을 제거할 것인지 여부를 가리킵니다. 이 메소드는 서비스를 정지시키지는 않습니다. 하지만 서비스가 전면에서 실행되고 있는 중에 정지되면, 그에 따라 상태바 알림도 제거됩니다.

상태바 알림에 대한 자세한 내용은 상태바 알림 생성하기에서 학습하실 수 있습니다.


서비스의 생명주기 관리하기

서비스의 생명주기는 액티비티의 생명주기보다 훨씬 단순합니다. 하지만 서비스는 사용자의 눈에 보이지 않는 후면(background)에서 동작하기 때문에, 생성되고 종료되는 것에 더 신경을 써야 합니다.

서비스의 생명주기(생성에서 종료까지)는 두가지 경우가 있습니다:

  • 시작된(started) 서비스
    다른 앱컴포넌트의 startService() 호출에 의해 시작된 서비스입니다. 서비스 내에서 stopSelf()를 호출하거나, 다른 앱컴포넌트에서 stopService()를 호출하여 정지시키기 전까지는 계속 실행됩니다. 서비스가 정지되고 나면 시스템에 의해 종료됩니다.
  • 바인드 된(bound) 서비스
    다른 앱컴포넌트(클라이언트)의 bindService() 호출에 의해 바인드 된 서비스입니다. 클라이언트는 IBinder 인터페이스를 통해 서비스와 통신할 수 있으며, unbindService()를 호출하여 바인드를 끊을 수 있습니다. 여러개의 클라이언트가 같은 서비스를 바인드할 수 있으며, 바인드 된 모든 클라이언트가 바인드를 끊으면 시스템에 의해 종료됩니다.(시작된 서비스처럼 정지 메소드를 호출할 필요가 없습니다.)

위의 두가지 경우는 완전히 구분되는 것이 아닙니다. 이것은 startService()에 의해 시작된 서비스를 바인드 할 수 있다는 것을 의미합니다. 예를 들면, 음악을 재생하는 서비스는 일단 startService() 호출로 시작될 수 있고, 그 다음에 재생/정지/이전곡/다음곡 등의 기능 및 현재곡 정보를 가져오는 등의 일을 하기 위해 bindService()를 호출하여 서비스를 바인드할 수 있습니다. 이러한 경우, 바인드 했던 클라이언트들이 모두 바인드를 끊어도 stopSelf()나 stopService()가 호출되지 않는다면 서비스가 종료되지 않고, 반대로 stopSelf()나 stopService()가 호출되더라도 모든 클라이언트가 바인드를 끊을 때까지는 서비스가 종료되지 않습니다.


생명주기 콜백 메소드 구현하기

액티비티처럼, 서비스도 생명주기 상태를 모니터링 하면서 적절한 시점에 원하는 작업을 할 수 있는 생명주기 콜백 메소드를 갖습니다. 아래의 서비스 코드 뼈대는 서비스의 생명주기 콜백 메소드들을 보여줍니다.

public class ExampleService extends Service {
   
int mStartMode;       // indicates how to behave if the service is killed
   
IBinder mBinder;      // interface for clients that bind
   
boolean mAllowRebind; // indicates whether onRebind should be used

   
@Override
   
public void onCreate() {
       
// The service is being created
   
}
   
@Override
   
public int onStartCommand(Intent intent, int flags, int startId) {
       
// The service is starting, due to a call to startService()
       
return mStartMode;
   
}
   
@Override
   
public IBinder onBind(Intent intent) {
       
// A client is binding to the service with bindService()
       
return mBinder;
   
}
   
@Override
   
public boolean onUnbind(Intent intent) {
       
// All clients have unbound with unbindService()
       
return mAllowRebind;
   
}
   
@Override
   
public void onRebind(Intent intent) {
       
// A client is binding to the service with bindService(),
       
// after onUnbind() has already been called
   
}
   
@Override
   
public void onDestroy() {
       
// The service is no longer used and is being destroyed
   
}
}

메모: 액티비티의 콜백 메소드들과는 다르게 서비스의 콜백 메소드들을 구현할 때는 super class의 메소드들을 호출해 줄 필요가 없습니다.


그림 2. 서비스의 생명주기. 왼쪽은 startService()로 서비스가 생성된 경우이고, 오른쪽은 bindService()로 서비스가 생성된 경우입니다.


콜백 메소드들을 구현해보면, 서비스의 생명주기 안에서 두 개의 중첩된 루프 구간이 있다는 것을 알게 될 것입니다.

  • 전체 구간(entire lifetime)onCreate()onDestroy() 사이 구간입니다. 액티비티에서처럼, 서비스도 onCreate()에서 서비스가 실행되는 동안 사용할 멤버 변수 셋팅 등의 초기화를 하고, onDestroy()에서 그것들을 정리해줘야 합니다(release). 예를 들면, 음악 재생 서비스는 onCreate()에서 음악을 재생하는 쓰레드를 생성하고, onDestroy()에서 그 쓰레드를 종료합니다. 
    onCreate()와 onDestroy()는 startService()를 호출한 경우와 bindService()를 호출한 경우 모두에서 공통적으로 호출되는 콜백 메소드입니다.
  • 활성 구간(active lifetime)onStartCommand()onBind()가 호출되었을때부터 시작됩니다. 각 메소드는 각각 startService()와 bindService()로부터 인텐트를 전달 받습니다. 
    startService() 호출로 서비스가 시작된 경우에, 활성 구간의 끝은 전체 구간의 끝과 같이 onDestroy()입니다. 하지만 bindService() 호출로 서비스가 바인드된 경우에, 활성 구간의 끝은 onUnbind() 입니다.

메모: 시작된(started) 서비스가 stopSelf()나 stopService() 호출로 정지될 때, 그에 대응하는 onStop()과 같은 콜백 메소드는 없습니다. 따라서 서비스가 바인드 된 경우가 아니라면, 서비스가 정지될 때 호출되는 유일한 콜백 메소드는 onDestroy() 입니다.

그림2는 전형적인 서비스의 콜백 메소드들을 보여줍니다. 그림에서는 startService()로 생성된 경우와 bindService()로 생성된 경우를 구분하고 있기는 하지만, startService()로 시작된 서비스들을 bindService()로 바인드할 수 있다는 점을 명심해야할 것입니다. 따라서 onStartCommand()가 호출된 서비스에서 onBind()가 호출될 수도 있는 것입니다.

바인드 되는 서비스를 만드는 방법에 대한 자세한 내용은 서비스 바인드하기 문서에서 학습하실 수 있으며, onRebind() 콜백 메소드에 대한 내용은 해당 문서의 바인드 된 서비스의 생명주기 관리하기 섹션에서 학습하실 수 있습니다.


Posted by 개발자 김태우
,

(원문: http://developer.android.com/guide/components/tasks-and-back-stack.html)

(위치: Develop > API Guides > App Components > Activities
> Tasks and Back Stack
)

(전체 번역 글 목록으로 이동하기)


태스크와 백스택


앱은 보통 여러개의 액티비티로 구성됩니다. 각 액티비티는 특정한 목적을 갖는 화면을 보여주며, 다른 액티비티들을 실행할 수 있습니다. 예를 들어, 이메일앱은 읽지 않은 이메일 목록을 보여주는 액티비티가 있으며, 목록에서 이메일을 선택하면 그 이메일 내용을 보여주는 액티비티도 있습니다. 

액티비티는 디바이스에 설치된 다른 앱의 액티비티도 실행할 수 있습니다. 예를 들어, 내 앱에서 이메일 보내기가 되도록 하고 싶다면, Intent.ACTION_SEND 액션값과 이메일 주소 및 메시지를 담고 있는 인텐트를 만들어서 실행하면 됩니다. 그러면 디바이스에 설치된 모든 앱 중에서 해당 인텐트에 대응되는, 즉 이메일을 비롯한 데이터를 보내는 기능을 수행하는 액티비티가 실행될 것입니다. 만약 그러한 액티비티가 2개 이상이라면 사용자가 선택할 수 있도록 다이얼로그를 띄워 줍니다. 이메일 보내기 작업이 완료되면 이메일 쓰기 액티비티가 종료되면서 내 앱의 액티비티가 다시 resumed 상태가 되는데, 이는 마치 이메일 쓰기 액티비티가 내 앱의 일부인 것처럼 보이게 합니다. 이러한 경우, 비록 액티비티들이 서로 다른 앱에 속해 있긴 하지만, 같은 태스크에 있음으로써 사용자에게 자연스러운 사용자 경험(UX)을 제공할 수 있는 것입니다. 

태스크는 사용자에 의해 실행된 액티비티들의 모음입니다. 액티비티들은 생성되면서 "백스택"이라 불리는 스택에 쌓이게 됩니다.

디바이스의 홈 화면은 거의 모든 태스크들의 시작점입니다. 사용자가 앱 런처에서 앱 아이콘을 터치하거나, 홈 화면의 바로가기 아이콘을 터치하면, 앱의 태스크가 전면(foreground)으로 나오게 됩니다. 만약 앱을 최근에 실행한 적이 없어서 태스크가 없다면, 새 태스크가 생성되며 앱의 "메인" 액티비티가 실행되고 태스크의 root가 됩니다.

현재 액티비티가 다른 액티비티를 실행하면, 실행된 새 액티비티가 스택의 top에 들어가면서 포커스를 획득하며, 이전 액티비티는 여전히 스택에 들어가 있기는 하지만 stopped 상태가 됩니다. 액티비티가 정지되더라도(stop), 시스템은 액티비티의 사용자 인터페이스(UI)의 현재 상태를 보존하며, 사용자가 뒤로가기버튼을 눌렀을 때, 현재의 액티비티를 스택에서 꺼내고(popped. 이때 액티비티는 종료됩니다), 이전 액티비티를 resumed 상태로 만듭니다(정지되기전 UI 상태가 복구됩니다). 스택에 있는 액티비티의 배열은 절대 바뀌지 않습니다. 단지 스택에 넣고(push) 꺼내는(pop) 동작만 있을 뿐입니다. 액티비티가 생성될 때 스택에 넣고, 사용자가 뒤로가기버튼을 눌러서 종료될 때 꺼내는 것입니다. 이러한 백스택의 자료구조를 "후입선출"이라고 표현합니다. 아래의 그림1은 액티비티들이 백스택에 어떻게 들어가고 나오는지를 보여줍니다.


그림 1. 이 그림은 태스크의 새 액티비티가 백스택에 어떻게 추가되는지와, 뒤로가기버튼을 눌렀을 때 현재 액티비티가 종료되며 이전 액티비티가 다시 resumed 상태가 되는 것을 보여줍니다.


만약 사용자가 계속 뒤로가기버튼을 누른다면, 스택의 top에 있는 액티비티가 제거되면서 이전 액티비티가 resumed 상태로 스택의 top이 될 것이고, 결국은 홈화면으로 빠져나갈 것입니다(또는 해당 태스크가 시작되기 전의 마지막 액티비티가 실행될 것입니다). 태스크의 모든 액티비티가 제거되면, 그 태스크도 없어집니다.


그림 2. 두개의 태스크: 태스크 B는 전면(foreground)에서 사용자와 상호작용을 하지만, 태스크 A는 후면(background)에서 다시 resumed 상태가 되기를 기다립니다.


태스크는 사용자가 다른 태스크를 시작하거나, 홈버튼을 눌러서 홈화면으로 전환될 때, "후면(background)"으로 이동합니다. 후면에 있을 때, 태스크의 모든 액티비티들은 stopped 상태가 되지만, 태스크의 백스택은 액티비티들을 잘 유지하며, 그림2에서 보이는 바와 같이 다른 태스크에게 자리를 내주고 포커스만 잃게 되는 것입니다. 태스크는 다시 "전면(foreground)"으로 나설 수 있으며, 사용자는 해당 태스크의 마지막 화면을 볼 수 있습니다. 예를 들어, 현재의 태스크(Task A)가 스택에 3개의 액티비티를 담고 있는데, 사용자가 홈버튼을 눌러 홈화면으로 이동한 후 앱 런처에서 다른 앱을 실행한다고 가정해 봅시다. 홈화면으로 이동할 때, Task A는 후면으로 이동합니다. 그리고 다른 앱을 실행할 때, 시스템은 그 앱의 태스크인 Task B를 시작하는데 그것은 자신의 액티비티들을 담을 스택을 따로 갖습니다. 그 앱에서 사용자가 할 일을 마치고 홈화면으로 돌아갔다가 다시 이전의 앱을 실행하면, Task A가 다시 전면으로 나설 것이며, 스택에 있던 3개의 액티비티는 모두 무사하고, 스택의 top에 있는 액티비티(사용자가 Task A에서 마지막으로 실행한 액티비티)가 다시 resumed 상태로 될 것입니다. 홈화면으로 이동할 때 Task B는 후면으로 이동합니다. 이전 태스크를 다시 전면으로 가져오는 방법은 해당 앱의 아이콘을 클릭하거나, 홈버튼을 길게 누르면 보이는 최근 태스크 목록에서 태스크를 선택하면 됩니다. 이것은 안드로이드 멀티태스킹의 한 예입니다.

메모: 여러개의 태스크가 동시에 후면에 있을 수 있긴 하지만, 메모리가 부족할 경우 시스템이 메모리를 회복하기 위해 후면에 있는 태스크의 액티비티들을 종료할 수도 있습니다. 본 문서에서는 액티비티의 상태를 저장하는 방법에 대해서도 학습할 것입니다. 


그림 3. 하나의 액티비티가 여러개의 서로 다른 객체로 생성될 수 있습니다.


백스택의 액티비티들은 절대 재배치되지 않기 때문에, 만약 내 앱에서 특정 액티비티가 여러개의 다른 액티비티로부터 실행될 수 있다면, 그 액티비티는 생성될 때마다 새로운 객체로 스택에 추가될 것입니다(기본적으로는, 스택에 있는 동일한 액티비티의 객체를 재사용하지 않습니다). 위의 그림3에서 보이는 바와 같이 하나의 액티비티가 여러번 객체로 생성될 수 있는 것입니다. 그리고 그림3과 같은 상황에서 사용자가 뒤로가기버튼을 연이어 누른다면, 백스택의 top에 있는 액티비티부터 차례대로 (해당 액티비티의 현재 UI상태가) 화면에 출력될 것입니다. 하지만, 액티비티의 객체가 한번만 생성되고 그것이 재사용되도록 할 수도 있으며, 자세한 내용은 태스크 관리하기 섹션에서 학습하실 수 있습니다.

액티비티와 태스크에 관한 기본적인 동작을 요약하면 아래와 같습니다:

  • 액티비티 A가 액티비티 B를 실행하면, 액티비티 A는 stopped 상태가 되지만, 시스템은 (스크롤 위치나 입력 영역에 입력된 문자열 등의) 액티비티 A에 대한 현재 상태를 유지합니다. 그리고 액티비티 B가 전면(foreground)에 있는 상태에서 뒤로가기버튼을 누르면 액티비티 A가 이전 상태를 유지하면서 다시 resumed 상태가 됩니다.

  • 사용자가 홈 버튼을 눌러서 현재 태스크로부터 빠져나가면, 현재 액티비티는 stopped 상태가 되고 태스크는 후면(background)으로 이동하지만, 시스템은 태스크의 액티비티들에 대한 상태를 유지해 줍니다. 그리고 만약 사용자가 런처에서 해당 앱 아이콘을 눌러 다시 앱을 실행하고자 하면, 해당 태스크가 다시 전면(foreground)으로 나오면서 백스택의 top에 있는 액티비티가 다시 resumed 상태가 됩니다.

  • 사용자가 뒤로가기버튼을 누르면, 현재 액티비티가 백스택에서 빠지면서 종료되고, 이전 액티비티가 resumed 상태로 변경됩니다. 액티비티가 종료되면, 시스템은 더이상 종료된 액티비티의 상태를 유지해주지 않습니다.

  • 액티비티는 (어떤 태스크에서건) 여러번 객체로 생성될 수 있습니다. 

네비게이션 설계: 안드로이드에서 앱의 네비게이션이 어떻게 동작하는지에 대한 자세한 내용은 안드로이드 디자인 영역의 네비게이션 가이드에서 학습하실 수 있습니다.


액티비티의 상태 저장하기

위에서 학습한 바에 따르면, 시스템은 기본적으로 액티비티가 stopped 상태로 될 때, 그 상태를 유지해줍니다. 이대로라면, 사용자가 뒤로가기버튼을 눌러 이전 액티비티로 이동할 때, 보여지는 상태는 사용자가 그 액티비티에서 벗어날 때의 마지막 상태가 됩니다. 하지만, 액티비티는 사용자의 의도와 상관없이 시스템에 의해 종료 및 재생성 되는 경우가 있기 때문에, 액티비티의 콜백 메소드를 이용하여 상태를 저장해야 합니다.

액티비티가 stopped 상태로 변경되면(새 액티비티를 실행하거나, 태스크가 후면(background)으로 이동하거나 하는 경우), 시스템은 메모리를 확보하기 위해 액티비티를 완전히 종료시켜버릴 수도 있습니다. 이러한 경우, 액티비티의 상태에 대한 정보는 모두 잃게 되지만, 백스택에서도 해당 액티비티가 빠지는 것은 아닙니다. 따라서, 시스템에 의해 종료된 액티비티가 백스택의 top에 오게 되면, (재사용할 액티비티 객체가 종료되었으므로) 액티비티를 다시 생성해야 합니다. 이러한 경우 액티비티의 상태를 복구하기 위해서는, 액티비티의 onSaveInstanceState() 콜백 메소드에서 상태를 저장하도록 구현해야 합니다. 

이와 관련하여 더 자세한 내용은 액티비티의 상태 저장하기 문서에서 학습하실 수 있습니다.


태스크 관리하기 

위에서 학습한 바와 같이 안드로이드는 기본적으로, 연이어 생성되는 액티비티들을 같은 태스크 내에 그리고 "후입선출" 스택에 집어 넣습니다. 태스크와 백스택을 관리하는 안드로이드의 이러한 방식은 대부분의 앱에서 훌륭하게 작동하며, 보통은 액티비티들이 어떤 태스크에 속하고 어떤 백스택에 들어가 있는지를 신경쓸 필요가 없습니다. 하지만, 내 앱에서는 이러한 안드로이드의 기본 동작과는 다르게 구현하고 싶을 수 있겠죠. 액티비티가 생성될 때 현재 태스크가 아닌 새로운 태스크를 시작하고 싶을 수도 있고, 현재 태스크에 액티비티의 객체가 있다면 액티비티 객체를 새로 생성하지 않고 이미 있는 액티비티 객체를 재사용하고 싶을 수도 있을 것입니다. 또는 사용자가 현재 태스크에서 벗어날 때, root에 있는 액티비티를 제외하고 나머지는 모두 제거하고 싶을 수도 있을 것입니다.

이러한 요구사항들은, 매니페스트 파일의 <activity> 요소에 관련 속성을 추가하거나, 소스코드의 startActivity()에 넘길 인텐트에 플래그를 추가함으로써 구현할 수 있습니다.

<activity> 요소의 주요 속성들:

taskAffinity
launchMode
allowTaskReparenting
clearTaskOnLaunch
alwaysRetainTaskState
finishOnTaskLaunch

인텐트의 주요 플래그들:

FLAG_ACTIVITY_NEW_TASK
FLAG_ACTIVITY_CLEAR_TOP
FLAG_ACTIVITY_SINGLE_TOP

이어지는 섹션에서는, 액티비티들이 어떤 태스크에 속하고 어떤 백스택에 들어가게 될지를 정의하기 위해서, 매니페스트의 속성들 및 인텐트의 플래그들을 사용하는 방법에 대하여 학습할 것입니다.

주의사항: 대부분의 앱에서는 액티비티와 태스크에 대한 기본 동작을 변경할 필요가 없습니다. 만약 변경할 필요가 있다면, 앱이 실행되는 동안의 사용성과 다른 액티비티 및 태스크에서 뒤로가기버튼을 눌러 돌아오는 동작 등에 대하여 충분히 테스트를 함으로써, 사용자가 기대하는 동작과 어긋나지 않도록 신경을 써야 합니다.


시작(launch) 모드 정의하기

시작 모드는 액티비티의 새 객체가 현재의 태스크에 어떻게 속하게 될지를 결정해 줍니다. 아래와 같이 2가지 방식으로 시작 모드를 지정할 수 있습니다:

매니페스트 파일 사용하기
메니페스트 파일에 액티비티를 선언할 때, 액티비티 생성시 태스크에 어떻게 속하게 될지를 지정할 수 있습니다.

인텐트 플래그 사용하기
소스코드에서 startActivity() 호출할 때, 인텐트에 플래그를 추가함으로써 액티비티가 태스크에 어떻게 속하게 될지를 지정할 수 있습니다.

액티비티 A가 액티비티 B를 실행하는 경우에, 액티비티 B는 매니페스트 파일에 속성을 추가함으로써 자신이 현재 태스크에 어떻게 속하게 될지를 지정할 수 있고, 액티비티 A는 액티비티 B를 실행하는 인텐트에 플래그를 추가함으로써 같은 역할을 할 수 있습니다. 이 둘은 모두 액티비티 B와 태스크의 관계를 지정하는 것이며, 액티비티 A의 인텐트 실행이 액티비티 B의 매니페스트 파일 지정보다 우선합니다.

메모: 매니페스트 파일에 지정되는 일부 시작 모드는 인텐트를 통해 구현될 수 없고, 반대로 인텐트 플래그로 지정되는 일부 시작 모드는 매니페스트 파일에 지정될 수 없습니다.


매니페스트 파일 사용하기

매니페스트 파일에 액티비티를 선언할 때, <activity> 요소에 launchMode 속성을 추가함으로써 액티비티가 태스크와 어떤 관계를 맺게 될지를 지정할 수 있습니다.

launchMode 속성은 액티비티가 태스크에 어떤 방식으로 추가될 지를 지정합니다. 아래에 4가지 launchMode 속성값에 대해 간단히 설명하겠습니다:

"standard" (기본값)
시스템은 액티비티를 실행하려고 인텐트를 넘겨주고 있는 그 태스크에서 액티비티의 새 객체를 생성합니다. 액티비티는 여러번 객체를 생성할 수 있고, 각각은 서로 다른 태스크에 들어갈 수도 있으며, 하나의 태스크가 (한 액티비티에 대한) 여러개의 객체를 포함할 수 있습니다.

"singleTop"
어떤 액티비티의 객체가 이미 현재 태스크의 top에 있는데 그 액티비티에 대하여 startActivity()를 호출할 경우, 시스템은 액티비티의 새 객체를 생성하는 대신에 태스크의 top에 있는 액티비티 객체의 onNewIntent()를 호출함으로써 재사용합니다. 위의 standard와 마찬가지로, 액티비티는 여러번 객체를 생성할 수 있고, 각각은 서로 다른 태스크에 들어갈 수도 있으며, 하나의 태스크가 (한 액티비티에 대한) 여러개의 객체를 포함할 수 있습니다. 하지만, 태스크의 top에 하나의 액티비티에 대한 2개의 객체가 들어가는 상황만은 허용하지 않으며, 이미 있는 객체를 재사용하도록 하는 것입니다.

예를 들어, 태스크의 백스택이 A, B, C, D라는 액티비티로 채워져있다고 가정해 봅시다(스택은 A-B-C-D이며, D가 top입니다). 그리고 시스템에게 D를 실행해 달라는 인텐트가 왔다고 합시다. 만약 D의 시작 모드(launchMode)가 "standard"라면, D의 새 객체가 생성되어 백스택에 추가되고 스택은 A-B-C-D-D가 될 것입니다. 하지만 D의 시작 모드가 "singleTop"이라면, 백스택의 top에 이미 D의 객체가 있기 때문에 그 객체가 onNewIntent()를 통해 해당 인텐트를 받을 것이고 스택은 그대로 A-B-C-D가 될 것입니다. 하지만 시작되는 액티비티가 D가 아닌 B라면 시작 모드가 "singleTop"이라 하더라도, 새 객체가 생성되어 백스택에 추가될 것이고 스택은 A-B-C-D-B가 될 것입니다. 

메모: 액티비티의 새 객체가 생성되어 태스크에 추가되었을 때, 사용자가 뒤로가기버튼을 누르면 이전 액티비티로 돌아갈 수 있었습니다. 그러나 시작 모드가 "singleTop"이라서 태스크의 top에 있는 액티비티가 재사용된 상황이라면, 사용자가 뒤로가기버튼을 눌렀을 때, 재사용되기 전 상태로 돌아가는 것이 아니라, 현재 액티비티가 종료되고 그 이전의 액티비티가 재시작될 것입니다.

"singleTask"
시스템은 새 태스크를 생성하고, 태스크의 root에 액티비티의 새 객체를 생성합니다. 하지만, 태스크에 이미 해당 액티비티의 객체가 존재한다면, 액티비티의 새 객체를 생성하지 않고 기존 객체의 onNewIntent()를 호출함으로써 재사용합니다. 하나의 액티비티에 대한 객체는 오직 하나만 존재할 수 있습니다.

메모: 비록 액티비티가 새 태스크에 생성되었다 하더라도, 뒤로가기버튼을 누르면 이전 액티비티로 돌아갈 수 있습니다.

역자의 추가글: singleTask에 대한 설명이 오해의 소지가 많아 몇가지 설명을 덧붙입니다. 시스템은 시작 모드가 singleTask인 액티비티를 실행할 때 taskAffinity가 같은 태스크가 있는지를 확인하여, 만약 있다면 그 태스크에 액티비티의 객체를 생성하여 넣으며, 이때의 위치는 태스크의 root가 아니겠죠. 그리고 만약 taskAffinity가 같은 태스크가 없다면, 새 태스크를 생성하고 그 태스크의 root에 액티비티의 새 객체를 생성합니다. taskAffinity에 대한 자세한 내용은 아래의 친밀도(affinity) 다루기에서 학습하실 수 있습니다.

"singleInstance"
자신이 속한 태스크에 다른 액티비티의 객체들을 들어오지 못하게 한다는 점을 제외하고는 "singleTask"와 비슷합니다. 하나의 액티비티에 대한 객체는 오직 하나이고, 자신이 속한 태스크에는 오직 자신만 있을 뿐입니다. 이 액티비티에서 다른 액티비티를 실행한다면, 실행된 액티비티는 다른 태스크에 속하게 됩니다.

다른 예로, 인터넷 브라우저 앱은 브라우저 액티비티의 시작 모드를 singleTask로 지정했기 때문에, 내 앱에서 브라우저를 실행할 때 브라우저 액티비티가 내 앱의 태스크에 들어가지 않습니다. 대신에 새 태스크를 생성하거나, 이미 존재하는 브라우저의 태스크가 후면(background)에 있다면 전면(foreground)으로 가져와서 재사용합니다. 

액티비티가 새 태스크에 들어갔건, 기존의 태스크에 들어갔건 상관없이 사용자가 뒤로가기버튼을 누르면 현재 액티비티가 종료되고 이전 액티비티가 실행됩니다. 하지만, 시작 모드가 singleTask인 액티비티를 실행하는데, 그 액티비티는 이미 후면에 있는 태스크에 존재하고 그 태스크에는 다른 액티비티도 있는 경우, 시스템은 후면에 있던 태스크를 통째로 전면으로 가져옵니다. 이 때, 현재의 백스택의 top에다가, 후면에서 가져온 태스크의 액티비티들을 모두 집어넣습니다. 아래의 그림4가 이 과정을 보여줍니다.


그림 4. 시작 모드가 "singleTask"인 액티비티를 백스택에 추가하는 모습을 보여줍니다. 만약 액티비티가 후면에 있는 다른 태스크에 이미 존재한다면, 그 태스크가 통째로 현재의 태스크의 top으로 옮겨오게 됩니다.

매니페스트 파일에 시작 모드를 설정하는 방법에 대한 더 자세한 내용은, <activity>의 android:launchMode에서 학습하실 수 있습니다.

메모: 매니페스트 파일에서 지정한 launchMode는, 액티비티를 실행할 때 넘겨주는 인텐트에 포함된 플래그값으로 덮어씌워질 수 있습니다. 다시 말해서, 인텐트에 포함된 (시작 모드에 대한) 플래그값이 매니페스트 파일에 지정된 시작 모드보다 우선합니다.


인텐트의 플래그 사용하기

액티비티를 실행할 때, startActivity()에 넘겨주는 인텐트에 플래그를 설정함으로써 액티비티의 시작 모드를 바꿀 수 있습니다. 그 플래그값들은 아래와 같습니다:

FLAG_ACTIVITY_NEW_TASK
실행하려는 액티비티의 객체가 존재하지 않으면 새 태스크의 root에 액티비티의 새 객체가 들어갑니다. 하지만 후면(background)에 이미 그 액티비티를 담고 있는 태스크가 있다면, 전면(foreground)으로 가져와서 마지막 상태를 복구하고 인텐트를 onNewIntent()에 넘겨줍니다. 하지만 후면에 있던 태스크의 top에 있던 액티비티가 실행되기 때문에, 실행하고자 했던 액티비티와 실행된 액티비티가 다를 수도 있습니다. 따라서 대부분의 경우 FLAG_ACTIVITY_CLEAR_TOP과 함께 사용합니다.
이것(FLAG_ACTIVITY_NEW_TASK + FLAG_ACTIVITY_CLEAR_TOP)은 위에서 학습한 "singleTask"의 역할과 같습니다.

FLAG_ACTIVITY_SINGLE_TOP
현재 실행중인 액티비티(현재 태스크의 top에 있는)에서 다시 동일한 액티비티를 실행하려고 하는 경우, 새 객체를 생성하는 대신 현재 액티비티의 onNewIntent()를 호출함으로써 액티비티를 재사용합니다.
이것은 위에서 학습한 "singleTop"의 역할과 같습니다.

FLAG_ACTIVITY_CLEAR_TOP
만약 실행하려고 하는 액티비티의 객체가 이미 현재 태스크에 존재한다면, 새 객체를 생성하는 대신, 그 액티비티 객체의 위에 있는 다른 액티비티들을 모두 제거하여 그 액티비티가 태스크의 top이 되도록 합니다. 
이것과 역할이 같은 launchMode값은 없습니다.

다시 말하지만, FLAG_ACTIVITY_CLEAR_TOP은 대부분의 경우 FLAG_ACTIVITY_NEW_TASK와 함께 사용됩니다. 이 경우, 실행하려고 하는 액티비티가 후면(background)에 있는 태스크에 존재한다면, 그 태스크를 전면(foreground)으로 가져오고, 해당 액티비티 위에 다른 액티비티들이 있다면 모두 제거하여 그 액티비티가 실행되도록 합니다.

메모: 인텐트의 플래그가 FLAG_ACTIVITY_CLEAR_TOP인데, 만약 실행하려는 액티비티의 시작 모드가 "standard"이면, 기존에 있던 액티비티의 객체를 제거후 다시 생성하여 태스크에 넣습니다. 시작 모드가 "standard"일때는 시스템이 새 액티비티를 일단 생성하고 보기 때문입니다. 


친밀도(affinity) 다루기

친밀도(affinity)는 액티비티가 어떤 태스크에 속하고 싶어하는가를 지정할 수 있습니다. 기본적으로 같은 앱에 속한 액티비티들은 같은 친밀도(패키지명)를 가지고 있기 때문에, 같은 태스크에 속하고 싶어하지만, 액티비티의 친밀도를 바꿀 수 있습니다. 서로 다른 앱의 액티비티들이 같은 친밀도를 가질 수도 있고, 하나의 앱의 액티비티들이 다른 친밀도를 가질 수도 있습니다. 

친밀도는 매니페스트 파일의 <activity> 요소에 taskAffinity 속성을 통해 지정할 수 있습니다.

taskAffinity 속성의 기본값은 <manifest>에 선언된 패키지명이며 이것이 앱의 친밀도이기 때문에, 내 앱에서 다른 태스크를 생성하고자 taskAffinity를 지정하는 것이라면 패키지명과 다른 문자열로 지정해야 합니다.

친밀도는 아래의 2가지 상황에서 작동합니다:

  • 액티비티를 실행하는 인텐트에 FLAG_ACTIVITY_NEW_TASK가 포함되어 있을 때.
    기본적으로 새 액티비티는, 생성될 때 startActivity()를 호출한 액티비티와 같은 태스크에 들어갑니다. 하지만, startActivity()에 넘긴 인텐트에 FLAG_ACTIVITY_NEW_TASK가 포함되어 있다면, 시스템은 액티비티의 친밀도와 같은 태스크를 찾습니다. 만약 그러한 태스크가 존재한다면 그 태스크에 새 액티비티를 넣고, 존재하지 않는다면 새 태스크를 생성합니다. 
    만약 이 플래그를 이용하여 새 태스크를 만들었는데 사용자가 홈버튼을 눌러 빠져나간 경우 다시 그 태스크로 가고 싶다면, 그 태스크로 이동할 수 있는 방법이 존재해야 한다는 것을 개발시 고려해야 합니다. (노티피케이션 매니저와 같은) 일부 요소들은 항상 다른 태스크에서 액티비티를 실행하기 때문에, startActivity()에 넘기는 인텐트에 항상 FLAG_ACTIVITY_NEW_TASK를 설정합니다. 만약 이러한 방식으로 액티비티가 실행되었다면, 사용자가 그 태스크로 이동할 수 있는 독립적인 방법이 있어야 합니다. 예를 들면, 태스크의 root에 있는 액티비티가 매니페스트에 선언될 때 인텐트 필터에 CATEGORY_LAUNCHER가 선언되어 있다면, 런처화면에 아이콘이 제공되어 그 아이콘을 누르면 그 태스크로 이동하게 됩니다. 태스크를 시작하는 것과 관련한 자세한 내용은 태스크 시작하기에서 학습하실 수 있습니다.

  • <activity>의 allowTaskReparenting 속성값이 "true"일 때.
    이 경우에, 액티비티가 현재는 자신이 시작된 태스크에 있다하더라도, 자신과 친밀도가 같은 태스크가 전면(foreground)으로 나올 때, 그 태스크로 이동할 수 있습니다. 
    예를 들어, 어떤 여행 앱에, 선택된 도시의 날씨 정보를 보여주는 액티비티가 있다고 가정해 봅시다. 그 액티비티는 여행 앱의 다른 모든 액티비티들과 같은 친밀도를 가지고 있고, allowTaskReparentingtrue로 지정되어 있습니다. 내 앱에서 그 날씨 정보 액티비티를 실행하면, 그 액티비티는 그것을 실행한 내 액티비티의 태스크에 들어갈 것입니다. 하지만, 여행 앱의 태스크가 전면(foreground)으로 나올 때, 그 날씨 정보 액티비티는 여행 앱의 태스크로 옮겨질 것입니다.

팁: 만약 하나의 .apk파일이 사용자 관점에서 2개 이상의 "앱"으로 구성되어 있다면, 각 "앱"에 해당하는 액티비티들에 대하여 각각의 친밀도를 부여할 수도 있을 것입니다.


백스택 비우기

만약 사용자가 태스크에서 오랜 시간 벗어나 있었다면, 시스템은 그 태스크의 root 액티비티만 남기고 나머지는 모두 제거합니다. 사용자가 다시 그 태스크에 돌아왔을때는 root에 있던 액티비티 하나만 남아있겠죠. 시스템이 이렇게 동작하는 이유는, 사용자가 어떤 태스크에서 벗어나 오랜 시간이 지났다면 아마도 이전에 뭘했는지를 잊었을 것이고 다시 돌아왔을때는 새로 시작하는 것이 나을 것이라고 판단하기 때문입니다.

하지만 이러한 동작도 액티비티의 몇가지 속성을 통해 바꿀 수 있습니다:

alwaysRetainTaskState
태스크의 root 액티비티의 이 속성이 "true"이면, 오랜 시간이 지나도 액티비티들을 제거하지 않고 모든 액티비티들을 유지합니다.

clearTaskOnLaunch
태스크의 root 액티비티의 이 속성이 "true"이면, 사용자가 이 태스크에서 벗어났다가 다시 돌아올 때, (벗어나 있던 시간과 상관없이) root 액티비티만 남기고 나머지는 모두 제거합니다. 달리 말하면, alwaysRetainTaskState 속성과 반대되는 속성이라고 할 수 있습니다. 사용자가 아주 잠깐 벗어났다가 돌아오더라도 태스크는 초기 상태가 되는 것입니다. 

finishOnTaskLaunch
이 속성은 clearTaskOnLaunch와 비슷하지만, 태스크가 아닌 하나의 액티비티(root 액티비티 포함)에 대해서 동작합니다. 이 속성이 "true"일 때, 액티비티는 오직 태스크가 전면(foreground)에 있을 때만 태스크 안에 유지될 수 있습니다. 사용자가 태스크에서 벗어났다가 돌아올 경우에 그 액티비티는 제거됩니다.


태스크 시작하기

태스크의 진입점을 나타내기 위해, 액티비티에 액션값이 "android.intent.action.MAIN"이고 카테고리가 "android.intent.category.LAUNCHER"인 인텐트 필터를 선언할 수 있습니다. 예제 코드:

<activity ... >
   
<intent-filter ... >
       
<action android:name="android.intent.action.MAIN" />
       
<category android:name="android.intent.category.LAUNCHER" />
   
</intent-filter>
    ...
</activity>

위의 인텐트 필터는 액티비티에 설정된 아이콘과 이름을 앱의 런처화면에 보이게 해줍니다. 이는 사용자가 그 액티비티를 실행할 수 있도록 하고, 액티비티 실행후 태스크에서 벗어나더라도 언제던지 런처를 통해 태스크로 돌아올 수 있게 합니다. 

위에서 두번째로 얘기한, 태스크로 돌아올 수 있게 한다는 기능이 중요합니다: 사용자들은 태스크에서 벗어났다가 액티비티 런처를 통해 다시 돌아올 수 있어야 합니다. 이 때문에, 새 태스크를 생성할 수 있는 2가지 시작 모드인 "singleTask""singleInstance"는, 액티비티가 ACTION_MAINCATEGORY_LAUNCHER 필터를 가지고 있을 때만 사용하는 것이 좋습니다. 이러한 필터가 없이 실행된 경우를 상상해 보세요: 시작 모드가 "singleTask"인 액티비티를 실행하여 새 태스크가 생성되었고, 사용자가 거기서 홈버튼을 눌러 벗어났다면, 이제 그 태스크는 후면(background)으로 들어가게 되고 더이상 보이지 않습니다. 그 액티비티에는 위의 인텐트 필터가 없다고 했기 때문에 런처 화면에 보이지 않을 것입니다. 따라서 (내 앱이 태스크를 실행하지 않는 한) 이제는 사용자가 그 태스크로 돌아갈 방법이 없습니다.


Posted by 개발자 김태우
,