안드로이드 4대 컴포넌트 - Service_1(service?)

권민주·2024년 1월 10일

안드로이드

목록 보기
3/23
post-thumbnail

안드로이드에는 필수적인 앱 기본 구성 요소로 activity, service, broadcast receiver, content provider 4가지를 가집니다. 그 중 하나인 service의 기본 개념에 대해 설명하겠습니다.


1. Service란

1) service?

service는 여러 가지 이유로 백그라운드에서 앱을 계속 실행시키는 다목적 진입점입니다. 이는 UI없이 백그라운드에서 실행되는 구성 요소로, 오랫동안 실행되는 작업을 수행하거나 원격 프로세스를 위한 작업을 수행합니다.

예를 들어 service는 사용자가 다른 앱에 있는 동안에도 백그라운드에서 음악 재생과 파일 I/O 수행, 그리고 사용자와 activity간의 상호작용을 방해하지 않고 네트워크를 통해 데이터를 가져올 수도 있습니다. activity 같은 다른 component가 service를 시작시켜 다른 앱으로 전환되어도 사라지지 않은 채 백그라운드에서 계속 실행할 수 있고, 자신에게 바인딩하여 상호작용하게 할 수도 있습니다. service는 사용자와 애플리케이션 상호작용 여부 상관없이 백그라운드에서 실행될 수 있는 구성 요소이기에 필요한 경우에만 사용해야 합니다.

+ Service vs Thread

/ServiceThread
의미백그라운드에서 앱을 실행시키는 구성 요소백그라운드에서 프로세스 내 실행되는 흐름의 단위
역할오랫동안 실행되는 작업 수행동시에 여러 작업 실행
실행메인 스레드자체 스레드
생명주기액티비티 생명주기와 독립적인 자체 생명주기를 가짐. 액티비티 파괴되도 계속 실행액티비티 생명주기에 실행.
액티비티가 파괴되면 종료 및 중지
메모리 부족 시재시작 가능재시작 불가능
예시푸쉬알림, 음악 플레이어, 백그라운드에서도 파일 I/O어플 내 음악 플레이, 어플 내 파일 I/O

참고 : https://onlyfor-me-blog.tistory.com/393

2) service와 스레드

사용자가 애플리케이션과 상호작용할 때 기본 스레드 밖에서 작업을 수행해야 하는 경우, 새 스레드를 생성해야 합니다. 예를 들어 activity가 실행되는 중에만 음악을 재생하고자 하는 경우, onCreate() 안에 스레드를 생성하여 onStart()에서 실행한 다음 onStop()에서 중단합니다.

service는 기본적으로 자신의 호스팅 프로세스의 기본 스레드에서 실행됩니다. service는 자신의 스레드를 직접 생성하지 않으며, 특별히 지정하지 않는 한 별도의 프로세스에서 실행되지도 않습니다. service가 MP3 재생 또는 네트워킹 같이 CPU 집약적인 작업을 수행할 예정이거나 차단 작업을 수행할 예정인 경우 성능을 위해 service 내에 새 스레드를 생성해야 합니다. 별도의 스레드를 사용하면 '애플리케이션이 응답하지 않습니다(ANR)' 오류가 일어날 위험을 줄일 수 있으며, 애플리케이션의 기본 스레드는 activity와 사용자의 상호작용 전용으로 유지되어야 합니다.

3) 사용자에게 알림 전송

service가 실행되고 있을 때 사용자에게 토스트 알림 또는 상태 표시줄 알림 등을 사용해 이벤트를 알릴 수 있습니다.

토스트 알림은 현재 창의 표면에 잠깐 나타났다가 사라지는 메시지입니다.

상태 표시줄 알림은 상태 표시줄에 메시지가 포함된 아이콘을 제공하여 사용자가 이를 선택하면 activity 시작 같은 활동을 할 수 있게 합니다. 보통 파일 다운로드 완료 같은 일종의 백그라운드 작업이 완료되었고 이제 사용자가 그에 대한 조치를 취할 수 있는 경우에는 상태 표시줄 알림을 사용하는 것이 가장 좋습니다. 사용자가 확장된 뷰의 알림을 선택하면 해당 알림은 다운로드한 파일 보기 같은 activity를 시작할 수 있습니다.

4) service 유형

service에는 시스템에게 앱 관리 방법을 지시하는 두 가지 유형이 있는데 started servicebound service입니다. started service 작업이 완료될 때까지 해당 service를 계속 실행하라고 시스템에 지시합니다. 사용자가 앱에서 나간 후에도 음악을 재생하기와 백그라운드에서 일부 데이터를 동기화하기가 있습니다. 음악 재생과 백그라운드에서 데이터 동기화하기started service에서 다시 두가지 유형으로 나뉘는데, 이는 시스템이 service를 처리하는 방식이 다릅니다.

(ⅰ) Started service-foreground service

우선 음악 재생의 경우 foreground service입니다. 사용자가 바로 인식할 수 있는 작업이기 때문에, 앱은 사용자에게 실행 중이라는 알림을 보내면서 시스템에는 포그라운드로 옮기라는 지시를 합니다. 이때 알림 표시는 필수적입니다. service를 포그라운드에서 제거하거나 중지하지 않는 한 알림을 계속 표시해야합니다.

시스템은 이러한 종류의 service 프로세스가 계속 실행되는 것을 우선시 해야합니다. 이 service가 사라지면 사용자는 불만을 가지게 되기 때문입니다. 그렇기에 foreground service사용자가 앱과 상호작용하지 않을 때도 계속 실행됩니다. 필요시 foreground service를 직접 사용하는 것보다 WorkManager API를 사용하기도 합니다.

(ⅱ) Started service-background service

데이터 동기화 같이 정기적인 백그라운드에서의 활동은 background service입니다. 사용자가 이 작업이 실행되고 있다고 직접 인식할 수 없기 때문에 시스템은 좀 더 자유롭게 프로세스를 관리할 수 있습니다. 어느 앱이 압축 저장하는데 service를 사용했다면 이것은 대개 background service입니다.

이 service는 도중에 종료될 수도 있는데 사용자와 좀 더 직접적인 관련이 있는 작업에 RAM이 필요할 경우가 있기 때문입니다. 물론 나중에 service를 다시 시작할 수도 있습니다. 앱이 API 레벨 26 이상을 대상으로 한다면, 앱이 포그라운드에 있지 않을 때 시스템에서 background service 실행를 제한합니다. 백그라운드에서는 위치 정보에 엑세스하지 못하기 때문입니다. 이와 같은 경우에서는 WorkManager 사용해야 합니다.

(ⅲ) Bound service

bound service 다른 앱이나 component 심지어 다른 service에서 bindService()를 호출하여 해당 service를 바인딩하고 싶을 때 실행됩니다. bound service클라이언트-서버 인터페이스를 제공하여, component가 service와 상호작용하게 하고 응답을 보내 결과를 받을 수 있으며 여러 프로세스에 걸쳐 프로세스 간 통신(IPC)으로 수행할 수도 있습니다. 이는 service가 다른 프로세스에 API를 제공하고 시스템은 이러한 프로세스 사이의 종속성이 있음을 알게 됩니다.

예를 들어 프로세스 A가 프로세스 B의 service에 바인딩되어 있을 경우, 시스템은 프로세스 A를 위해 프로세스 B 및 그 service를 실행해야 한다고 인식하게 됩니다. 또한 사용자가 프로세스 A에 관심을 기울이고 있다면 시스템에서 프로세스 B도 사용자가 관심을 기울이는 것처럼 취급해야 합니다.

bound service는 다른 애플리케이션 component에 바인딩되어 있는 경우에만 실행됩니다. 여러 componet가 service에 한꺼번에 바인딩될 수 있지만 바인딩이 해제되면 해당 service는 소멸됩니다.

service는 유연하기 때문에 각종 고차원적 시스템 개념에서 매우 유용한 기본 구성 요소로 사용되었습니다. 라이브 배경화면, 알림 리스너, 화면 보호기, 입력 메서드, 접근성 서비스 및 여러 가지 기타 핵심 시스템 기능들은 애플리케이션에서 구현되고 애플리케이션이 실행될 때 시스템에서 바인딩되는 service로 빌드됩니다.

그러므로 serivce는 총 foregound, background, bound 세 가지 유형으로 나뉘게 됩니다. 다른 유형으로 나뉘게 된다고 별도의 service인 것은 아닙니다. started servicebound service를 동시에 동작할 수 있습니다. 즉 무한히 실행되도록 시작하면서 바인딩 할 수 있습니다. 이는 단순히 콜백 메서드 구현 여부에 좌우되는 문제입니다. onStartCommand()는 component가 service를 시작하게 하고 onBind()는 바인드를 허용합니다. service가 시작되었든 바인딩 되었든 아니면 모두이든, component 혹은 별도의 애플리케이션에서도 해당 service를 사용할 수 있으며 이는 어느 component든 Intent로 activity를 시작시킬 수 있는 것과 같습니다. 그러나 매니페스트에서 serivce를 비공개로 선언하여 다른 애플리케이션으로부터의 액세스를 차단할 수도 있습니다.

5) manifest 선언

activity 및 다른 component와 마찬가지로, service는 모두 애플리케이션의 매니페스트 파일에서 선언해야 합니다. <service> 요소를 <application> 요소의 하위로 추가하면 됩니다.

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

<service> 요소에는 service를 시작하는 데 필요한 권한과 service를 실행해야 하는 프로세스 등의 특성을 정의할 수 있습니다. android:name이 유일 필수 특성인데 service의 클래스 이름을 나타냅니다. 이 이름을 바꾸게 되면, service를 시작하거나 바인딩할 명시적 인텐트에 대한 종속성 때문에 코드가 깨질 위험이 있어 애플리케이션 출시 후에는 이 이름을 그대로 두어야 합니다. android:exported 특성을 포함하고 이를 false로 설정하면 해당 service를 본인의 앱에만 사용 가능하게 할 수 있습니다. 이렇게 하면 다른 앱이 여러분의 service를 시작하지 못하도록 효과적으로 방지하며, 이는 명시적 인텐트를 사용하는 경우에도 적용됩니다.

앱의 보안을 지키기 위해서는 service를 시작할 때에는 항상 명시적 인텐트만 사용하고 service에 대한 인텐트 필터는 선언하지 말아야 합니다. 암시적 인텐트를 사용하여 service를 시작하면 보안이 위험해집니다. 인텐트에 어느 service가 응답할 것인지 확신할 수 없고, 사용자는 어느 service가 시작되는지 볼 수 없기 때문입니다. Android 5.0, API 레벨 21부터 시스템은 개발자가 암시적 인텐트로 bindService()를 호출하면 예외를 발생시킵니다.

사용자는 기기에서 어떤 service가 실행되는지 볼 수 있습니다. 정체를 모르거나 신뢰할 수 없는 service를 보면 사용자가 service를 중단할 수 있습니다. 사용자에 의해 우발적으로 service가 중단되는 불상사를 막으려면 앱 매니페스트의 <service> 요소에 android:description 특성을 추가해야 합니다. 여기에 service가 하는 일과 service의 장점을 설명하는 간략한 문장을 기재하면 됩니다.

6) 주요 메서드

serivce를 생성하려면, Service의 하위 클래스를 생성하거나 기존 하위 클래스 중 하나를 사용해야 합니다. 구현에서는, 수명 주기의 주요 부분을 처리하는 일부 콜백 메서드를 재정의해야 하며 필요시 service에 바인딩할 component에 대한 메커니즘을 제공해야 합니다. 다음은 재정의가 필요한 중요한 콜백 메서드입니다.

(ⅰ) onStartCommand()

activity 같은 다른 component가 service를 시작하고자 할 때, startService()을 부르면 시스템은 이 메서드를 호출합니다. 이 메서드가 실행되면, service가 시작되고 백그라운드에서 무한히 실행될 수 있습니다. service 작업이 완료되었을 때 해당 service를 중단하는 건 개발자 역량이며, 이때 stopSelf() 또는 stopService()를 호출하면 됩니다. 바인딩만 제공하고자 하는 경우 해당 메서드를 구현하지 않아도 됩니다.

(ⅱ) onBind()

RPC 같이 다른 component가 해당 serivce에 바인딩되고자 할 때, bindService()을 부르면 시스템은 이 메서드를 호출합니다. 이 메서드에는 클라이언트가 service와 통신을 송수신할 때 사용할 인터페이스를 제공해야 하며 이 인터페이스는 IBinder를 반환해야 합니다. 이 메서드는 항상 구현해야 하지만 바인딩을 원치 않을 때는 null을 반환하면 됩니다.

(ⅲ) onCreate()

시스템은 service가 처음 생성되었을 때, 즉 service가 onStartCommand() 또는 onBind()를 호출하기 전에 이 메서드를 호출하여 초기 설정 과정을 거칩니다. service가 이미 실행 중인 경우에는 호출되지 않습니다.

(ⅳ) onDestroy()

service를 더 이상 사용하지 않고 소멸시킬 때 호출됩니다. service는 스레드, 등록된 리스너 또는 수신기 등의 각종 리소스를 정리하기 위해 구현해야 합니다. 이는 service가 수신하는 마지막 호출입니다.

어느 component가 startService()를 호출하여 service를 시작하면 onStartCommand()에 대한 호출 발생하고, 해당 service는 알아서 stopSelf()로 스스로 중단할 때까지 또는 다른 component가 stopService()를 호출하여 service를 중단시킬 때까지 실행 중인 상태로 유지됩니다. onStartCommand()를 호출하지 않은 채 bindService()만을 호출하여 service를 생성한다면, service는 해당 component에 바인딩되는 동안만 실행됩니다. service가 모든 클라이언트로부터 바인딩이 해제되면 시스템이 이를 소멸시킵니다.

7) service 수명 주기

service의 수명 주기는 activity의 수명 주기보다 훨씬 간단합니다. 그렇지만 service가 사용자가 모르게 백그라운드에서 실행될 수 있기 때문에 신경 써야합니다. service가 생성될 때부터 소멸될 때까지 수명 주기는 다음 두 경로 중 하나를 따를 수 있습니다.

(ⅰ) started service

  • 다른 구성요소가 startService()를 호출하여 service 생성
    -> service가 무기한 실행
  • 자체적으로 stopSelf() 호출 or 다른 구성요소가 stopService() 호출하여 service를 중지
  • service가 중단되면 시스템이 이를 소멸

(ⅱ) bound service

  • 다른 구성요소가 bindService()를 호출하면 service가 생성
  • 클라이언트가 IBinder 인터페이스를 통해 service와 통신
  • 클라이언트가 unbindService()를 호출하여 연결을 종료
  • 여러 클라이언트가 동일한 service에 바인딩 가능
  • 모든 클라이언트가 바인딩을 해제하면 시스템이 service를 소멸(service가 자체적으로 중지할 필요 X )

이 두 경로는 완전히 분리되지 않습니다. 이미 startService()로 시작된 service에 바인딩할 수 있습니다. 예를 들어 재생할 음악을 식별하는 Intent를 포함하여 startService()를 호출하면 백그라운드 음악 service를 시작할 수 있습니다. 나중에 사용자가 플레이어를 제어하거나 현재 노래에 관한 정보를 가져오려고 할 때 bindService()를 호출하여 activiy가 service에 바인딩할 수 있습니다. 이 경우 모든 클라이언트가 바인딩을 해제할 때까지 stopService() 또는 stopSelf()는 실제로 service를 중지하지 않습니다.

activity와 마찬가지로 service에도 수명 주기 콜백 메서드가 있습니다. 이를 구현하면 service의 상태 변경 내용을 모니터할 수 있고 적절한 시기에 작업을 수행할 수 있습니다. 다음의 service는 각 수명 주기 메서드를 보여줍니다.

public class ExampleService extends Service {
    int startMode;       // service가 종료되면 어떻게 행동할지 나타냄
    IBinder binder;      // 클라이언트와 바인드할 인터페이스
    boolean allowRebind; // onRebind를 사용 여부 나타냄

    @Override
    public void onCreate() {
        // service 생성하는 곳
    }
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //startService()가 호출되어 service가 시작하는 곳
	return startMode;
    }
    
    @Override
    public IBinder onBind(Intent intent) {
        // bindService()가 호출되어 클라이언트가 service와 바인딩되는 곳
	return binder;
    }
    
    @Override
    public boolean onUnbind(Intent intent) {
        unbindService()가 호출되여 모든 클라이언트가 해제됨
	return allowRebind;
    }
    
    @Override
    public void onRebind(Intent intent) {
        //onUnbind()가 이미 호출된 후 클라이언트가 
	   //bindService()로 다시 service와 바인딩되는 곳
    }
    
    @Override
    public void onDestroy() {
        // service가 더 이상 사용되지 않아 소멸되는 곳
    }
    
}

activity 수명 주기 콜백 메서드와는 달리 service는 슈퍼클래스 구현을 호출하지 않아도 됩니다.

위의 그림은 service에 대한 일반적인 콜백 메서드를 나타낸 것입니다. 이 그림에서는 startService()로 생성된 service와 bindService()로 생성된 service를 구분하고 있지만, 어떤 식으로 시작되었든 모든 service는 클라이언트와 바인딩될 수 있습니다. 클라이언트가 startService()호출하여 onStartCommand()로 처음 시작된 service라고 해도 클라이언트가 bindService()를 호출하면 여전히 onBind()에 대한 호출을 받을 수 있습니다. 시작된 service를 중단하려면 stopSelf() 또는 stopService()를 호출하면 되지만, service에 대한 개별 콜백은 없습니다. 즉 onStop() 콜백이 없습니다. 그러므로 service가 클라이언트에 바인딩되어 있지 않은 한, 시스템은 service가 중단되면 이를 소멸시킵니다. 수신되는 콜백은 onDestroy()가 유일합니다.

메서드를 구현함으로써, service 수명 주기의 두 가지 중첩된 루프를 모니터링할 수 있습니다.

  • service의 전체 수명
    • onCreate()가 호출된 시점부터 onDestroy() 반환 시점까지.
    • activity같이 service는 자신의 초기 설정을 onCreate()에서 수행.
    • 남은 리소스를 모두 onDestroy()에서 해제.
    • onCreate()onDestroy() 메서드는 모든 service에 호출.
      (startService() 또는 bindService()로 생성되었든 상관없이 적용.)
    • 예를 들어 음악 재생 service는 스레드를 생성하고, 이 스레드의 onCreate()에서 음악이 재생. 그런 다음, onDestroy()에서 스레드를 중단.
  • service의 활성 수명
    • onStartCommand() 또는 onBind()에 대한 호출에서부터 시작.
    • 각 메서드는 Intent를 받아서 startService() 또는 bindService()에 전달.
    • service가 시작되면 수명 주기 전체가 종료되는 것과 동시에 활성 수명 주기도 종료.
    • service는 onStartCommand()가 반환된 뒤에도 활성 상태 유지.
    • service가 바인딩된 경우,onUnbind()가 반환되면 활성 수명 주기가 종료.

2. Service 변화

service를 구현하기 위해서는 단지 Service를 extends하고 몇가지 메소드를 재정의하면 됩니다. 위에 언급된 주요 메소드들이 그것들이지요. service가 멀티스레딩을 수행해야 하는 경우 Service 클래스를 확장하여 각각의 수신 인텐트를 처리하게 할 수 있습니다. 다음은 각 시작 요청에 대해 work 스레드로 작업을 수행하여 한 번에 하나씩만 순차적으로 처리하는 코드 예시입니다.

public class HelloService extends Service {
  private Looper serviceLooper;
  private ServiceHandler serviceHandler;

  //스레드에서 메시지를 받는 핸들러입니다
  private final class ServiceHandler extends Handler {
  
      public ServiceHandler(Looper looper) {
          super(looper);
      }
      
      @Override
      public void handleMessage(Message msg) {
       // 보통 여기서 파일 다운같은 작업을 합니다.
       // 본 예시에서는 단순히 5초 sleep 합니다
       try{
           Thread.sleep(5000);
       }catch(InterruptedException e){
          //인터럽트 상태 복원
          Thread.currentThread().interrupt();
       }
       //다른 작업을 처리하는 동안 service를 중지하지 않도록 
       //startId를 사용하여 service를 중지합니다
          stopSelf(msg.arg1);
      }
      
  }

  @Override
  public void onCreate() {
 //service를 실행하는 스레드를 시작합니다. 
 //service는 일반적으로 프로세스의 main 스레드에서 실행되므로 
 //방해되지 않게 별도의 스레드를 만듭니다.
 //또한 CPU 집약적 작업이 UI를 중단시키지 않도록 백그라운드 우선순위를 설정합니다.
    HandlerThread thread = new HandlerThread("ServiceStartArguments",
            Process.THREAD_PRIORITY_BACKGROUND);
    thread.start();

    // HandlerThread의 Looper를 얻어 핸들러에 사용합니다
    serviceLooper = thread.getLooper();
    serviceHandler = new ServiceHandler(serviceLooper);
  }

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
  // 각 시작 요청에 대해 작업을 시작하라는 메시지와 start ID를 전달하면 
  //작업 완료 시 어떤 요청을 중지해야하는 알 수 있습니다
      Message msg = serviceHandler.obtainMessage();
      msg.arg1 = startId;
      serviceHandler.sendMessage(msg);

      //중단되면 이곳으로 리턴되어 재시작합니다
      return START_STICKY;
  }

  @Override
  public IBinder onBind(Intent intent) {
     //바인딩을 하지 않았기에 null을 반환시킵니다.
      return null;
  }

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

위 예제코드는 onStartCommand()에 들어오는 모든 호출 처리와 백그라운드 스레드에서 실행되는 Handler 작업을 보여줍니다. 하나씩 처리하는 작업을 예시로 보여드렸지만 여러 개의 요청을 동시에 수행할 수도 있습니다. 각 호출을 onStartCommand()로 직접 처리하기 때문입니다. 스레드풀에서 각 요청에 대해 새 스레드를 하나씩 생성한 다음 이전 요청이 끝날 때까지 기다리지 않고 곧바로 실행하면 됩니다.

1) IntentService 클래스

Service의 하위 클래스로, worker 스레드를 사용하여 모든 시작 요청을 처리하되 한 번에 하나씩 처리합니다. 그렇기에 service가 여러 개의 요청을 동시에 처리하지 않아도 되는 경우에 적합합니다. 위의 예제를 보면 알 수 있 듯이 service 클래스를 사용하면 많은 코드량이 필요하기 때문에 간단히 구현할 수 있는 IntentService를 제공하였습니다. onHandleIntent()를 구현하는데 이는 각 시작 요청에 대해 인텐트를 수신해서 백그라운드 작업을 완료하도록 합니다.작업 큐를 통해 시작 요청을 처리합니다.

IntentService 클래스는 다음과 같이 작동합니다.

  • 애플리케이션의 main 스레드와는 별도로, onStartCommand()에 전달된 모든 인텐트를 실행하는 기본 worker 스레드를 생성
  • 인텐트를 한 번에 하나씩 onHandleIntent() 구현에 전달하는 작업 큐를 생성하므로, 다중스레딩에 대해 염려할 필요 없음
  • 시작 요청이 모두 처리된 후 service를 중단하므로, 개발자가 stopSelf()를 호출할 필요가 전혀 없음
  • onBind()의 기본 구현을 제공하여 null을 반환
  • onStartCommand()의 기본 구현을 제공하여, 인텐트를 작업 큐로 보내고 그다음은 onHandleIntent() 구현으로 보냄

다음은 IntentService 구현의 예입니다. 클라이언트가 제공한 작업을 완료하기 위해 onHandleIntent()를 구현합니다.

public class HelloIntentService extends IntentService {

  /*생성자를 구현해야하며 super(workder 스레드이름)을 꼭 생성해야합니다.*/
  public HelloIntentService() {
      super("HelloIntentService");
  }
  
  /**
   IntentServic는 기본 worker 스레드에서 인텐트로 service를 시작할 때 이 메소드를 부릅니다. 
   이 메서드가 리턴되면  IntentService가 service를 적당한 시기에 멈춥니다
   */
  @Override
  protected void onHandleIntent(Intent intent) {
       // 보통 여기서 파일 다운같은 작업을 합니다.
       // 본 예시에서는 단순히 5초 sleep 합니다
       try{
           Thread.sleep(5000);
       }catch(InterruptedException e){
          //인터럽트 상태 복원
          Thread.currentThread().interrupt();
       }
    }

}

단지 생성자 하나와 onHandleIntent()만 구현하면 됩니다. 다른 콜백 메서드도 재정의하려면(onCreate(), onStartCommand() 또는 onDestroy()) 슈퍼 구현을 꼭 호출해야 합니다. 그래야만 IntentService가 작업자 스레드의 수명을 적절하게 처리할 수 있습니다. 예를 들어 onStartCommand()는 반드시 기본 구현을 반환해야 합니다. 그래야 인텐트가 onHandleIntent()로 전달됩니다. onHandleIntent() 외에 슈퍼 클래스를 호출하지 않아도 되는 메서드는 onBind()입니다.물론 onBind()는 service가 바인드를 허용하는 경우에만 구현해야 합니다.

IntentService는 백그라운드에서 비동기 작업 때 자동으로 스레드를 생성하여 작업을 처리하고 스스로 중단하여 개발자에게 편할 뿐 아니라 코드량도 적어졌습니다. 하지만 백그라운드 실행 제한으로 deprecated되었죠.

❓❓❓
백그라운드 제한이란 백그라운드에서 실행 중인 service는 기기의 리소스를 소비하기 때문에 잠재적으로 나쁜 사용자 경험을 초래할 수 있습니다. 이러한 문제를 완화하기 위해, 시스템은 service에 여러 가지 제한을 적용합니다.

2) JobScheduler(extends JobService)

IntentService 대체로 JobScheduler이 나온 것은 아니지만 백그라운드 최적화를 위해 만들어졌습니다. JobService라는 service를 사용하고 백그라운드 제한이 생김에 따라 이를 해결하는 방법은 중요하기 때문에 이에 대한 내용을 다루어 보겠습니다.

우선 JobScheduler는 이름에서 알 수 있듯이 작업을 스케쥴링을 해주는 API입니다. 이 프레임워크는 언제 job을 실행하는지 파악하고 최대한 배치 또는 지연하려고 합니다. JobInfo으로 선언된 조건이 충족되면 시스템은 응용 프로그램의 JobService에서 해당 job을 실행합니다. JobInfo는 네트워크의 연결 상태나 충전 여부 등 JobService가 실행되어야 하는 조건을 관리합니다. 이 조건은 하나일 수도 여러 개일 수도 있습니다. JobService는 시작 및 종료 시의 동작 을처리할 수 있으며 onStartJob()onStopJob() 콜백 메소드를 제공합니다. Job 실행을 위한 권한을 등록한 후, 구현된 JobService에 scheduleJob메소드로 선언된 JobInfo을 전달하면 됩니다.

(ⅰ) service 권한 등록

service 권한을 등록하기 위해 매니페스트 파일에서 <application> 요소의 하위로 service 요소의 permission 속성을 추가하면 됩니다.

AndroidManifest.xml

<manifest ... >
  ...
  <application ... >
      <service
            android:name=".SampleService"//선언한 service 이름
            android:permission="android.permission.BIND_JOB_SERVICE"
            android:exported="true"/>
      ...
  </application>
</manifest>

(ⅱ) service 구현

SampleService.java

public class SampleService extends JobService {

    @Override
    public boolean onStartJob(JobParameters params) {
        //조건에 만족하여 실행하였을 때 호출됨
        //종료 후 동작할 job이 없으면 false,
        //종료 후에도 동작할 job이 있으면 true을 리턴
        //true일 경우 jobFinished()을 호출하여 job 완료를 알리거나
        //조건이 충족되지 않을 때까지 활성 상태 유지
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        //jobFinished()호출 전, job 완료 전에 중지해야 할 경우 호출
        //중지 후 다시 스케쥴러에 등록하려면 true, 아니면 false 리턴
        return false;
    }

}
  • onStartJob()

    • onStartJob()은 Job이 시작할 때 시스템에 의해 호출되는 콜백 . JobService는 메인스레드에서 실행되므로, 필요에 따라 Thread를 호출해야함.
    • onStartJob()의 종료 시 Thread 같이 지속할 동작이 있다면 true, 지속할 동작 없이 완료된다면 false를 리턴.
    • false를 반환하면 job이 이미 완료되었음을 의미하기에 job에 대한 시스템의 wake lock이 해제되고 onStopJob()이 호출되지 않음.

      ❓❓❓
      wakelock은 기기가 절전 모드로 전환되기 전 일부 작업을 완료하기 위해, CPU를 실행 중인 상태로 유지시켜주는 PowerManager 시스템 서비스 기능입니다. wake lock을 사용하면 애플리케이션에서 호스트 기기의 전원 상태를 제어할 수 있습니다.

    • true를 반환할 경우 jobFinished()를 호출하여 job이 완료되었음을 명시적으로 시스템에 알리거나 job에 필요한 제약 조건이 더 이상 충족되지 않을 때까지 job이 활성 상태로 유지.
    • 예를 들어 setRequiresCharging(true)을 사용하여 job이 예약된 경우 사용자가 장치의 전원을 분리하면 job이 OnStopJob() 콜백을 실행되고 해당 job과 연결된 진행 중인 모든 작업이 종료, wake lock이 해제.
    • onStopJob이 호출되었다면 jobFinished()를 호출할 필요 없음. jobFinished()은 두개의 param을 받는데 wake lock을 풀어야하는 job(현재 자신)과, retry 여부. 따라서 true를 넣으면 job이 재실행 되고 false를 넣으면 재실행 없이 job이 완료.
    • 시스템은 job이 실행되는 동안 앱을 대신하여 wake lock을 유지합니다. wakelock은 onStartJob()가 호출되기 전에 획득되며 jobFinish()를 호출하거나 StopJob()을 호출하여 job이 조기에 종료됨을 알리기 전까지는 해제되지 않음.
  • onStopJob()

    • onStopJob()JobFinished() 호출 전, 시스템에서 job 실행을 중지해야 한다고 결정한 경우에 호출. (이 메서드가 호출되면 JobFinished()를 호출할 필요 X)
    • 시스템이 중지해야하는 경우는 지정된 조건이 더 이상 충족되지 않을 때.
      (예를 들어 setRequiredNetworkType()으로 WiFi를 요청했지만 job이 실행되는 동안 WiFi 연결 상태가 바뀔 수 있음.)
    • 중지 후 다시 스케쥴러에 등록하려면 true, 아니면 false 리턴.
    • 메서드가 리턴 또는 시간 초과되면 시스템은 job을 대신하여 유지하고 있는 wake lock을 해제.

(ⅲ) service 실행

service를 구현하였으면 service를 실행시킬 곳에 아래와 같이 등록시키면 됩니다.

JobInfo job = new JobInfo.Builder(
//job id 등록
123,
//조건 충족 시 실행시킬 service class
new ComponentName(getApplicationContext(), SampleService.class)
)
//조건 설정                    
.setRequiresStorageNotLow(true)                    
.build();
        
JobScheduler jobScheduler= (JobScheduler)getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(job);

job를 취소시키려면 스케쥴러에 cancel()를 호출하여 job id를 넘겨주면 됩니다.

jobScheduler.cancel(123);

이렇듯 백그라운드 실행 제한을 해결하기 위해 제시된 방법이지만 가장 큰 문제가 있었으니 바로 하위 호환성이었습니다. API level 21부터만 지원이 되었죠. 안드로이드에는 다양한 버전을 사용한 사용자들이 많았기에 실수 또는 문제를 유발할 수 있는 점이었습니다. 그렇게 JobIntentService가 나왔습니다.

3) JobIntentService

JobIntentService는 job/service를 위해 큐에 적재되어 있는 작업을 처리하기 위한 도우미입니다.

Android O 이상에서 실행되는 경우, JobScheduler를 사용하여 schedule()대신 enqueue()으로 job을 전송합니다. 이전 버전의 플랫폼에서 실행되는 경우, 기본 인텐트를 실행하는 경우와 똑같이 startService()를 사용합니다.

또한 wakelock을 알아서 관리해주기 때문에 이전처럼 WakefulBroadcastReceiver를 사용할 필요가 없습니다.

Android O에서 실행되는 경우, 큐에 적재될 때부터 job이 디스패치될 때까지 그리고 job이 실행되는 동안 wake lock을 유지합니다. 이전 버전의 플랫폼에서 실행되는 경우, wake lock 처리는 PowerManager을 직접 부르면서 이 클래스에 에뮬레이션됩니다. 이는 응용 프로그램이 WAKE_LOCK 권한을 요청해야 함을 의미합니다.

(ⅰ) 버전 별 차이점

Android O 이상에서 Job으로 실행할 경우와 O 이전으로 실행할 경우 몇 가지 중요한 차이점이 있습니다

  • 실행 조건
    • O 이전
      장치가 doze 또는 다른 조건에 관계없이 즉시 service를 시작하여 적재된 작업을 순차적으로 실행
    • O 이후
      JobScheduler를 통해 적재된 작업을 순차적으로 실행. 내부적으로는 고정된 Job의 실행 조건(jobInfo.setOverrideDeadline(0).build())에 의해 실행되므로, 이외의 조건은 직접 설정할 수 없음. 이때 job은 장치가 doze일 때 실행하지 않으며, 실행해야 할 많은 job을 요구당하여 강력한 메모리 압박을 받는 경우 service보다 더 지연될 수 있음.

      ❓❓❓
      doze이란 사용자가 충전기를 연결하지 않고 화면이 꺼진 채로 기기를 일정 기간 방치해두면 , 기기는 doze 즉 잠자기 모드를 시작합니다. 이 모드에서는 시스템이 네트워크 및 CPU를 많이 사용하는 service에 대한 앱의 액세스를 제한하여 배터리를 절약하려고 합니다. 또한, 앱이 네트워크에 액세스하지 못하도록 하고 작업, 동기화 및 표준 알람을 지연시킵니다.

  • 실행 제한
    • O 이전
      일반적인 service 실행 의미론이 적용. service는 무한정 실행될 수 있지만 실행 시간이 길수록 시스템이 프로세스를 완전히 종료할 가능성이 높아지며 메모리 압력 하에서는 최근에 시작된 service어도 프로세스가 종료됨을 예상될 수 있음
    • O 이후
      JobService 실행 시간 제한이 적용되며, 이후 job이 중지되고(프로세스를 종료는 아님) 나중에 실행을 계속하도록 일정이 조정. 장치의 메모리 상태에 따라 동시 job 수가 조정되기 때문에 시스템이 메모리 압력 하에 있을 때는 일반적으로 job이 종료되지 않음.

(ⅱ) service 구현

O이상에서는 JobScheduler를 사용하기 때문에, 시스템이 상호 작용하기 매니페스트에 등록해야합니다.

enqueueWork()를 호출하여 service으로 전송하고, 처리할 새 작업을 enqueueWork를 사용하여 HandleWork에서 실행합니다. 아래는 JobIntentService 예시 입니다.

public class SimpleJobIntentService extends JobIntentService {
    
     //이 service의 고유한 job ID.
    static final int JOB_ID = 1000;

    //service에서 큐를 적재하기 위한 편의 메소드
    static void enqueueWork(Context context, Intent work) {
        enqueueWork(context, SimpleJobIntentService.class, JOB_ID, work);
    }

    @Override
    protected void onHandleWork(@NonNull Intent intent) {
        // 할 일을 받았기에 이 시점에서 이미 시스템 또는 프레임워크는 wake lock을 가지고 있음
        Log.i("SimpleJobIntentService", "Executing work: " + intent);
        String label = intent.getStringExtra("label");
        if (label == null) {
            label = intent.toString();
        }
        toast("Executing: " + label);
        for (int i = 0; i < 5; i++) {
            Log.i("SimpleJobIntentService", "Running service " + (i + 1)
                    + "/5 @ " + SystemClock.elapsedRealtime());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
        Log.i("SimpleJobIntentService", "Completed service @ " + SystemClock.elapsedRealtime());
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        toast("All work complete");
    }

    @SuppressWarnings("deprecation")
    final Handler mHandler = new Handler();

    // 테스트 표시 
    void toast(final CharSequence text) {
        mHandler.post(new Runnable() {
            @Override public void run() {
                Toast.makeText(SimpleJobIntentService.this, text, Toast.LENGTH_SHORT).show();
            }
        });
    }
}

이렇게 JobIntentService를 사용하였지만 10분의 제한시간이 있어 이후 강제종료가 되었습니다. 권한이 필수적으로도 필요했습니다.

참고 :
https://medium.com/til-kotlin-ko/android-o%EC%97%90%EC%84%9C%EC%9D%98-%EB%B0%B1%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EC%B2%98%EB%A6%AC%EB%A5%BC-%EC%9C%84%ED%95%9C-jobintentservice-250af2f7783c,
https://tourspace.tistory.com/38

4) workManager

WorkManager는 백그라운드 처리에 권장하는 기본 API입니다. 대부분의 백그라운드 처리는 지속적인 작업으로 처리되는데 WorkManager는 지속적인 작업에 권장되는 솔루션이기 때문입니다. 앱이 다시 시작되거나 시스템이 재부팅될 때 작업이 예약된 채로 남아 있으면 그 작업은 지속됩니다.

WorkManager는 사용자가 화면을 벗어나 이동하거나, 앱이 종료되거나, 기기가 다시 시작되더라도 안정적으로 실행되어야 하는 작업을 대상으로 설계되었습니다. 예를 들어 백엔드 service에 로그 또는 분석 전송, 주기적으로 서버와 애플리케이션 데이터 동기화가 있습니다.

(ⅰ) 지속적인 작업 유형

WorkManager가 처리하는 지속적인 작업의 유형은 세 가지입니다.

  • 즉시
    즉시 시작하고 곧 완료해야 하는 작업. 신속하게 처리 가능
  • 장기 실행
    10분 이상, 오래 실행될 수 있는 작업.
  • 지연 가능
    시작을 지연하고 주기적으로 실행될 수 있는 예약된 작업

(ⅱ) workManager 특징

workManager는 다음과 같은 특징들을 가집니다.

  • 작업 제약 조건
    작업 제약 조건을 사용하여 작업을 실행하는 데 최적인 조건을 선언적으로 정의. 예를 들어, 기기가 무제한 네트워크에 있을 때 또는 기기가 유휴 상태이거나 배터리가 충분할 때만 실행.

  • 강력한 예약 관리
    WorkManager를 사용하면 가변 스케쥴링을 통해 한 번 또는 반복적으로 실행할 작업을 예약. 작업에 태그 및 이름을 지정하여 고유 작업 및 대체 가능한 작업을 예약하고 작업 그룹으로 함께 모니터링하거나 취소. 예약된 작업은 내부적으로 관리되는 SQLite 데이터베이스에 저장되며 기기를 재부팅해도 WorkManager는 작업이 유지되고 다시 예약되도록 보장. 또한 절전 기능을 사용하고 doze 같은 권장사항을 준수하므로 배터리 소모를 걱정하지 않아도 됨.

  • 신속 처리 작업
    WorkManager를 사용하여 백그라운드에서 즉시 실행할 작업을 예약. 사용자에게 중요하고 몇 분 내에 완료되는 작업에는 신속 처리 작업을 사용해야 함.

  • 유연한 재시도 정책
    경우에 따라 작업이 실패하기도 함. WorkManager는 구성 가능한 exponential back-off 정책을 비롯한 유연한 재시도 정책을 제공.

  • 작업 체이닝
    복잡하게 연결된 작업의 경우, 순차적이거나 병렬적으로 실행되는 작업을 제어할 수 있는 직관적인 인터페이스를 사용하여 개별 작업을 함께 연결.

  • 내장 스레딩 상호 운용성
    WorkManager는 코루틴 및 RxJava와 원활하게 통합되며 고유한 비동기 API를 연결할 수 있는 유연성을 제공. 코루틴과 WorkManager가 여러 사용 사례에 권장되지만, 상호 배타적이지는 않음. WorkManager를 통해 예약된 작업 내에서 코루틴을 사용할 수도 있음.

(ⅲ) work 구현

WorkManager를 사용하기 위해서는 우선 필요한 종속 항목을 추가하고 Worker 클래스를 사용하여 작업을 정의해야 합니다. doWork() 메서드로 WorkManager에서 제공하는 백그라운드 스레드에서 비동기적으로 실행할 수 있습니다.

WorkManager에서 실행할 작업을 만들려면 Worker 클래스를 확장하고 doWork() 메서드를 재정의합니다. 예를 들어 이미지를 업로드하는 Worker를 만들려면 다음과 같이 할 수 있습니다.

public class UploadWorker extends Worker {
   public UploadWorker(
       @NonNull Context context,
       @NonNull WorkerParameters params) {
       super(context, params);
   }

   @Override
   public Result doWork() {

     // 여기서 작업할 내용을 작성합니다. 예로 이미지 업로드
     uploadImages();

     //Result으로 작업이 성공적으로 완료하였는지 나타냄
     return Result.success();
   }
}

doWork()에서 반환된 Result는 작업의 성공 여부를 알려주며 실패한 경우 WorkManager service에 작업을 재시도해야 하는지 알려줍니다.

  • Result.success(): 작업이 성공적으로 완료되었습니다.
  • Result.failure(): 작업에 실패했습니다.
  • Result.retry(): 작업에 실패했으며 재시도 정책에 따라 다른 시점에 시도되어야 합니다.

(ⅳ) WorkRequest로 service 예약

작업을 정의하고 나면 실행을 위해 WorkManager service로 예약해야 합니다. WorkManager에서는 작업을 예약하는 다양한 방법을 제공합니다. 일정한 간격의 주기로 실행되도록 예약하거나 한 번만 실행되도록 예약할 수 있습니다.

어떤 작업 예약 방식을 선택하든 항상 WorkRequest를 사용합니다. Worker는 작업 단위를 정의하는 반면 WorkRequest 및 서브클래스는 언제, 어떻게 작업이 실행되어야 하는지 정의합니다. 가장 간단한 경우 다음 예와 같이 OneTimeWorkRequest를 사용하면 됩니다.

WorkRequest uploadWorkRequest =
   new OneTimeWorkRequest.Builder(UploadWorker.class)
       .build();

필요한 경우 setConstraints()를 통해 제약 조건을 설정합니다.

(ⅳ) WorkRequest 제출

마지막으로 enqueue() 메서드를 사용하여 WorkRequest를 WorkManager에 제출해야 합니다.

WorkManager
    .getInstance(myContext)
    .enqueue(uploadWorkRequest);

작업자가 실행되는 정확한 시간은 WorkRequest에 사용된 제약조건과 시스템 최적화에 따라 달라집니다. WorkManager는 이러한 제한사항에 따라 최상의 상태로 작동하도록 설계되었습니다.

출처 : https://developer.android.com/

다음 SERVICE 포스트
<service_2>
<service_3>
<service 예제>

profile
안드로이드 개발자:D

0개의 댓글