안드로이드에서 서비스(Service)는 UI와 상호작용하지 않고 백그라운드에서 작업을 수행하는 컴포넌트입니다. 특히 서비스는 액티비티가 종료된 이후에도 계속해서 동작할 수 있어, 음악 재생, 위치 추적, 데이터 동기화 등 다양한 백그라운드 작업에 활용됩니다.
하지만 안드로이드 시스템은 메모리 확보를 위해 백그라운드 서비스를 임의로 종료(Dalvik/ART 프로세스 킬)하는 경우가 있습니다. 이번 글에서는 시스템이 쉽게 종료하지 못하도록 서비스를 유지하는 방법을 예제와 함께 단계별로 살펴보겠습니다.
핵심 원리: WakeLock과 START_STICKY
서비스가 시스템에 의해 종료되지 않도록 하려면 두 가지 요소가 중요합니다.
- WakeLock – 화면 꺼짐이나 절전 모드 전환으로 인한 작업 중단을 방지합니다.
- START_STICKY – 시스템이 서비스를 종료하더라도 가능한 한 자동으로 재시작하도록 요청합니다.
Step 1 — 새 프로젝트 생성
Android Studio에서 File ⇒ New Project를 선택하고, 필요한 정보를 모두 입력하여 새 프로젝트를 생성합니다.
Step 2 — 레이아웃 파일 작성 (res/layout/activity_main.xml)
아래 코드를 activity_main.xml에 추가합니다.
<?xml version = "1.0" encoding = "utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android = "https://schemas.android.com/apk/res/android"
xmlns:app = "https://schemas.android.com/apk/res-auto"
xmlns:tools = "https://schemas.android.com/tools"
android:layout_width = "match_parent"
android:layout_height = "match_parent"
tools:context = ".MainActivity">
<TextView
android:id = "@+id/text"
android:layout_width = "wrap_content"
android:layout_height = "wrap_content"
android:text = "Start Service"
android:textSize = "25sp"
app:layout_constraintBottom_toBottomOf = "parent"
app:layout_constraintLeft_toLeftOf = "parent"
app:layout_constraintRight_toRightOf = "parent"
app:layout_constraintTop_toTopOf = "parent" />
</android.support.constraint.ConstraintLayout>위 코드에서는 TextView 하나를 배치했습니다. 사용자가 이 TextView를 클릭하면 서비스가 시작되고, 다시 클릭하면 서비스가 중지되도록 구현할 것입니다.
Step 3 — 메인 액티비티 작성 (src/MainActivity.java)
package com.example.andy.myapplication;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView text = findViewById(R.id.text);
text.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isMyServiceRunning(service.class)) {
text.setText("Stoped");
stopService(new Intent(MainActivity.this, service.class));
} else {
text.setText("Started");
startService(new Intent(MainActivity.this, service.class));
}
}
});
}
private boolean isMyServiceRunning(Class<?> serviceClass) {
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
if (serviceClass.getName().equals(service.service.getClassName())) {
return true;
}
}
return false;
}
}위 코드에서는 Intent에 컨텍스트와 서비스 클래스를 전달하여 서비스를 시작하고 중지합니다. 또한 isMyServiceRunning() 메서드를 통해 현재 해당 서비스가 실행 중인지 확인한 뒤, 실행 상태에 따라 시작/중지 동작을 분기 처리합니다.
서비스 클래스 작성 (service.class)
이제 패키지 폴더 내에 서비스 클래스를 생성하고 아래 코드를 추가합니다.
package com.example.andy.myapplication;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;
import android.support.annotation.RequiresApi;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.widget.Toast;
public class service extends Service {
PowerManager pm;
@SuppressLint("InvalidWakeLockTag")
PowerManager.WakeLock wl;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@SuppressLint("InvalidWakeLockTag")
@Override
public void onCreate() {
super.onCreate();
pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag");
}
@TargetApi(Build.VERSION_CODES.O)
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
wl.acquire();
//Do some task
Toast.makeText(this, "Notification Service started by user.", Toast.LENGTH_LONG).show();
return START_STICKY;
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onDestroy() {
super.onDestroy();
wl.release();
Toast.makeText(this, "Notification Service destroyed by user.", Toast.LENGTH_LONG).show();
}
}코드 핵심 로직 설명
위 코드의 핵심은 PowerManager를 통해 WakeLock을 획득하는 부분입니다. WakeLock을 사용하면 화면이 꺼지거나 기기가 절전 모드로 전환되어도 서비스 스레드가 중단되지 않으므로, 안드로이드 시스템이 서비스를 쉽게 종료하지 못하게 됩니다.
@SuppressLint("InvalidWakeLockTag")
@Override
public void onCreate() {
super.onCreate();
pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag");
}
@TargetApi(Build.VERSION_CODES.O)
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
wl.acquire();
//Do some task
Toast.makeText(this, "Notification Service started by user.", Toast.LENGTH_LONG).show();
return START_STICKY;
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onDestroy() {
super.onDestroy();
wl.release();
Toast.makeText(this, "Notification Service destroyed by user.", Toast.LENGTH_LONG).show();
}동작 흐름을 정리하면 다음과 같습니다.
onCreate(): PowerManager를 가져오고 SCREEN_DIM_WAKE_LOCK 타입의 WakeLock 객체를 생성합니다.onStartCommand():wl.acquire()로 WakeLock을 획득하고,START_STICKY를 반환하여 시스템이 서비스를 종료하더라도 재시작을 시도하도록 설정합니다.onDestroy(): 서비스가 종료될 때wl.release()로 WakeLock을 해제하여 배터리 소모를 방지합니다.
Step 4 — 매니페스트 설정 (manifest.xml)
WAKE_LOCK 권한과 서비스 선언을 추가하기 위해 아래 코드를 manifest.xml에 작성합니다.
<?xml version = "1.0" encoding = "utf-8"?>
<manifest xmlns:android = "https://schemas.android.com/apk/res/android"
package = "com.example.andy.myapplication">
<uses-permission android:name = "android.permission.WAKE_LOCK"/>
<application
android:allowBackup = "true"
android:icon = "@mipmap/ic_launcher"
android:label = "@string/app_name"
android:roundIcon = "@mipmap/ic_launcher_round"
android:supportsRtl = "true"
android:theme = "@style/AppTheme">
<activity android:name = ".MainActivity">
<intent-filter>
<action android:name = "android.intent.action.MAIN" />
<category android:name = "android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name = ".service"/>
</application>
</manifest>여기서 주목할 부분은 두 가지입니다.
<uses-permission android:name="android.permission.WAKE_LOCK"/>— WakeLock 사용을 위한 필수 권한입니다.<service android:name=".service"/>— 서비스 컴포넌트를 매니페스트에 등록해야 정상적으로 동작합니다.
앱 실행 및 결과 확인
실제 안드로이드 기기가 컴퓨터에 연결되어 있다고 가정하고 애플리케이션을 실행해 보겠습니다. Android Studio에서 프로젝트의 액티비티 파일 중 하나를 연 뒤, 툴바에서 Run 아이콘을 클릭하세요. 실행 옵션에서 실제 모바일 기기를 선택하면 기기에 아래와 같은 초기 화면이 표시됩니다.

초기 화면에서 TextView를 클릭하면 아래와 같이 노티피케이션 서비스가 시작됩니다.

서비스가 시작된 상태에서 다시 TextView를 클릭하면 아래와 같이 노티피케이션 서비스가 중지됩니다.

마무리
이처럼 WakeLock과 START_STICKY를 조합하면 시스템이 임의로 종료하기 어려운 안정적인 백그라운드 서비스를 구현할 수 있습니다. 다만 최신 안드로이드 버전일수록 백그라운드 제약이 강화되고 있으므로, 실제 앱 개발 시에는 포그라운드 서비스와 WorkManager 같은 최신 API를 함께 고려하는 것이 좋습니다.