[Android] 11(1). 서비스

9 minute read


서비스

서비스는 화면이 없는 액티비티입니다.

서비스가 백그라운드(서브 스레드)에서 동작하는 컴포넌트로 알려져있는데 실제로 서비스만으로는 백그라운드에서 동작하지 않습니다. 그리고 화면이 없는 액티비티라고 표현한 이유는 서비스가 메인 스레드를 사용하기 때문입니다.

액티비티에서 함수를 호출하고 난 직후에 서비스에서 함수를 호출하면 액티비티의 함수가 완료되어야만 서비스의 함수가 호출됩니다. 반면 서비스와 동일한 동작을 백그라운드 스레드에서 실행하면 두 함수는 동시에(병렬로) 실행됩니다.

따라서 서비스는 기존의 백그라운드 처리와는 다른 개념으로 접근해야 합니다.


서비스의 실행 방식


서비스는 스타티드 서비스^Started\ Service^와 바운드 서비스^Bound\ Service^ 두 가지 형태로 실행됩니다. 그리고 최종적으로 앱이 꺼져도 실행되는 서비스는 포어그라운드 서비스^Foreground\ Service^ 형태로 만들어야 합니다.

스타티드 서비스

스타티드 서비스는 startService( ) 메서드로 호출하며 액티비티와 상관없이 독립적으로 동작할 때 사용합니다.

액티비티의 종료와 무관하게 동작하므로 일반적으로 많이 사용하는 서비스입니다. 스타티드 서비스가 이미 동작 중인 상태에서 서비스의 재시작을 요청할 경우 새로 만들지 않고, 생성되어 있는 서비스를 호출합니다.

image-20210824120008245


바운드 서비스

바운드 서비스는 bindService( ) 메서드로 호출하며 액티비티와 값을 주고 받을 필요가 있을 때 사용합니다.

여러 개의 액티비티가 같은 서비스를 사용할 수 있어서 기존에 생성되어 있는 서비스를 바인딩해서 재사용할 수 있습니다.


액티비티와 값을 주고받기 위한 인터페이스를 제공하지만, 인터페이스의 사용이 복잡하고 연결된 액티비티가 종료되면 서비스도 같이 종료되는 터라 특별한 경우를 제외하고는 잘 사용되지 않습니다.

단, 액티비티 화면이 떠 있는 상태에서 백그라운드 처리도 함께할 경우에는 스타티드 서비스보다 효율적일 수 있습니다.

image-20210824120503801



서비스 만들기


서비스를 만드는 방법은 액티비티와 동일합니다.

[app] - [java] 밑에 있는 패키지명을 마우스 우클릭하여 [New] - [Service] - [Service]를 선택합니다.

클래스명에는 ‘MyService’를 입력하고 [Finish]를 클릭하여 MyService 서비스를 생성합니다.

image-20210824120756199


처음 생성하면 바운드 서비스를 할 수 있는 onBind( ) 메서드가 오버라이드되어 있습니다. onBind( ) 메서드는 스타티드 서비스에서는 사용하지 않습니다.


새로운 서비스를 생성하면 AndroidManifest.xml 파일에 < service > 태그로 등록됩니다.

    <application
        ...
        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true"></service>

        <activity android:name=".MainActivity">
            ...
        </activity>
    </application>



스타티드 서비스 만들기


MyActivity.kt

1. 명령어 상수 선언

서비스 호출 시에는 Intent 인스턴스의 action 프로퍼티에 미리 정의해둔 명령어를 담아서 전달할 수 있습니다.

서비스 클래스 내에 테스트로 사용할 명령어 몇 개를 정의합니다. 일반적으로 명령어는 ‘패키지명 + 명령어’ 조합으로 만들어집니다.

    // 명령어를 상수로 선언
    companion object{
        // 명령어는 일반적으로 '패키지명+명령어' 조합으로 정의
        val ACTION_START = "kr.co.hanbit.servicetest.START"
        val ACTION_RUN = "kr.co.hanbit.servicetest.RUN"
        val ACTION_STOP = "kr.co.hanbit.servicetest.STOP"
    }


2. onStartCommand( ) 메서드 오버라이드

서비스 클래스의 onStartCommand( ) 메서드는 스타티드 서비스를 시작하는 메서드인 startService(Intent)가 호출되면 명령어를 수신합니다.

    // 호출 시 명령어 전달
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val action = intent?.action
        Toast.makeText(this, "서비스 시작", Toast.LENGTH_SHORT).show()
        Log.d("StartedService", "action = $action")
        return super.onStartCommand(intent, flags, startId)
    }

여기서는 “서비스 시작”이라는 문구를 화면에 띄우고, 전달받은 명령어(action)를 로그로 출력하도록 했습니다.


3. onDestroy( ) 메서드 오버라이드

모든 서비스 종료 시 호출되는 onDestroy 메서드를 오버라이드합니다.

    // 서비스 종료 시 호출
    override fun onDestroy(){
        Toast.makeText(this, "서비스 종료", Toast.LENGTH_SHORT).show()
        Log.d("Service", "서비스 종료.")
        super.onDestroy()
    }

여기서는 화면과 로그에 모두 “스타티드 서비스 종료”라는 문구를 출력하도록 했습니다.


MainActivity.kt

1. 스타티드 서비스를 호출하는 serviceStart( ) 메서드 정의

스타티드 서비스를 호출하는 코드를 작성합니다.

먼저 안드로이드에 전달할 Intent를 만들고, MyService에 미리 정의해둔 명령을 action에 담아서 같이 전달합니다.

새로운 메서드를 만들 때 파라미터로 view: View를 사용하면 클릭리스너 연결이 없어도 레이아웃 파일에서 메서드에 직접 접근할 수 있습니다.

    // 스타티드 서비스 시작
    fun serviceStart(view: View){
        val intent = Intent(this, MyService::class.java)
        intent.action = MyService.ACTION_START
        startService(intent)
    }

실질적인 서비스 호출은 startService(Intent) 메서드에서 합니다.


2. 스타티드 서비스를 종료하는 stopService( ) 메서드 정의

    // 스타티드 서비스 중단
    fun serviceStop(view: View){
        val intent = Intent(this, MyService::class.java)
        stopService(intent)
    }

서비스를 중단하기 위해서는 stopService(Intent) 메서드를 호출합니다.


activity_main.xml

앞에서 view: View 파라미터를 사용하는 메서드는 레이아웃 파일에서 바로 메서드에 연결할 수 있다고 했습니다.

버튼 두 개를 배치하고 두 버튼의 onClick 속성에 각각 serviceStart, serviceStop를 지정합니다.

image-20210824155514439

  • 서비스 START 버튼

image-20210824122527747

  • 서비스 STOP 버튼

image-20210824122603856



바운드 서비스 만들기

바인드 서비스를 생성하려면 서비스와 액티비티를 연결하기 위한 ServiceConnection을 생성해야 합니다.


MyActivity.kt

1. 바인더 클래스 생성하고 변수에 저장

서비스 클래스 안에 바인더 클래스를 하나 만들고 변수에 담아둡니다.

액티비티와 바운드 서비스가 연결되면 바인더의 getService( ) 메서드를 통해 서비스에 접근할 수 있습니다.

    // 바운드 서비스와 액티비티 연결
    inner class MyBinder: Binder(){
        fun getService(): MyService{
            return this@MyService
        }
    }
    val binder = MyBinder()


2. onBind( ) 메서드 오버라이드

앞서 스타티드 서비스에서는 사용하지 않았던 onBind 메서드를 오버라이드합니다.

바운드 서비스가 연결되면 앞에서 생성했던 binder 변수를 반환합니다.

    // 바운드 서비스를 이용할 때 사용
    override fun onBind(intent: Intent): IBinder {
        return binder
    }


MainActivity.kt

1. 바운드 서비스와 연결하는 ServiceConnection 생성

바운드 서비스와 연결할 수 있는 서비스 커넥션을 만듭니다. 만든 서비스 커넥션을 bindService( ) 메서드를 통해 시스템에 전달하면 바운드 서비스와 연결할 수 있습니다.


onServiceConneced( )는 바운드 서비스가 연결되면 호출되는 데 반해, onServiceDisconnected( )는 서비스가 ‘비정상적으로’ 종료되었을 때만 호출됩니다. 즉, unBindService( )에 의해 정상적으로 종료되면 호출되지 않습니다.

이런 구조이기 때문에 서비스가 연결되면 isService 변수에 ‘true’를 입력해두고 현재 서비스가 연결되어 있는지를 확인하는 로직이 필요합니다.

    // 바운드 서비스와 연결할 수 있는 서비스 커넥션 생성
    var myService: MyService? = null // 바운드 서비스
    var isService = false // 현재 서비스가 연결되어 있는지 여부
    val connection = object: ServiceConnection {
        // 서비스 연결 시 호출
        override fun onServiceConnected(name: ComponentName, service: IBinder){
            val binder = service as MyService.MyBinder
            myService = binder.getService()
            isService = true

            Log.d("BoundService", "바운드 서비스 연결")
        }
        // 서비스가 '비정상적으로' 죵료 시 호출
        override fun onServiceDisconnected(name: ComponentName?) {
            isService = false
        }
    }


2. 바운드 서비스를 연결하는 serviceBind( ) 메서드 정의

    // 바운드 서비스를 호출하면서 생성한 커넥션을 전달
    fun serviceBind(view: View){
        val intent = Intent(this, MyService::class.java)
        bindService(intent, connection, Context.BIND_AUTO_CREATE)
        Toast.makeText(this, "바운드 서비스 연결", Toast.LENGTH_SHORT).show()
    }

실질적인 바운드 서비스 연결은 bindService(Intent, ServiceConnection, Mode) 메서드가 수행합니다.

세번째 옵션인 Context.BIND_AUTO_CREATE를 설정하면 서비스가 생성되어 있지 않으면 생성 후 바인딩을 하고 이미 생성되어 있으면 바로 바인딩을 합니다.


3. 바운드 서비스를 연결 해제하는 serviceUnbind( ) 메서드 정의

연결을 해제하기 위해서는 unbindService를 호출하는데, 서비스가 실행되고 있지 않을 때 호출하면 오류가 발생합니다.

따라서 isService가 true인지를 먼저 체크하고 바인드를 해제한 후에 isService를 false로 변경합니다.

    // 바운드 서비스 연결 해제
    fun serviceUnbind(view: View){
        // 서비스가 실행 중인지 먼저 체크(실행 중이지 않을 때 호출하면 오류 발생)
        if (isService){
            unbindService(connection)
            isService = false
            Toast.makeText(this, "바운드 서비스 연결 해제", Toast.LENGTH_SHORT).show()
        }else{
            Toast.makeText(this, "바운드 서비스가 연결되지 않았습니다.", Toast.LENGTH_SHORT).show()
        }
    }

실질적인 바운드 서비스 연결 해제는 unbindService(ServiceConnection) 메서드가 수행합니다.


activity_main.xml

스타티드 서비스 때와 마찬가지로 버튼을 두 개 배치하고 각각 onClick 속성에 serviceBind, serviceUnbind 메서드를 연결합니다.

image-20210824161711537



서비스의 메서드 호출하기

바운드 서비스는 스타티드 서비스와 다르게 액티비티에서 서비스의 메서드를 직접 호출해서 사용할 수 있습니다.


MyActivity.kt

MyService 클래스에 문자열 하나를 반환하는 serviceMessage( ) 메서드를 정의합니다.

    // 바운드 서비스의 메서드(테스트)
    fun serviceMessage(): String{
        return "바운드 서비스 함수 호출됨"
    }


MainActivity.kt

메인 액티비티에서 바운드 서비스의 함수를 호출하는 callServiceFunction( ) 메서드를 추가합니다.

    // 바운드 서비스의 메서드 호출
    fun callServiceFunction(view: View){
        if(isService){
            val message = myService?.serviceMessage()
            Toast.makeText(this, "message = $message", Toast.LENGTH_SHORT).show()
        }else{
            Toast.makeText(this, "바운드 서비스가 연결되지 않았습니다.", Toast.LENGTH_SHORT).show()
        }
    }

서비스의 메서드는 바운드 서비스가 연결되어 있을 때만 호출할 수 있습니다.


activity_main.xml

버튼 하나를 배치하고 onClick 속성에 callServiceFunction을 지정합니다.

image-20210824162330353



포어그라운드 서비스


스타티드 서비스와 바운드 서비스는 안드로이드 서비스의 시작 방식을 기준으로 분류하였고, 실행 구조를 기준으로는 포어그라운드와 백그라운드 서비스로 분류할 수 있습니다. 기본적으로 서비스는 모두 백그라운드 서비스입니다.

포어그라운드 서비스는 상태 바 등을 통해 사용자에게 알림을 통해 현재 작업이 진행 중이라는 것을 알려줘야 합니다.


백그라운드 서비스는 안드로이드 앱이 꺼지거나 안드로이드의 가용 자원이 부족하면 시스템에 의해 제거될 수 있지만, 포어그라운드 서비스는 사용자가 알림을 통해 서비스가 동작하고 있다는 것을 인지하고 있기 때문에 가용 자원 부족과 같은 이유로는 종료되지 않습니다.

포어그라운드 서비스를 사용하기 위해서는 서비스를 먼저 생성한 후에 시스템에 포어그라운드로 사용된다는 것을 알려줘야 합니다.


포어그라운드 서비스의 구성

포어그라운드 서비스를 사용하려면 먼저 몇 가지 단계를 거쳐야 합니다.

  1. AndroidManifest.xml 파일에 포어그라운드 서비스 권한을 명세합니다.
  2. 서비스를 먼저 실행합니다.
  3. 서비스 안에서 startForeground( ) 메서드를 호출해서 서비스가 포어그라운드로 실행되고 있다는 것을 안드로이드에 알려줘야 합니다.


포어그라운드 서비스 코드 작성


AndroidManifest.xml

먼저 AndroidManifest.xml 파일에 다음 포어그라운드 권한을 명세합니다.

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


Foreground.kt

0. 서비스 클래스 생성

[app] - [java] - [패키지명 우클릭] - [New] - [Service] - [Service] 를 선택해 Foreground라는 이름의 서비스를 생성합니다.

onBind( ) 메서드 블록 안에 보이는 TODO( ) 행은 삭제하고 오류를 막기 위해 비어있는 Binder()를 리턴해 놓습니다.

class Foreground : Service() {
    override fun onBind(intent: Intent): IBinder {
        return Binder()
    }
}


1. 채널 ID 선언

포어그라운드 서비스를 사용하기 위해서는 안드로이드 화면 상단에 나타나는 상태 바에 알림을 함께 띄워야 하는데, 이 알림이 사용할 채널을 설정할 때 사용할 채널 ID를 상수로 정의해둡니다.

		// 서비스가 사용할 채널 아이디를 상수로 정의
    val CHANNEL_ID = "ForegroundChannel"


2. 알림 채널을 생성하는 createNotificationChannel( ) 메서드 정의

포어그라운드 서비스에 사용할 알림을 실행하기 전에 알림 채널을 생성하는 메서드를 먼저 만들어 놓습니다.

안드로이드 오레오 버전부터 모든 알림은 채널 단위로 동작하도록 설계되어 있습니다.

    // 알림 채널을 생성하는 메서드
    fun createNotificationChannel(){
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            val serviceChannel = NotificationChannel(
                CHANNEL_ID,
                "Foreground Service Channel",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            val manager = getSystemService(NotificationManager::class.java)
            manager.createNotificationChannel(serviceChannel)
        }
    }


3. onStartCommand( ) 메서드 오버라이드

onStartCommand 메서드 안에 ‘알림 채널 생성’ - ‘알림 생성’ - ‘알림 실행’의 순으로 코드를 작성합니다.


먼저 앞에서 만들어둔 메서드를 호출해서 알림 채널을 생성합니다.

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 알림 채널을 생성
        createNotificationChannel()

        return super.onStartCommand(intent, flags, startId)
    }


이제 알림을 생성합니다. 알림 제목으로 “Foreground Service”를, 알림에 사용할 아이콘으로는 프로젝트를 생성하면 기본으로 포함되어 있는 sym_def_app_icon을 사용합니다.

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        createNotificationChannel()
        // 알림 생성
        val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("Foreground Service")
            .setSmallIcon(R.mipmap.ic_launcher_round)
            .build()


마지막으로 startForeground( ) 메서드로 생성한 알림을 실행합니다.

아래는 onStartCommand( ) 메서드의 전체 코드입니다.

    // onStartCommand 오버라이드
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 알림 채널을 생성
        createNotificationChannel()
        // 알림 생성
        val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("Foreground Service")
            .setSmallIcon(R.mipmap.ic_launcher_round)
            .build()
        // 생성한 알림을 실행
        startForeground(1, notification)

        return super.onStartCommand(intent, flags, startId)
    }


activity_main.xml

액티비티는 다음과 같이 구성합니다.

image-20210824184440358

  • 시작 버튼: id=btnStart
  • 종료 버튼: id=btnStop


MainActivity.kt

메인 액티비티에서는 바인딩을 연결한 후, 두 개의 버튼에 클릭 리스너를 달아줍니다.

포어그라운드 서비스를 시작할 때는 startService( )가 아닌 ContextCompat.startForegroundService( ) 메서드를 사용하고, 종료할 때는 스타티드 서비스와 마찬가지로 stopService( ) 메서드를 사용합니다.

아래는 메인 액티비티의 전체 코드입니다.

class MainActivity : AppCompatActivity() {

    val binding by lazy{ActivityMainBinding.inflate(layoutInflater)}

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        // 시작 버튼 클릭리스너
        binding.btnStart.setOnClickListener {
            val intent = Intent(this, Foreground::class.java)
            ContextCompat.startForegroundService(this, intent)
        }

        // 종료 버튼 클릭리스너
        binding.btnStop.setOnClickListener {
            val intent = Intent(this, Foreground::class.java)
            stopService(intent)
        }
    }
}



[결과 화면]

에뮬레이터에서 실행한 후 서비스 시작 버튼을 클릭하면 하얀색 동그란 모양의 아이콘이 안드로이드 상태 바에 나타납니다. 상단을 아래로 스와이프해서 끌어내리면 알림창도 나타납니다.

image-20210824185037278

포어그라운드 서비스는 실행한 액티비티를 강제 종료해도 서비스가 종료되지 않고, 서비스 종료를 명시적으로 해줘야(여기서는 ‘서비스 종료’ 버튼 클릭) 서비스가 종료되고 알림이 사라집니다.



정리


  • 서비스는 백그라운드에서 동작하지만 메인 스레드를 이용합니다.
  • 서비스는 서비스의 시작 방식을 기준으로 스타티드 서비스, 바운드 서비스로 나눌 수 있고, 실행 구조를 기준으로 포어그라운드 서비스와 백그라운드 서비스로 나눌 수 있습니다.
  • 스타티드 서비스
    • 액티비티의 종료와 무관하게 동작합니다.
    • serviceStart 메서드로 시작하고 serviceStop 메서드로 중지합니다.
    • 서비스 클래스 내에서는 서비스 시작 시 onStartCommand 메서드가 호출되고, 종료 시 onDestroy 메서드가 호출됩니다.
  • 바운드 서비스
    • 액티비티와 값을 주고받을 필요가 있을 때 사용하며 액티비티가 종료될 때 함께 종료됩니다. 여러 개의 액티비티가 같은 서비스를 사용할 수 있어서 기존에 생성되어 있는 서비스를 바인딩해서 재사용할 수 있습니다.
    • 바운드 서비스를 사용하기 위해서는 액티비티에서 ServiceConnection을 생성하고 안에 onServiceConnectedonServiceDisconnected 메서드를 오버라이드해야 합니다.
      • onServiceDisconnected 메서드는 바운드 서비스가 비정상적으로 종료되었을 때만 호출됩니다.
    • serviceBind 메서드로 연결하고 serviceUnbind 메서드로 연결 해제합니다.
      • 연결 해제를 할 때는 반드시 현재 바운드 서비스가 연결되어 있는지부터 체크해야 합니다.
    • 서비스 클래스 내에서는 바인더 내부 클래스를 정의하고, 변수에 담아둡니다. onBind 메서드 호출 시 바인더 변수를 반환합니다.
    • 마찬가지로 서비스 종료 시 onDestroy 메서드가 호출됩니다.
  • 포어그라운드 서비스
    • 백그라운드 서비스(스타티드/바운드 서비스)와 다르게 알림을 통해 현재 작업이 진행 중이라는 것을 사용자에게 알려줍니다.
    • 사용하려면 AndroidManifest.xml 파일에 권한을 명세해야 합니다.
    • ContextCompat.startForegroundService( )로 시작하고 stopService( )로 종료합니다.
    • 서비스 클래스 내에서는 알림 채널을 생성하고 알림을 생성한 후에 startForeground 메서드로 생성한 알림을 실행합니다.

Categories:

Updated:

Leave a comment