Android Application Component
안드로이드 어플리케이션을 구성하는 4가지 컴포넌트
- Activity
- Broadcast Receiver
- Content Provider
- Service
모든 컴포넌트들은 Main Thread 에서 실행됩니다. 따라서 Service 사용시 Thread 작업이 필요한 경우 별도의 작업 Thread를 만들어 사용해야 합니다.
Service 란?
Service는 Android Application을 구성하는 4가지 Component 중 하나입니다.
Activity 처럼 사용자와 상호작용하는 컴포넌트가 아니고 Background에서 동작하는 컴포넌트입니다.
Activity 화면에서의 동작 뿐만 아니라 Acitivity가 종료되어 있는 상태에서도 동작하기 위해 사용되는 컴포넌트입니다.
Service 사용 방법
Service 의 종류는 2가지가 있습니다.
-
백그라운드 데몬 : 배경에서 계속 실행되는 프로세스이다. 클라이언트가 가동시켜 놓기만 하면 사용자의 명령이 없어도 지속적으로 실행된다. MP3 플레이어가 대표적인 예인데 화면에 보이지 않지만 배경에서 노래를 계속 재생한다. 전통적인 의미의 서비스에 가장 가까운 형태이다.
-
바운드 서비스 : 클라이언트를 위해 특정한 기능을 제공하는 역할을 한다. 자신의 기능을 메서드로 노출시키며 클라이언트는 메서드를 호출함으로써 서비스를 이용한다. 다른 운영체제의 COM, CORBA에 대응되는 개념이다.
Service의 LifeCycle
사용 형태가 조금 다르지만 백그라운드 데몬과 바운드 서비스는 둘 다 Service 클래스를 파생시켜 작성한다.
사용 용도에 따라 가장 뚜렷한 차이가 발생하는 부분은 서비스의 생명주기(Life Cycle)이다.
서비스를 시작할 때 onCreate, 종료될 때 onDestroy 메서드가 호출되는 것은 동일하지만 중간에 호출되는 메서드가 다르다.
데몬일 때는 백그라운드 서비스를 시작하라는 onStartCommand 메서드가 호출되고 원격 호출일 때는 클라이언트에게 인터페이스를 노출하는 onBind 메서드가 호출된다.
1. 백그라운드 데몬
클라이언트가 startService 메서드로 서비스를 기동시키면 onStartCommand 메서드가 호출되는데 이후부터 백그라운드 동작을 본격적으로 시작한다.
int onStartCommand(Intent intent, int flags, int startId)
-
intent는 클라이언트가 서비스를 시작할 때 전달한 인텐트이다. 만약 프로세스가 강제 종료된 후 재시작되는 것이라면 이때는 null이 전달된다. (START_STICKY 일 때)
-
flags는 서비스의 요청에 대한 추가 정보이다.
-
startId는 서비스 요청에 대한 고유한 식별자이다. stopSelfResult 메서드로 서비스가 스스로 종료할 때 이 식별자가 사용된다.
-
리턴값은 서비스의 동작 모드를 지정하는데 주로 접착식과 비접착식 둘 중 하나를 리턴한다. 접착식은 시작과 종료가 명시적이고 비접착식은 명령을 처리하는 동안만 실행된다는 점이 다르며 프로세스가 장제 종료될 때 재시작 여부가 다르다.
서비스의 동작 모드
START_NOT_STICKY
onStartCommand()에서 이 값이 리턴된 후, 시스템이 서비스를 종료시켰다면, 다시 서비스가 실행될 수 있는 여건이 되더라도 서비스를 다시 생성하지 않습니다. 이것은 불필요하게 서비스가 실행되는 것을 막는 가장 안전한 방법이지만, 완료되지 못한 작업들 또한 더이상 수행되지 않습니다.
START_STICKY
onStartCommand()에서 이 값이 리턴된 후, 시스템이 서비스를 종료시켰다면, 다시 서비스가 실행될 수 있는 여건이 되었을 때, 서비스를 생성하고 onStartCommand()를 호출해 줍니다(하지만 종료전의 마지막 인텐트를 다시 전달해 주지는 않습니다). 이것은 미디어 플레이어처럼 계속 실행되길 원하는 경우에 적합합니다.
START_REDELIVER_INTENT
onStartCommand()에서 이 값이 리턴된 후, 시스템이 서비스를 종료시켰다면, 다시 서비스가 실행될 수 있는 여건이 되었을 때, 서비스를 생성하고 onStartCommand()를 호출하며 종료전의 마지막 인텐트를 인자로 넘겨줍니다. 이것은 파일 다운로드처럼 반드시 완료가 되어야 하는 경우에 적합합니다.
서비스는 프로세스의 메인 스레드에서 실행되므로 너무 오래 시간을 끌거나 브로킹해서는 안된다. 즉시 리턴해야 하므로 오래 걸리는 작업이나 지속적으로 해야 할 작업은 반드시 스레드로 분리해야 한다.
Manifest 등록
서비스는 응용 프로그램을 구성하는 컴포넌트이므로 매니페스트에 등록해야 한다.
<service android:name="com.sample.MyService"
android:enabled="true">
<intent-filter>
<action android:name="com.sample.NEWS" />
</intent-filter>
</service>
태그 안에 서비스에 대한 정보를 등록한다.
- name 속성에 서비스 클래스의 이름을 지정
- enabled 속성에 서비스 사용 여부를 지정한다. enabled의 디폴트가 true여서 굳이 속성을 밝힐 필요는 없지만 차후 이 서비스를 잠시 무력화시켜 놓고 싶다면 서비스 자체를 제거할 필요 없이 enabled 속성을 조정함으로써 간단하게 사용 중지 시킬 수 있다.
인텐트 필터의 액션에는 서비스의 이름을 지정한다.
- 내부에서만 사용하려면 클래스명으로 호출 가능하므로 굳이 이름을 줄 필요 없지만 외부 패키지에서 서비스를 호출하려면 이름이 필요하다.
- 서비스 액션명은 시스템 전역적으로 유일해야 하므로 중복 방지를 위해 패키지명을 포함시키는 것이 좋다.
서비스 시작/중지
클라이언트에서 서비스를 시작 및 중지할 때는 다음 메서드를 호출한다.
ComponentName startService(Intent service)
boolean stopService(Intent service)
외부 패키지나 외부 프로그램에서 호출
서비스는 유일한 이름이 있으므로 외부 패키지나 외부 프로그램에서도 호출이 가능하다. 외부에서 다른 패키지에 속한 클래스를 직접 참조할 수 없으므로 이름을 사용하여 다음과 같이 호출한다.
intent = new Inent("com.sample.NEWS");
startService(intent);
그러나 이 방법은 관련이 없는 외부에서 서비스를 마음대로 기동시키는 것이라 보안상 위험할 수 있으며 권장하지 않는다.
Note: 5.0 이후에서는 비권장이 아니라 아예 암시적 인텐트를 사용할 수 없도록 보안이 강화되었으며 이 코드를 실행하면 "Service intent must be explicit" 예외가 발생한다. 이후 서비스는 반드시 명시적 인텐트로만 호출해야 한다.
2. IntentService
서비스는 호출 앱의 메인 스레드상에서 실행된다. 따라서 서비스가 시간을 너무 오래 끌면 그동안 사용자의 입력에 반응할 수 없다. 아주 짧은 작업이라면 상관없지만 오래 걸린다면 별도의 스레드로 분리하는 것이 바람직하다.
서비스에서 하는 작업은 대체로 시간이 오래 걸리므로 스레드를 생성하여 작업을 분담하는 것이 일상적이다.
보통 멀티 스레드로 작성하며 구조가 뻔하므로 이런 구조를 자동으로 처리하는 IntentService 클래스
가 제공된다.
Service의 서브 클래스이며 인텐트로 요청을 전달받아 별도의 스레드를 생성하고 onHandleIntent 메서드를 호출한다.
뻔한 코드를 미리 구현해 놓은 편의 메서드이므로 생명 주기 메서드는 일체 구현할 필요없이 onHandleIntent에서 요청에 대한 처리만 수행하면 된다.
onHandleIntent 메서드가 리턴하면 서비스는 알아서 종료되므로 stopSelf 메서드도 호출할 필요 없다. 한번에 하나의 요청만 처리할 수 있으며 시간이 좀 오래 걸리더라도 분리된 스레드에서 실행되므로 메인 스레드의 작업을 방해하지 않는다.
3. 바운드 서비스
바운드 서비스는 특정 기능을 제공하는 메서드를 클라이언트에게 노출한다.
클라이언트는 서비스에 연결하여 메서드를 호출함으로써 통신을 수행하며 자신이 직접 구현하지 않은 기능을 사용한다. 바운드 서비스는 일련의 기능 집합을 제공하는 라이브러리와 유사하다.
Binder
로컬에서 호출되는 바운드 서비스는 Binder 클래스를 확장하는 방식으로 구현한다. getService 메서드를 재정의하여 자신이 속한 서비스 객체를 리턴하며 서비스는 Binder 객체를 멤버로 생성해 놓고 onBind 메서드에서 이 객체를 리턴하여 클라이언트에게 자신을 노출한다.
public class CalcService extends Service {
public class ClacBinder extends Binder {
CalcService getService() { return CalcService.this }
}
ClacBinder mBinder = new ClacBinder();
public IBinder onBinder(Intent intent) {
return mBinder;
}
public int getLCM(int a, int b) throws RemoteException {
...
return i;
}
public boolean isPrime(int n) throws RemoteException {
...
return true;
}
}
서비스 연결/해제
클라이언트에서 서비스에 연결하거나 해제할 때 다음 메서드를 호출하여 바인딩한다. 바인딩이란 클라이언트와 서비스를 연결하는 동작이다.
boolean bindService(Intent service, ServiceConnection conn, int flags)
void unbindService(ServiceConnection conn)
- 첫 번째 인수 service는 사용하고자 하는 서비스를 지정하는데 같은 패키지에 있으면 클래스명으로 지정하고 외부에 있다면 서비스의 액션명을 사용한다.
- 두 번째 인수 conn은 서비스가 연결, 해제될 때의 동작을 정의하는 연결 객체이다. 서비스는 사용하는 클라이언트는 ServiceConnection 인터페이스를 구현하는데 클라이언트와 서비스가 연결되거나 해제될 때 호출되는 콜백 메서드를 정의한다.
- 마지막 인수 flag는 서비스 바인딩 방식을 지정하는데 통상 BIND_AUTO_CREATE로 지정하여 서비스를 자동으로 기동시킨다.
public class MainActivity extends Activity {
...
public void onResume() {
super.onResume();
Intent intent = new Intent(this, CalcService.class);
bindService(intent, serviceConnection, BIND_AUTO_CREATE);
}
public void onPause() {
super.onPause();
unbindService(serviceConnection);
}
ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
mCalc = ((CalcService.CalcBinder)binder).getService();
}
public void onServiceDisconnected(ComponentName className) {
mCalc = null;
}
}
}
서비스가 연결될 때 onServiceConnected 메서드가 호출되며 이때 인수로 전달되는 binder 객체를 서비스 객체 타입으로 캐스팅한다. 이 서비스 객체를 구해두면 이후부터 이 객체의 메서드를 호출함으로써 서비스의 기능을 사용한다.