http://blog.naver.com/PostView.nhn?blogId=nife0719&logNo=221029613969&parentCategoryNo=&categoryNo=26&viewDate=&isShowPopularPosts=false&from=postView


구글에서 안드로이드 마시멜로(6.0, SDK 23)부터 배터리 최적화와 관련되어 도즈(Doze)와 어플 대기모드(App Standby) 정책을 시작하였습니다. 최근 누가(7.0, SDK 24)에서는 좀 더 세분화된 도즈 정책을 적용하기도 했습니다. 이러한 배터리 효율 정책은 기기나 어플을 수면 상태로 만들어버리고 여러 가지 제한이 발생하기 때문에 이러한 제한이 어플의 핵심 기능을 방해한다면 큰 문제가 발생하게 됩니다. 

지금 살펴볼 내용은 이러한 구글의 배터리 최적화 정책에 대응하는 방법에 대해서 다뤄보고자 합니다. 마시멜로와 누가의 도즈 조건과 제한의 차이, 어플 대기모드의 조건과 제한, 테스트용 도즈 및 어플 대기모드 설정, 도즈 및 어플 대기모드 대응 방법 등을 살펴볼 예정입니다. 대응 방법 중 예제 코드를 제공하기에 내용이 너무 많은 경우에는 자세하게 설명된 사이트를 링크해놓았습니다. 

※ 목차
1. 도즈(Doze) 조건과 제한  
  1.1 Marshmallow (6.0, SDK 23) 
  1.2 Nougat(7.0, SDK 24)      
2. 어플 대기모드(App Standby) 조건과 제한 
3. 테스트를 위한 도즈 및 어플 대기모드 설정하기  
4. 배터리 최적화 대응하기  
  4.1 화이트 리스트(Whitelist) 등록 
  4.2 알람 매니저(Alarm Manager) 활용  
  4.3 FCM(Firebase Cloud Messaging) 활용 
  4.4 Foreground Service 활용
[부록] 참고문헌

1. 도즈(Doze) 조건과 제한
도즈는 안드로이드 버전에 따라 진입하게 되는 조건이 달라집니다. 안드로이드 Nougat(7.0)에서는 도즈의 level을 나누어서 배터리 효율을 극대화하도록 정책을 변경하였습니다. 

1.1 Marshmallow (6.0, SDK 23) 
도즈를 처음 실시했던 마시멜로 버전의 경우에는 한 가지 종류의 도즈만 진행됩니다. 도즈는 충전기 연결이 되지 않은 배터리 동작이고 화면이 꺼진 상태로 움직임이 없는 경우가 일정 시간 이상 지속(기준 시간은 제조사마다 다르다고 합니다) 되는 경우 진행됩니다. 도즈가 진행되면 아래와 같은 제한이 발생됩니다. 이러한 제한은 도즈의 중간중간에 발생되는 maintenance window(주황색 막대) 기간에 풀리게 되고 어플들은 이 기간에 작업을 수행하게 됩니다. 

 도즈 진행 시 제한되는 항목
 - 네트워크 액세스가 일시 중단됩니다.
 - Sync Adapter 실행을 허용하지 않습니다. 
 - Job Scheduler 실행을 허용하지 않습니다 
 - Wake Lock을 무시합니다. 
 - Alarm Manager의 처리가 maintenance window로 연기됩니다. 
   (알람을 설정해야 하는 경우에는 특정 API를 사용합니다. 4.2 알람 매니저 활용에서 설명됩니다.) 
 - Wi-Fi 스캔을 수행하지 않습니다. 

1.2 Nougat (7.0, SDK 24) 
마시멜로의 다음 버전인 누가에서는 스마트폰의 움직임 여부에 따라서 두 가지 종류의 도즈가 진행됩니다. 마시멜로와는 달리 
충전기 연결이 되지 않은 배터리 동작이고 화면이 꺼진 상태에서 움직임이 있는 경우에도 도즈가 진행됩니다. 이는 스마트폰을 주머니에 넣어놓는 상황과 유사하며 이러한 상황에서 배터리 효율 향상시키기 위해 새로운 도즈 종류를 추가했다고 합니다.

▶ 배터리 동작, 화면 꺼짐, 움직임 있는 경우 (도즈 제한 항목 중 세부 항목만 제한)
 - 네트워크 액세스가 일시 중단됩니다.
 - Sync Adapter 실행을 허용하지 않습니다. 
 - Job Scheduler 실행을 허용하지 않습니다 

(도즈 진행 시 제한되는 항목의 상위 3개 항목만 제한되며 Light Doze 혹은 Doze 1단계로 부릅니다.)

▶ 배터리 동작, 화면 꺼짐, 움직임 없는 경우 (세부 항목만 제한하다가 모든 항목 제한됨)
이 경우에는 처음에는 움직임이 있는 경우와 동일하게 도즈 제한 항목의 일부만 제한하다가 시간이 좀 더 경과되면 모든 항목에 대해서 제한하게 됩니다. 모든 항목에 대해서 제한하는 도즈 상태를 Deep Doze 혹은 Doze 2단계로 부릅니다. 안드로이드 누가 버전에서의 Deep Doze는 마시멜로의 Doze와 동일합니다.

2. 어플 대기모드(App Standby) 조건과 제한
안드로이드는 마시멜로 버전부터 도즈 외에 어플 대기모드(App Standby)를 진행하고 있습니다. 도즈가 시스템 전체에 해당되는 배터리 최적화 정책이라면 어플 대기모드는 각 어플마다 적용하는 방식입니다. 어플 대기모드는 각 어플마다 아래의 경우에 속하지 않을 때 진행됩니다.


 어플 대기모드 조건 (아래의 경우에 속하지 않을 때 진행)
  - 어플을 명시적으로 실행합니다.
  - 어플에서 Foreground Service를 실행 중입니다. 
  - 잠금 화면이나 알림 창에 표시되는 알림을 생성 중입니다.
  - 기기 관리 앱에 속합니다.

어플 대기모드가 진행되는 어플은 Light Doze와 거의 동일한 제한이 발생하게 됩니다. 

 어플 대기모드 제한
 - 네트워크 액세스가 일시 중단됩니다. (하루에 한 번 액세스가 가능해집니다.)
 - Sync Adapter 실행을 허용하지 않습니다. 
 - Job Scheduler 실행을 허용하지 않습니다 

3. 테스트를 위한 도즈 및 어플 대기모드 설정하기
도즈 및 어플 대기모드는 조건을 만족한 후에 일정 시간이 지나야 하기 때문에 개발 및 테스트를 진행할 때에는 강제적으로 도즈 및 어플 대기모드를 진행할 수 있게 지원하고 있습니다. 안드로이드 스튜디오에서 제공하는 터미널에서 ADB를 사용하여 간단하게 설정이 가능합니다. 

 ADB 실행이 가능한 디렉터리로 이동
필자의 경우에는 안드로이드 sdk의 설치 디렉터리를 따로 설정해두어서 adb 툴이 있는 디렉터리로 이동하는 작업을 먼저 수행하였습니다. adb 툴은 안드로이드의 sdk 설치 디렉터리에서 "platform-tools" 폴더 내에 위치합니다. sdk 설치 디렉터리는 안드로이드 스튜디오의 메뉴 중에 파란색 물음표(?) 좌측에 있는 아이콘을 클릭하면 확인할 수 있습니다. 안드로이드 스튜디오에서 아래의 코드를 입력하여 adb 툴이 있는 위치로 이동하였습니다. 

$ D: $ cd D:\AppData\Local\Android\sdk\platform-tools

 ADB를 통한 도즈 설정
adb를 통해 도즈를 설정하기 이전에 스마트폰을 아래와 같은 상태로 만들어 줍니다.
 - 안드로이드 스튜디오가 설치된 PC와 USB 연결
 - 스마트폰의 화면을 끔
 - 어플에서 특정 기능을 테스트하는 경우, 어플을 킨 후 테스트할 항목을 진행 시킨 후에 화면을 끔
 위의 상태에서 안드로이드 스튜디오의 터미널에서 아래의 코드를 입력해주면 됩니다. 두 번째 코드는 "IDLE"이라는 결과가 나타날 때까지 반복 입력해주면 됩니다.

$ adb shell dumpsys battery unplug $ adb shell dumpsys deviceidle step

도즈 상태에서 다시 배터리 충전 상태로 복귀하는 방법은 아래 코드를 입력하면 됩니다.

$ adb shell dumpsys battery reset

 ADB를 통한 어플 대기모드 설정
adb를 통해 어플 대기모드를 설정하기 이전에 스마트폰을 아래와 같은 상태로 만들어 줍니다.
 - 안드로이드 스튜디오가 설치된 PC와 USB 연결
 - 테스트할 어플을 실행
위의 상태에서 안드로이드 스튜디오의 터미널에서 아래의 코드를 입력해주면 됩니다. 

$ adb shell dumpsys battery unplug $ adb shell am set-inactive <packageName> true

테스트가 끝나고 어플 대기모드에서 빠져나올 때는 아래의 코드를 입력해주면 됩니다.

$ adb shell am set-inactive <packageName> false $ adb shell am get-inactive <packageName>

4. 배터리 최적화 대응하기
앞서 설명한 도즈와 어플 대기모드가 어플의 기능 수행에 영향을 미칠 때 대응할 수 있는 방법입니다. 크게 4가지 방법이 존재하며 각 방법마다 사용이 가능한 어플의 종류가 달라질 수 있습니다. 각 방법에 대한 내용과 특징에 대해 정리하면 아래의 표와 같습니다.

방법
내용
특징
 화이트 리스트 등록
도즈와 어플 대기모드의 대상으로부터 제외되는 화이트 리스트에 등록합니다.
채팅, 메시지 앱 등 특정 조건의 앱만 등록이 가능합니다. 해당 조건이 아닐 때 화이트 리스트 등록을 요청하는 경우에는 구글 플레이 스토어에 어플 등록이 안되는 경우가 발생합니다.
알람 매니저 활용
특정 기능을 수행하도록 알람을 설정해 어플 혹은 스마트폰을 깨웁니다. 
알람 매니저에서 사용하는 API에 따라 해당 어플만 깨울 수도 있으며 기기 자체를 도즈 상태에서 벗어나게 할 수도 있습니다. 도즈를 깨우는 알람은 배터리 효율을 방해하기 때문에 추천하지 않는다고 합니다. 
FCM 활용 
Firebase의 푸시 메시지를 활용해 어플에게 메시지를 전송합니다.
도즈는 유지되고 해당 어플만 잠깐 도즈에서 벗어나 메시지를 수신합니다. 푸시 메시지의 priority를 high-priority로 설정해야 합니다. 
Foreground Service 활용
Foreground service를 사용해 service가 계속 진행되도록 합니다.
구글 개발자 문서에서는 공식적으로 언급하지 않은 방법입니다. Foregound service의 형태를 사용하는 경우 상태 메시지에 나타납니다. 

4.1 화이트 리스트(Whitelist) 등록  
구글은 배터리 효율 정책에서 기본적으로 전체 어플을 대상으로 진행하고 이 중 필요한 어플을 제외하는 형태인 화이트 리스트 방식을 사용했습니다. 사용자마다 어플의 수도 다르고 배터리 효율을 최대화하고자 이러한 방식을 사용한 것으로 생각됩니다. 기본 어플 및 몇 가지 어플들을 제외하고는 화이트 리스트에 등록되어 있지 않기 때문에 도즈 및 어플 대기모드로부터 자유롭고자 하는 어플은 화이트 리스트에 등록하는 과정이 필요합니다. 
다이얼로그를 통해 화이트 리스트 등록 화면에 접근하도록 하여 사용자에게 직접 등록을 시킬 수도 있으며 Manifest에 등록하여 권한 형태로 획득할 수도 있습니다. 

 어플이 화이트 리스트에 등록되어 있는지 확인 
우선 해당 어플이 화이트 리스트에 등록되어 있는지 isIgnoringBatteryOptimizations() 코드를 통해 확인하는 과정이 필요합니다. 예제 코드는 다음과 같습니다.

PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); boolean isWhiteListing = false; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { isWhiteListing = pm.isIgnoringBatteryOptimizations(mContext.getPackageName()); }

 사용자에게 직접 화이트 리스트 등록을 요청
어플이 화이트 리스트에 없는 경우, 
ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS 코드를 통해 사용자에게 직접 화이트 리스트에 등록하도록 요청할 수 있습니다. 보통 다이얼로그를 통해 화이트 리스트 등록 화면으로 진입할 것인지에 대한 여부를 물은 뒤 등록 화면으로 이동하도록 합니다. 예제 코드는 다음과 같습니다. 

AlertDialog.Builder setdialog = new AlertDialog.Builder(MainActivity.this); setdialog.setTitle("추가 설정이 필요합니다.") .setMessage("어플을 문제없이 사용하기 위해서는 해당 어플을 \"배터리 사용량 최적화\" 목록에서 \"제외\"해야 합니다. 설정화면으로 이동하시겠습니까?") .setPositiveButton("네", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { startActivity(new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)); } }) .setNegativeButton("아니오", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Toast.makeText(MainActivity.this, "설정을 취소했습니다.", Toast.LENGTH_SHORT).show(); } }) .create() .show();

 화이트 리스트 등록 권한을 요청
어플이 화이트 리스트에 없는 경우, 
ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 코드를 통해 사용자에게 바로 권한을 부여받을 수도 있습니다. 위의 코드를 통해 권한을 요청하려면 우선 Manifest에 아래와 같이 permission 코드를 추가해야 합니다.

<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>

어플 시작 시 화이트 리스트 등록 여부를 확인 후 권한을 요청합니다. 바로 권한을 요청해도 되지만 사용자의 이해를 돕기 위해 다이얼로그를 통해 권한에 대해 설명한 후 요청하도록 합니다. 예제 코드는 다음과 같습니다.

if(!isWhiteListing){ AlertDialog.Builder setdialog = new AlertDialog.Builder(MainActivity.this); setdialog.setTitle("권한이 필요합니다.") .setMessage("어플을 사용하기 위해서는 해당 어플을 \"배터리 사용량 최적화\" 목록에서 제외하는 권한이 필요합니다. 계속하시겠습니까?") .setPositiveButton("예", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); intent.setData(Uri.parse("package:"+ context.getPackageName())); context.startActivity(intent); } }) .setNegativeButton("아니오", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Toast.makeText(MainActivity.this, "권한 설정을 취소했습니다.", Toast.LENGTH_SHORT).show(); } }) .create() .show(); }

화이트 리스트에 어플을 등록하는 방식은 도즈 및 어플 대기모드를 대응하는 가장 간편한 방법이지만 사용할 수 있는 어플의 종류가 제한되어 있습니다. 채팅, 메시징 등의 어플에만 적용이 가능하며 해당 조건이 아닌 어플이 화이트 리스트 등록을 요청하는 경우에는 플레이 스토어에 어플 등록이 안 되는 경우가 발생할 수 있다고 합니다. 화이트 리스트의 등록을 요청할 수 있는 어플의 조건은 아래의 사이트 제일 하단에서 확인할 수 있습니다. 

4.2 알람 매니저(Alarm Manager) 활용  
화이트 리스트 등록 대상이 아니지만 도즈 및 대기모드에서 특정 기능을 수행해야 하는 경우에는 알람 매니저를 활용할 수 있습니다. 알람 매니저를 통해 특정 시간대에 어플을 깨워 기능을 수행하도록 설정합니다. 이때 알람 매니저의 
setExact()로는 도즈 및 대기모드에서 깨우는 것이 안 되고 maintenance window로 넘어가게 됩니다. 사용 가능한 알람 매니저의 종류와 특징에 대한 설명을 다음과 같습니다.

API
특징
setAndAllowWhileIdle() 
setExactAndAllowWhileIdle()
도즈 상태는 유지되고 해당 어플만 도즈에서 잠깐(10초 정도) 깨어납니다. 나머지 어플들은 도즈 상태에 있습니다. 구글 개발자 문서 상에서는 해당 API를 통한 알람은 어플마다 9분에 한 번씩만 사용이 가능하다고 명시되어 있습니다. 갤럭시 S7(SM-G930S)으로 테스트해본 결과 10분에 한 번씩 울리도록 설정해도 가끔 입력한 시간보다 밀려서 알람이 울리는 경우가 발생합니다.
setAlarmClock()
도즈 상태에서 벗어납니다. 이는 해당 어플뿐만 아니라 나머지 어플들까지 도즈 상태에서 벗어나게 됩니다. 구글 개발자 문서 상에서는 알람이 울리기 바로 전에 도즈에서 벗어나도록 한다고 명시되어 있습니다. 갤럭시 S7(SM-G930S)으로 테스트해본 결과 알람 등록 시 알람 등록 표시가 상태 메시지에 나타나며 알람은 정확하게 설정한 시간이 발생합니다.

 setAndAllowWhileIdle() or setExactAndAllowWhileIdle() 활용
설정한 시간에 해당 어플만 도즈 상태에서 깨우는 알람 매니저 API입니다. Exact가 붙은 API가 절대 시간을 바탕으로 진행되기 때문에 좀 더 정확합니다. 특정 서비스를 깨워 실행시키는 예제 코드는 다음과 같습니다. 


 - 설정한 시간에 특정 A 서비스를 시작하도록 하는 BroadcastReceiver 코드

public class RestartService extends BroadcastReceiver { public static final String ACTION_RESTART_SERVICE = "ACTION.Restart. A"; @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(ACTION_RESTART_SERVICE)) { Intent i = new Intent(context, A.class); context.startService(i); } } }

 - BroadcastReceiver를 Manifest에 등록하는 코드

<receiver android:name="com.example.yourpackage.RestartService" android:process=":remote"/>

 - 현재 시간 기점으로 10분 후에 알람이 울리도록 설정하는 코드

Calendar restart = Calendar.getInstance(); restart.setTimeInMillis(System.currentTimeMillis()); restart.add(Calendar.MINUTE, 10); Intent intent = new Intent(A.this, RestartService.class); intent.setAction(RestartService.ACTION_RESTART_SERVICE); PendingIntent sender = PendingIntent.getBroadcast(A.this, 0, intent, 0); AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE); am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, restart.getTimeInMillis(), sender);

 - 알람 해제 코드

Intent intent = new Intent(A.this, RestartService.class); intent.setAction(RestartService.ACTION_RESTART_SERVICE); PendingIntent sender = PendingIntent.getBroadcast(A.this, 0, intent, 0); AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE); am.cancel(sender);

 setAlarmClock() 활용
설정한 시간에 기기 자체를 도즈 상태에서 깨우는 알람 매니저 API입니다. Broadcast Receiver와 Manifest 등록, 알람 해제 코드는 위와 동일하게 사용하며 알람을 설정하는 코드만 바뀝니다. 


 - 현재 시간 기점으로 10분 후에 알람이 울리도록 설정하는 코드

Calendar restart = Calendar.getInstance(); restart.setTimeInMillis(System.currentTimeMillis()); restart.add(Calendar.MINUTE, 10); Intent intent = new Intent(A.this, RestartService.class); intent.setAction(RestartService.ACTION_RESTART_SERVICE); PendingIntent sender = PendingIntent.getBroadcast(A.this, 0, intent, 0); AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE); AlarmManager.AlarmClockInfo ac = new AlarmManager.AlarmClockInfo(restart.getTimeInMillis(), sender); am.setAlarmClock(ac, sender);

4.3 FCM(Firebase Cloud Messaging) 활용  
도즈 및 어플 대기모드에서는 네트워크 액세스가 제한되기 때문에 서버로부터 데이터를 받아오는 어플의 경우 문제가 발생할 수 있습니다. 이때 활용 가능한 방법인 FCM(Firebase Cloud Messaging)을 활용한 푸시입니다. FCM을 활용해 high-priority로 푸시를 보내게 되면 해당 어플은 도즈 및 어플 대기모드 상태에서도 푸시를 받을 수 있습니다. 일반적인 푸시는 normal-prioriy이기 때문에 반드시 high-priority로 설정을 해주어야 도즈 및 어플 대기모드에서도 받을 수 있습니다. FCM의 메시지 정보를 설정하는 방법은 아래의 사이트에서 확인할 수 있습니다. 

FCM을 설정하고 푸시를 구현하는 방법은 아래의 사이트를 통해 확인할 수 있습니다.

4.4 Foreground Service 활용 
구글 개발자 문서에서는 언급되어 있지 않지만 Foreground Service를 통해서도 도즈 및 어플 대기모드에 대응할 수 있습니다. Foreground Service를 유지하고 있는 동안에는 해당 서비스는 도즈 및 어플 대기모드에 의해 제한받지 않기 때문입니다. 
"포그라운드 서비스는 사용자가 능동적으로 인식하고 있으므로 메모리 부족 시에도 시스템이 중단할 후보로 고려되지 않는 서비스를 말합니다."라고 구글 개발자 문서에 언급된 것과 같은 방식으로 도즈 및 어플 대기모드 후보에 포함되지 않는 것으로 생각됩니다. Foreground Service의 경우에는 상태 표시줄에 알림이 나타나게 되며 가장 대표적인 Foreground Service는 음악 재생 플레이어입니다. 

 Foreground Service 실행
기존의 Service 클래스에 추가하여 Foreground Service의 실행이 가능한 코드는 다음과 같습니다. 예제 코드에서 설정한 값들 외에 더 다양한 설정이 가능합니다. startForeground() 사용 시 포함되는 notification_id는 정수로 된 ID이며(예: 1) 0은 설정이 안 됩니다. 

PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setSmallIcon(R.drawable.ic_launcher) .setContentTitle("This is Foreground Service") .setContentText("Foreground Service Running") .setTicker("Service Running") .setContentIntent(pendingIntent); Notification notification = builder.build(); startForeground(notification_id, notification);

 Foreground Service 종료
실행시켰던 Foreground Service를 종료할 때에는 stopForeground()를 사용합니다. 입력되는 값은 boolean 형태이며 true를 입력하면 종료가 됩니다.

stopForeground(true);