안드로이드의 4대 컴포넌트는 다음과 같다.
이 중 Service는 화면이 없는 컴포넌트다.
사용자와 직접 상호작용하지 않지만, 앱의 “뒤에서 돌아가야 하는 작업”을 담당한다.
이 글에서는 “무슨 일이 있어도 BLE 통신이 살아 있어야 한다”라는 실제 개발 요구사항을 통해
Service, 특히 Foreground Service가 왜 필요했는지를 정리해본다.
Service를 처음 제대로 다뤄본 계기는 스타트업 인턴 시절 받은 첫 번째 과제였다.
요구사항은 대략 다음과 같았다.
해외 전시회에서 회사의 전기자전거용 속도계를 시연해야 한다.
실제 전기자전거를 해외로 가져갈 수 없으니,
안드로이드 앱에서 전기자전거 신호를 흉내 내어
BLE 통신을 보내는 앱을 만들어라.단, 부스 운영 중에는 앱이 백그라운드로 가도
절대 꺼지면 안 된다.
처음 들었을 때는 다소 추상적인 요구처럼 들렸지만, 이를 개발 용어로 정리하면 훨씬 명확해진다.
위 요구사항을 정리하면 다음과 같다.
기기를 잠그거나 앱이 포커싱되지 않은 상태에서도
BLE subscriber에게 notification이 계속 전달되어야 한다.
여기서 중요한 포인트는 두 가지다.
onStop() 상태onDestroy()만 아니라면 계속 BLE 통신이 유지되어야 함이 시점에서 Activity만으로는 요구사항을 만족할 수 없다는 것이 분명해진다.
Activity는 사용자 인터페이스를 담당하는 컴포넌트다.
onStop()즉, “좀비처럼 살아 있어야 하는 작업”을 Activity에 두는 것은 구조적으로 잘못된 선택이다.
이때 등장하는 컴포넌트가 바로 Service다.
Service는 다음과 같은 특징을 가진다.
하지만 여기서 한 가지 더 중요한 사실이 있다.
일반 Service는 백그라운드에서 오래 살아남지 못한다.
안드로이드는 배터리와 리소스를 보호하기 위해
백그라운드에서 오래 실행되는 작업을 적극적으로 종료한다.
그래서 BLE처럼 “지속적이고 중요한 작업”에는 Foreground Service가 필요하다.
Foreground Service는 사용자에게 명시적으로 알리고, 시스템에게도 중요하다고 선언하는 Service다.
이를 위해 사용되는 핵심 API가 startForeground()다.
BLE 통신을 담당하는 Service는 다음과 같이 구현했다.
class BluetoothService(
context: Context,
) : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = createNotification()
startForeground(1, notification)
return START_STICKY
}
}
startForeground()START_STICKY이 설정 덕분에,
BLE 통신은 계속 유지될 수 있었다.
Foreground Service는 Manifest에도 명시해야 한다.
<service
android:name=".bluetooth.BluetoothService"
android:foregroundServiceType="dataSync" />
foregroundServiceTypedataSync 유형에 해당이 설정이 없으면 Android 9(API 28) 이상에서 정상 동작하지 않는다.
이 경험을 통해 Service를 이렇게 정리하게 되었다.
Service는 단순히 “백그라운드 작업용” 컴포넌트가 아니라, 안드로이드 시스템과 협상하는 방식에 가깝다.
BLE 통신 요구사항 하나를 만족시키기 위해
Service, Foreground Service, 생명주기를 함께 고민했던 경험은
이후 안드로이드 구조를 이해하는 데 큰 도움이 되었다.