[Immortal Service: 죽지않는 서비스]
앱을 개발하다보면 죽지 않는 서비스(Immortal Service)가 필요한 순간들이 있다.
필자는 사용자의 위치정보를 지속적으로 가져오는 GPS 서비스가 필요했다.
당장 생각나는 다른 예시로는, 실시간 영상 스트리밍 어플을 구현할 때, Socket 통신 부분을 서비스 컴포넌트로 작성하여 스트리밍을 계속 유지할 수도 있겠다.
이번 글에서는 앱의 필수 기능으로, 절대로 죽으면 안되는 서비스를 구현하는 방법에 대해서 알아보자.
[주의]
이후로 작성된 내용은 필자가 직접 구현하고 작성한 것으로, 검증을 받은 것이 아니다. 때문에 환경에 따라 문제가 발생할 수 있다는 것을 미리 고지한다.
[다양한 구현 방법]
죽지않는 서비스는 생각보다 다양한 방법으로 구현할 수 있다.
- BroadcastReceiver 사용 : 서비스가 종료될 때 브로드캐스트를 보내고, 해당 브로드캐스트를 수신하는 ‘BroadcastReceiver’를 등록하여 서비스의 종료를 감지. 서비스가 종료됨을 감지하면 서비스를 재시작하는 로직을 실행한다.
- 서비스 바인딩 :
ServiceConnection
을 통해 서비스에 바인드한 후,onServiceDisconnceted()
콜백 메서드를 통해 서비스의 종료를 감지한다. 이 방법은 액티비티와 서비스가 직접 통신할 수 있다.
- 서비스 상태 확인 :
MyService.isServiceRun(this)
와 같은 커스텀 메서드를 만들어 서비스의 상태를 주기적으로 확인하고, 종료를 판단.
- 이외에도 EventBus, Livedata, RxJava와 같은 라이브러리를 이용하는 방식, Shared Preferences, DB, Handler and Message 등, 다양한 방법들이 있다.
위처럼 다양한 방법이 있다곤 하지만, 핵심은 결국 하나다.
서비스가 종료됨을 감지하면 다시 실행시키는 것
필자는 회사 프로젝트에 Jetpack과 Compose를 적극 도입 중인 관계로 LiveData를 사용하여 죽지않는 서비스를 구현하였다.
[기본 개념]
Android의 Immortal Service는 Foreground Service로 구현한다.
Foreground Service에 관련된 글은 [이 글](독수리아님ㅎ)을 참조해 주기 바란다.
Oreo 이전 버전들은 그냥 startService()
로 서비스를 만들면 된다.
필자는 얼마전 자사 프로젝트의 최소 Sdk를 Oreo로 설정하였기에 이 글에서는 이전 버전에서의 구현은 고려하지 않겠다.
앞서 핵심은 “서비스가 종료됨을 감지하면 다시 실행시키는 것”이라고 했다.
서비스는 종료될 때, 액티비티와 동일하게 onDestroy()
메서드가 실행된다.
필자는 이 메서드가 호출될 때, 미리 선언해 둔 mutableLiveData의 값을 변경시켜 서비스가 종료되었음을 알렸다.
이후, 액티비티에서 서비스가 mutableLiveData의 값 변경 감지하면 다시 서비스를 실행시킨다.
그림으로 정리해보면 다음과 같다.
프로젝트는 GpsWorker, GpsService, GpsData(object), Activity로 구성된다.
아래 설명은 이번 게시글과 연관있는 GpsService, GpsData, Activity만 작성되어있으며, 전체 코드가 아닌 필요한 일부분만 다룬다.
고로, 전체 소스는 [Github 링크]를 참고바란다.
[결과물]
우선, 작동하는 모습을 보도록하자.
이제 대략적인 코드를 살펴본 후, 필요한 부분에 대한 리뷰를 작성하겠다.
상단을 잘 보면 서비스 실행 여부에 따라 노티가 나타났다 없어졌다하는걸 볼 수 있다.
왜 계속 캡쳐한 이미지가 잘리는지 모르겠는데, 전체 소스를 실행해 보면 하단부의 토스트 메세지로 서비스 상태를 확인할 수 있도록 해뒀다.
[소스 코드]
코드는 Service, Obejct, Activity 순으로 작성하겠다.
1. GpsService
서비스의 실행과 관리를 담당하는 로직
class GpsService : Service() {
.
.
.
// 서비스 시작 시, 호출되는 메서드
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
// 포그라운드 서비스로 시작, 실행 중임을 알리는 노티 표시
startForeground(NOTIFICATION_ID, createChannel().build())
isServiceRunning.postValue(true)
// 위치 정보 제공자 생성
createFusedProvider()
return START_STICKY
}
.
.
.
// GpsWorker 인스턴스를 생성하고 위치 업데이트를 시작하는 메서드
private fun createFusedProvider() {
gpsWorker = GpsWorker(this)
gpsWorker.startLocationUpdates()
}
// 서비스가 완전 종료될 때 호출되는 메서드
override fun onDestroy() {
super.onDestroy()
// 위치 업데이트를 중지하고 서비스 실행 상태를 업데이트
gpsWorker.removeLocationUpdates()
isServiceRunning.postValue(false)
}
}
서비스가 시작될 때 호출되는 메서드인 onStartCommand
startForeground(NOTIFICATION_ID, createChannel().build())
→ 서비스를 Foreground Service로 시작한다. Foreground Service는 사용자에게 항상 보이는 알림을 표시해야 하기 때문에
createChannel().build()
로 알림 객체를 생성하고 반환한다.
isServiceRunning.postValue(true)
→
isServiceRunning
은GpsData
클래스의MutableLiveData
변수이다. 이 변수의 값을 true로 설정하여 서비스가 실행 중임을 알린다. 이 부분을 후에 GpsData 코드에서 다시 자세히 다루겠다.
createFusedProvider()
→
createFusedProvider
메서드를 호출하여 위치 정보 제공자인 GpsWorker의 인스턴스를 생성하고 위치 업데이트 진행.
return START_STICKY
→
onStartCommand
메서드는 시작 명령의 결과로START_STICKY
를 반환한다.
서비스가 종료될 때 호출되는 메서드인 onDestroy
gpsWorker.removeLocationUpdates()
→
GpsWorker
객체의removeLocationUpdates
메서드를 호출하여 위치 업데이트를 중지한다. 이는 불필요한 리소스 사용을 방지하기 위함이다.
isServiceRunning.postValue(false)
→
GpsData
클래스 내의isServiceRunning
값을false
로 업데이트한다. 이는 서비스가 더 이상 실행되고 있지 않음을 알리기 위함이다.
2. GpsData
서비스와 관련된 데이터와 기본 기능들을 제공하는 싱글턴 객체
object GpsData {
// 서비스의 실행 성탸룰 나타내는 LiveData이다. 기본값은 false
val isServiceRunning = MutableLiveData<Boolean>(false)
.
.
.
// GpsService를 시작하는 메서드
fun startGpsService(context: Context) {
// 서비스가 실행 중이지 않은 경우에만 서비스를 시작한다.
// 중복 동작과 ANR 방지 목적이다.
if(!isServiceRun(context)) {
val intent = Intent(context, GpsService::class.java)
ContextCompat.startForegroundService(context, intent)
Toast.makeText(context, "Service Start", Toast.LENGTH_SHORT).show()
}
}
// GpsService를 중지하는 메서드
fun stopGpsService(context: Context) {
val intent = Intent(context, GpsService::class.java)
context.stopService(intent)
}
// 서비스의 실행 상태를 반환하는 메서드
private fun isServiceRun(context: Context): Boolean {
if (isServiceRunning.value== true) {
return true
}
return false
}
}
LiveData를 사용한 이유
- 데이터 변경을 여러 컴포넌트와 공유할수 있기 때문이다.
- 여기서는 Service와 Activity에서 동일한 데이터인
isServiceRunning
을 관찰하며 데이터 변경을 서비스 재실행 여부에 반영한다.
3. MainActivity
class MainActivity : AppCompatActivity() {
private val isServiceRunning = Observer<Boolean> { isRunning ->
if (!isRunning) {
Timber.e("service 상태 변화 감지")
GpsData.startGpsService(this@MainActivity)
GpsData.isServiceRunning.postValue(true)
}
}
.
.
.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
binding.apply {
mainActivity = this@MainActivity
}
// 권한 요청
requestPermissionsLauncher.launch(permissions)
}
fun stopService() {
GpsData.stopGpsService(this@MainActivity)
Toast.makeText(this@MainActivity, "Service Stop", Toast.LENGTH_SHORT).show()
}
.
.
.
}
GpsData.isServiceRunning
의 값을 관찰하는 익명 객체 isServiceRunning
Observer<Boolean>
타입의 익명 객체이다.
-
isRunning
이라는 파라미터를 통해GpsData.isServiceRunning
의 현재 값 (즉, 서비스가 실행 중인지 여부)을 관찰하고 있다가 변경이 감지되면 해당 블록의 코드가 실행된다.
isRunning
이 false인 경우(서비스가 실행 X)GpsData.startGpsService(this@MainActivity)
를 통해 GPS 서비스를 시작한다.
- 마지막으로
GpsData.isServiceRunning
의 값을true
로 업데이트하여 서비스가 실행 중임을 나타낸다.
[마무리]
지금까지 현재 재직 중인 회사에서 실제로 사용한 방식을 바탕으로 간단한 죽지않는 위치 서비스를 구현해 보았다.
모쪼록 정보를 찾아 이 글을 발견한 사람에게 도움이 되었길 바라며, 질문과 오류에 대한 피드백은 언제든지 환영한다.
그럼 오늘도 빡세게 코딩하고 성장하자
Uploaded by N2T
'Android > 개인 기록' 카테고리의 다른 글
Debounce와 Throttle, Event 핸들링 (0) | 2024.05.18 |
---|---|
[Compose] Custom CameraView 만들기 (0) | 2024.03.29 |
Application Restart (앱 재실행) (0) | 2023.06.02 |
[Android: Basic] GPS Location Service (위치정보 서비스) (0) | 2023.05.23 |
Permission Check (권한 요청) (0) | 2023.04.07 |