(원문: 

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 개발자 김태우
,