Android : Service

woga·2022년 6월 19일
0

Android 공부

목록 보기
30/49
post-thumbnail

예전에도 한 번 포그라운드, 백그라운드 서비스 정리하면서 서비스에 관해서 정리한 적이 있는데, 이번 포스팅은 정말 서비스에 관해서다.

Next Step의 서비스 파트이기도 하고 조금 더 기본 지식을 정리하면 좋을 거 같아서 액티비티 시리즈에 이어 서비스 파트도 쭉 정리해보려고 한다.

서비스

서비스는 UI를 제공하지 않고 백그라운드에서 실행되는 컴포넌트이다.

백그라운드 스레드에서 작업을 오래 하는 문제

만약 개발자가 캘린더 앱을 만들려고 한다. 캘린더 앱은 네트워크 통해 휴일 정보를 가져오거나 일정을 꾸며주는 스티커를 다운로드하는 작업이 필요하다.

이런 선행 작업은 Application에서 할 수 있는 작업들이라 Application에서 스레드를 시작하는 코드를 가끔 봤을 것이다. 실행 시간이 30초나 걸리기 때문에 UI를 블로킹하지 않기 위해 백그라운드 스레드로 작업을 진행한다.

그런데 여기서 예상치 못한 문제가 있다. 스레드 실행을 마치기 전에 백 키로 앱을 빠져나오거나 홈 키로 나가서 다른 앱을 오래 사용하면 프로세스가 종료될 수 있다!

메모리가 부족할 경우 LMK(Low Memory Killer)는 우선순위가 높지 않은 프로세스를 종료하는데, LMK가 스레드 실행 도중에 앱 프로세스를 종료할 수 있기 때문에 30초나 걸리는 작업의 안정성을 보장할 수 없다.

사용자가 직업 최근 앱 목록에서 앱을 제거해버릴 수도 있다.
이때도 프로세스가 종료되면서 스레드가 종료된다. 앱이 내부적으로 무엇인가 열심히 하고 있다는 걸 사용자가 알 리가 없다.

(참고로 액티비티에서도 마찬가지다. 시간 오래 걸리는 작업을 스레드에서 실행하고 있다면 안전성에 문제가 생긴다)

프로세스 우선순위

프로세스가 LMK에 의해 강제 종료될 가능성은 순전히 우선순위에 따른다.

1) 포그라운드 프로세스
2) visible 프로세스
3) 서비스 프로세스

4) 백그라운드 프로세스 : 액티비티가 종료된 것은 아니지만, 사용자에게 더 이상 보이지 않고(홈 키 누른 후 태스크가 백그라운드로 이동) 활성화된 컴포넌트가 없는 프로세스이다. 보통 백그라운드 프로세스가 여러 개 존재한다.

5) 빈 프로세스 : 사용자가 백 키로 액티비티를 모두 종료하고 활성화 된 컴포넌트가 없다면 빈 프로세스가 된다. 이런 프로세스를 메모리에 한동안 유지하는 이유는 다음에 컴포넌트를 다시 띄울 때 빠르게 띄울 수 있도록 캐시로 사용하기 위해서다. 우선순위가 낮아서 리소스가 부족하면 가장 먼저 강제 종료 대상이 된다.


우선순위상 위 단계로 올라갈 수 있다면 작업의 안정성을 보장할 수 있다. 앞의 예와 같이 스레드에서 30초간 작업한다면 백 키로 모든 화면을 종료했을 때 앱 프로세스는 empty 프로세스가 되어서 LMK에 의해 언제든 종료될 수 있는 상태가 된다.

이를 막기 위해 Application에서 Service를 실행시켜보자.

class LifecycleApplication : Application() {

	override fun onCreate() {
    	super.onCreate()
        startService(Intent(this, SleepSerivce.class))
    }

}
class SleepService : Service() {
	
    override fun onCreate() {
    	val thread = Thread({
        	SystemClock.sleep(30000)
        }).start()
    }
    
    override fun onBind(intent: Intent): IBinder = null
}

이렇게 하면 서비스의 생명주기 메서드가 실행 중일 때는 우선순위가 높은 포그라운드 프로세스에 있다가, onStartCommand() 메서드가 리턴되고 난 이후에 세 번째 우선순위인 서비스 프로세스에 남는다.

LMK에 의해 언제든 제거되는 프로세스인 빈 프로세스가 아니므로 스레드에서 하는 작업을 무사히 종료할 수 있는 가능성이 높아진다.

사용자가 최근 앱 목록에서 제거해도 마찬가지다. 프로세스가 강제 종료되면 서비스는 onStartCommand() 리턴값에 따라 재시작 여부를 결정하는데, 디폴트 리턴 값은 START_STICKY로 서비스를 재시작한다.

최근 앱 목록에서 앱을 제거하면 LMK에 의해 종료되는 것과 동일하게 서비스가 재시작되는 반면, 환경 설정에서 앱 프로세스 강제 종료를 선택하면 프로세스는 완전히 제거되고 서비스는 재시작되지 않는다.


서비스는 UI 스레드에서 실행

서비스는 백그라운드상에서 실행되는 컴포넌트라고 주로 설명한다.

서비스는 액티비티처럼 눈에 보이는 가시 컴포넌트가 아니라는 의미로 이야기하는 것이지 서비스 자체가 메인 스레드가 아닌 별도 스레드에서 실행되는 것으로 착각하면 안 된다.

다시 말하면 서비스의 생명주기 메서드는 UI 스레드에서 실행되고 이 때문에 다른 UI 이벤트가 지연되는 경우가 생길 수 있다. 따라서 서비스에서 UI를 블로킹하는 작업이 있다면 백그라운드 스레드를 생성해서 작업을 진행해야 한다.

서비스는 스레드를 안정적으로 돌리기 위한 컴포넌트라고 이해하면 실제 개발과 이해에 도움을 준다.


서비스는 단일 인스턴스로 실행

서비스는 앱에서 1개의 인스턴스밖에 생기지 않는다.

따라서 일부러 싱글톤 객체를 만들고 그 안에서 백그라운드 스레드를 실행할 필요가 없다. 훨씬 안정적으로 동작하는 컴포넌트를 활용하면 된다.

서비스 시작 방법

Context에는 서비스를 시작하는 방법으로 startService()bindService() 메서드 2가지가 있다.

해당 그림에서 Unbounded Service와 Bounded Service로 구분했지만, Unbound Service는 혼동될 수 있는 용어이다.
바인딩되었다가 해제된 것으로 오해할 수 있기 때문에 Started Service라고 부르는게 더 맞다.

Started(Unbounded) vs Bound Service

서비스는 보통 스타티드와 바운드 서비스로 존재하는데, 스타티드이면서 바운드일 수도 있다.

스타티드 & 바운드 서비스는 코드도 복잡하고 고려할 것이 2배 이상이 되기 때문게 피하는 게 좋지만 어쩔 수 없지 사용해야 하는 경우도 있다.

예를 들어, 음악 재생 화면이 있을 때 화면을 종료해도 음악을 들을 수 있으려면 스타티드 서비스를 이용해야 한다. 그런데 다시 화면에 진입할 때 재생 중인 음악 정보를 화면에 보여줘야 한다면 바운드 서비스이기도 해야 한다.

또 다른 예시를 들어보자.

화면에서 파일 다운로드 명령을 하고 서비스에서 실제 다운로드를 진행한다. 이 때 SeekBar를 통해 다운로드 진행률을 표시하는데, 화면을 종료해도 다운로드는 계속되고 다시 화면에 진입해도 진행률을 표시해야 한다. 화면을 종료해도 다운로드를 계속해야하기 때문에 스타디트 서비스여야한다. 다시 화면에 진입해도 진행률을 표시해야해서 바운드 서비스이기도 해야 한다.


마무리

예시들을 통해 서비스에 대해 더 자세히 알아봤다

이번 포스팅은 정말 서비스에 대한 오해(?와 찍먹 수준으로 알아봤기 때문에 더 궁금한게 많을 수 있다.
다음엔 스타티드와 바운드 서비스에 대해 더 정리해볼 예정이다!

profile
와니와니와니와니 당근당근

0개의 댓글