본문 바로가기
깡샘 코틀린

15-3 백그라운드 제약

by 농농씨 2023. 7. 7.

안드로이드 앱은 액티비티, 서비스, 브로드캐스트 리시버, 콘텐츠 프로바이더 등 4개의 컴포넌트로 구성되는데, 액티비티를 제외하고는 모두 화면 구현이 아닌 백그라운드에서의 작업 처리가 목적인 컴포넌트들이다.

예전에는 화면이 출력된 적이 없는 상황에서도 서비스나 브로드캐스트 리시버로 백그라운드에서 작업을 처리할 수 있었지만, 안드로이드 8버전(API 레벨 26)부터는 제약을받는다. 따라서 브로드캐스트 리시버나 서비스를 이용할 때는 백그라운드 제약에 관해 잘 정리해둬야 한다. 

브로드캐스트 리시버의 백그라운드 제약부터 알아보자

 

 

리시버의 백그라운드 제약

브로드캐스트 리시버를 실행하려면 sendBroadcast() 함수로 인텐트를 시스템에 전달해야 한다. 그런데 브로드캐스트 리시버암시적 인텐트로 실행할 수 없다.

// 매니페스트에 브로드캐스트 리시버 등록
<receiver
    android:name=".MyReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter> // 브로드캐스트 리시버를 암시적 인텐트로 사용하기 위해 매니페스트 파일에 <intent-filter> 등록
        <action android:name="ACTION_RECEIVER" />
    </intent-filter>
</receiver>

만약 어떤 앱의 매니페스트에서 위처럼 인텐트 필터를 포함해 리시버를 등록했다면 다음처럼 암시적 인텐트로 리시버를 실행하는 코드를 생각할 수 있다.

// 암시적 인텐트로 브로드캐스트 리시버 실행
val intent = Intent("ACTION_RECEIVER") // 인텐트 객체 생성하고
// <intent-filter>에 등록한 name 속성 사용했으므로 암시적 인텐트!로 사용하고자 함
sendBroadcast(intent) // 함수로 시스템에 인텐트를 전달하고자 함(하지만 오류 날 것임)

그런데 앞의 코드로 인텐트를 시스템에 전달하면 리시버는 실행되지 않고 다음과 같은 오류가 발생한다. 즉, 매니페스트에 등록한 리시버를 명시적으로 실행하는 것은 아무런 문제가 없지만 암시적 인텐트로 실행하면 백그라운드 제약으로 오류가 발생한다.

Background execution not allowed: receiving Intent { act=ACTION_RECEIVER flg=0x10 }
to com.example.test15/.MyReceiver

 

그런데 리시버를 매니페스트에 등록하지 않고 다음처럼 코드에서 registerReceiver() 함수로 등록(동적 등록)하면 암시적 인텐트로도 잘 실행된다.

// 코드에서 브로드캐스트 리시버 등록(14-1의 리시버의 동적 등록 참조)
receiver = object : BroadcastReceiver() { // 리시버 객체 생성
	override fun onReceive(context: Context?, intent: Intent?) {
    	Log.d("kkang", "outet app dynamic receiver")
    }
}
registerReceiver(receiver, IntentFilter("ACTION_OUTER_DYNAMIC_RECEIVER"))
// 생성한 리시버 객체를 등록함(동적으로), 암시적 인텐트임

위처럼 코드에서 registerReceiver() 함수로 리시버를 등록하면 다음처럼 암시적 인텐트로 실행할 수 있다.

(음... 리시버 컴포넌트는 일단 필수속성인 name 속성을 포함해서 매니페스트에 등록하면 명시적 인텐트로 실행할 수 있다.

그런데 리시버는 매니페스트에 등록하지 않고도 동적으로 코드에서 등록할 수도 있다. 객체를 생성해서 필요한 순간에 등록하면 된다.

대신 동적으로 등록한 건 사용 후 해제해 줘야 한다.

대신 몇가지 시스템 상황에서는 액티비티나 서비스에서 리시버를 반드시 동적으로만 등록해야 한다.)

// 암시적 인텐트로 브로드캐스트 리시버 실행
val intent = Intent("ACITON_OUTER_DYNAMIC_RECEIVER") // 동적등록한 리시버의 실행을 위한 인텐트 객체 생성하고
sendBroadcast(intent) // 그 인텐트를 시스템에 전달함

정리하자면 매니페스트에 등록한 리시버암시적으로 실행할 때는 같은 앱의 리시버든 외부 앱의 리시버든 실행되지 않는다. 이것이 리시버의 백그라운드 제약이다.(리시버는 암시적으로 실행할 수 없다!)

 

 

서비스의 백그라운드 제약

서비스는 앱이 백그라운드 상태일 때 인텐트를 전달하면 오류가 발생한다. 포그라운드* 상황에서는 잘 실행되던 인텐트도 백그라운드 상황에서는 다음과 같은 오류가 발생한다.

*포그라운드란? 쉽게 말해 '눈에 보인다'

더보기

백그라운드 서비스 vs 포그라운드 서비스 vs 바인드 서비스

서비스는 포그라운드 서비스와 백그라운드 서비스로 나뉘며, 둘은 사용자에게 보이느냐 보이지 않느냐로 나뉜다.

그렇다면 '서비스'란 무엇이냐? 앱이 UI 없이 백그라운드에서 특정 시간동안 실행되는 것을 의미한다.

그럼 어차피 안 보이는 거면 '백그라운드' 서비스는 뭣하러 이름을 만들었으며, 눈에 보이는 '포그라운드' 서비스는 왜 존재하는 것인가?

서비스에 있어서 포그라운드와 백그라운드의 요점은 "앱과 상호작용을 하고 있는지, 하지 않는지"를 나누는 것이다. 즉, 앱이 무언가를 하고 있다는 것을 눈으로 보고, 손으로 터치하는 등 앱과 상호 작용을 할 수 있다면 포그라운드 서비스, 앱이 어떤 작업을 하고는 있지만 사용자가 직관적으로 확인할 수 없다면 백그라운드 서비스라고 보면 되겠다.

 

예를 들어, 알림창에서 앱이 무언가를 다운로드하거나 "앱이 실행 중..." 이런 창을 본 적이 있다면, 이것을 포그라운드 서비스라고 보면 될 것이고, 게임을 업데이트하는 중에 해당 앱을 종료해도 업데이트가 계속 되는 것은 백그라운드 서비스라고 보면 되겠다. 

 

그런데 바인드 서비스라는 것도 있다. 

쉽게 요약하자면, 포그라운드 서비스, 백그라운드 서비스는 서비스를 시작한 시점부터 서비스에게 맡긴 작업을 끝내기 전까지는 앱이 종료되든 말든 계속 실행되는 것이며, 바인드 서비스는 앱과 계속 통신을 하다가 서비스를 부른 액티비티들이 전부 종료되면 작업이 끝나지 않아도 종료되는 것이다.

자세한 건 아래의 첫번째 링크에서~

 

참고링크:

https://keykat7.blogspot.com/2021/01/android-notification-foreground-service.html

https://velog.io/@woga1999/Android-Foreground-Service-포그라운드-서비스

Not allowed to start service intent { act=ACTION_OUTER_SERVICE pkg=com.example.text_outter }:
app is in background uid null

기본적으로 포그라운드(foreground)는 앱의 화면이 전면에 보이는 상황이고, 백그라운드(background)는 앱의 화면이 안 보이는 상황이지만 세부적으로는 몇 가지를 더 고려해야 한다. 안드로이드 시스템에서 서비스가 정상적으로 실행되는 포그라운드 상황은 다음과 같다.

  • 액티비티가 시작되든 중지되든 상관없이 보이는 액티비티가 있을 때
  • 포그라운드 서비스가 있을 때
  • 앱의 서비스에 바인딩하거나 앱의 콘텐츠 프로바이더를 사용해 또 다른 포그라운드 앱이 연결되었을 때

이 외에는 백그라운드 상황으로 간주한다. 그리고 (보통 백그라운드일 때 서비스 컴포넌트의 인텐트를 전달하면 오류가 발생하지만) 앱이 백그라운드 상황이더라도 다음과 같은 경우에는 서비스가 정상적으로 실행된다.

  • 우선순위가 높은 파이어베이스 클라우드 메시징 처리(FCM) 처리
  • SMS/MMS 메시지와 같은 브로드캐스트 수신
  • 알림에서 PendingIntent 실행
  • VPN 앱이 포그라운드로 승격되기 전에 VpnService 시작

이처럼 서비스가 정상적으로 실행되는 포그라운드 상황은 안드로이드 버전이 변경되면서 계속 바뀔 수도 있지만, 어쨌든 중요한 것은 위에 나열한 상황 외에는 서비스가 실행되지 않는다는 점이다.

 

그런데 앱이 백그라운드 상황에서도 서비스를 실행할 방법이 하나 있기는 하다. startForegroundService() 함수로 인텐트를 시작하면 앱이 백그라운드 상황에서도 서비스가 실행된다.

 

// 안드로이드 버전에 따라 백그라운드 상황 대처
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.0) { // 백그라운드 제약 있는 버전이면
	startForegroundSerivce(intent) // 이 함수로 인텐트를 시작하고
} else { // 그렇지 않으면
	startService(intent) // 이전처럼 이 함수로 인텐트를 시작해라
}

서비스의 백그라운드 제약이 적용된 안드로이드 버전을 고려해서 8(Build.VERSION_CODES.0) 이상일 때는 startForegroundService() 함수로 인텐트를 실행하고, 8 미만일 때는 startService() 함수로 인텐트를 실행하는 호환성 코드를 작성한다.

그런데 앱이 백그라운드 상황에서 startForegroundService() 함수로 실행한 서비스는 얼마 후 다음과 같은 오류가 발생하면서 강제로 종료된다. 즉, 앱이 백그라운드 상태더라도 startForegroundService() 함수로 서비스를 실행할 수 있지만, 결국 오류가 발생하므로 서비스를 정상으로 유지할 수는 없다.

context.startForegroundService() did not then call Service.startForeground()

그렇다면 이런 함수를 왜 제공하는 걸까? 결론부터 말하면 서비스를 startForegroundService()함수로 실행했다면 빨리 startForeground() 함수를 호출해 포그라운드 상황으로 만들라는 의미이다. 그러면 서비스가 종료되지 않는다.

// 서비스 쪽 코드
val notification = builder.build()
startForeground(1, notification) // 알림 객체 전달하면서 startForeground() 함수 사용함

위 코드는 백그라운드 상황에서 startForegroundService() 함수로 실행된 서비스 쪽에 작성해야 한다. 그런데 startForeground() 함수의 매개변수는 알림 객체이므로 이 함수를 정상으로 실행하려면 다음처럼 매니페스트에 퍼미션을 등록해 줘야 한다.

(알림은 시스템에서 관리하는 거라서 매니페스트에 등록하는 거였나?)

// 매니페스트에 퍼미션 등록
<uses-permission android:name="nadroid.permission.FOREGROUND_SERVICE">

요약하자면 앱이 백그라운드 상황이더라도 startForegroundService() 함수를 이용하면 서비스를 실행할 수 있지만, 빨리 알림을 이용해 앱을 포그라운드 상황으로 만들어야 한다. 즉, 사용자에게 앱이 실행되고 있다는 것을 알려야 한다는 의미이다. 그래야만 앱이 백그라운드 제약에서 벗어날 수 있다.

 

 

 

 

 

 

공부일기

더보기

개념이 살짝 헷갈리지만~ 사용자에게 어떤 작업이 이루어지고 있다는 걸 알리는 장치가 존재한다는 것을 알게 되어서 흥미롭다.

 

'깡샘 코틀린' 카테고리의 다른 글

16-1 콘텐츠 프로바이더 이해하기  (0) 2023.07.08
15-4 잡 스케줄러  (0) 2023.07.08
15-2 바인딩 서비스  (0) 2023.07.07
15-1 서비스 컴포넌트  (0) 2023.07.06
14-2 시스템 상태 파악하기  (0) 2023.07.03