상세 컨텐츠

본문 제목

[Kotlin] 서비스(Service)

Mobile/Kotlin

by 클리엘 클리엘 2021. 1. 7. 12:46

본문

728x90

사용자에게 노출되지 않고 무엇인가를 지속적으로 처리해야 하는 경우에 사용되는 서비스는 백그라운드에서 보이지 않게 동작합니다.

 

얼핏 백그라운드 스레드와 비슷해 보이기는 하지만 스레드는 별도의 메인 스레드와는 상관없이 다중적인 작업 처리가 가능한 반면 서비스는 메인 스레드를 사용하므로 메인 스레드와 서비스 중 하나에만 처리를 집중할 수 있다는 차이가 있습니다.

 

서비스는 서비스가 시작하는 방식에 따라 2가지 정도로 나뉠 수 있는데 각각의 서비스에 대해서는 실제 서비스를 만들어 동작시켜 보면서 조금씩 살펴보도록 하겠습니다.


1. 스타티드 서비스

 

startService() 메서드로 실행되는 스타티드 서비스는 Ativity와는 무관하게 독립적으로 동작할 수 있습니다. 때문에 Activity가 종료되어도 지속적인 실행이 가능합니다.

 

스타티드 서비스를 만들어 보기 위해 우선 MainActivity에 서비스를 시작하는 버튼과 서비스를 중지하기 위한 버튼 2개를 배치합니다. 서비스 시작 버튼의 ID는 btnServiceStart이고 서비스 중지 버튼의 ID는 btnServiceStop입니다.

 

app -> java -> 패키지명에서 마우스 오른쪽을 클릭하여 New -> Service -> Service를 선택합니다.

 

그리고 다음 설정화면에서 Finish를 눌러주세요.

 

그러면 조금전 설정 화면에 들어가 있는 Class Name으로 kt파일이 생성되며 AndroidManifest.xml에 Service관련 태그가 등록됩니다.

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

생성된 kt파일에는 onBind() 메서드가 오버라이드로 생성되어 있는데 스타티드 서비스에서는 onBind() 메서드는 사용하지 않으므로 그냥 무시합니다. 대신 onStartCommand() 메서드를 새롭게 오버라이드 하여 생성해 주세요.

class MyService : Service() {
    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        return super.onStartCommand(intent, flags, startId)
    }
}

그리고 서비스가 종료될 때 생성되는 onDestroy() 메서드를 구현합니다.

class MyService : Service() {
    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d("Log", "Service Start")

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

    override fun onDestroy() {
        Log.d("Log", "Service Stop")

        super.onDestroy()
    }
}

예제에서는 서비스가 시작할때와 종료될 때 Log를 남기는 것 빼고는 별다른 동작을 하지 않지만 서비스의 동작과 종료 시 필요한 코드는 얼마든지 추가할 수 있습니다.

 

다시 MainActivity.kt로 돌아와 서비스를 실행하는 메서드와 중지하는 메서드를 추가합니다. 서비스 시작을 위해서는 startService() 메서드를 호출해야 하고 중지를 위해서는 stopService() 메서드를 호출하면 되는데 둘 다 위에서 만들어둔 Service의 Intent가 필요합니다.

fun serviceStart()
{
    val intent = Intent(this, MyService::class.java)
    startService(intent)
}

fun serviceStop()
{
    val intent = Intent(this, MyService::class.java)
    stopService(intent)
}

마지막으로 처음 만들어둔 버튼에 각각 서비스를 시작하는 메서드와 중지하는 메서드를 클릭 이벤트 리스너로 연결합니다.

class MainActivity : AppCompatActivity() {
    private lateinit var btnStart: Button
    private lateinit var btnStop: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btnStart = findViewById(R.id.btnServiceStart)
        btnStop = findViewById(R.id.btnServiceStop)

        btnStart.setOnClickListener {
            serviceStart()
        }

        btnStop.setOnClickListener {
            serviceStop()
        }
    }

    fun serviceStart()
    {
        val intent = Intent(this, MyService::class.java)
        startService(intent)
    }

    fun serviceStop()
    {
        val intent = Intent(this, MyService::class.java)
        stopService(intent)
    }
}

앱을 실행하고 로그 캣을 확인합니다.

 


2. 바운드 서비스

 

bindService() 메서드로 호출되며 Activity와 특정 데이터를 주고받을 필요가 있을 경우 사용됩니다. 때문에 연결된 Activity가 종료되면 서비스도 같이 종료하게 됩니다.

 

바운드 서비스는 위에서 만들어본 스타티드 서비스를 수정하는 방법으로 서비스를 생성하도록 하겠습니다. 우선 Activity의 화면을 다음과 같이 변경합니다.

 

본래 서비스 시작과 서비스 중지 버튼의 텍스트를 변경하고 가운데 서비스의 메서드를 호출하는 버튼을 추가합니다. 참고로 새로 추가한 버튼의 ID는 btnCallMethod입니다.

 

MyService.kt에서 MyService안에 바인더 클래스를 생성합니다. 그리고 해당 클래스를 변수에 담아 onBind() 메서드에서 이 변수를 반환하도록 합니다. 이 클래스의 내부 getService() 메서드는 Activity에서 서비스에 접근하기 위한 용도로 사용됩니다.

 

또한 MyService안에 새롭게 추가된 bindServiceTest() 메서드는 Activity에서 서비스로 접근해 서비스의 메서드 호출을 확인해보기 위한 용도로 만들어둔 것입니다. 앞서 바운드 서비스는 Activity와 데이터를 주고받을 필요가 있을 경우 사용한다고 했었는데 이와 같은 처리를 통해 구현이 가능한 것입니다.

class MyService : Service() {
    override fun onBind(intent: Intent): IBinder {
        return binder
    }

    inner class MyServiceBinder : Binder() {
        fun getService(): MyService {
            return this@MyService
        }
    }

    fun bindServiceTest(): String {
        return "abcdefg"
    }

    val binder = MyServiceBinder()
}

이제 MainActivity.kt에서 서비스와의 연결을 위한 커넥션을 생성합니다. onServiceConnected는 서비스와 연결이 이루어지면 호출되는 메서드이므로 이 메서드 안에서 연결된 서비스를 가져올 수 있도록 해야 합니다. 반면 onServiceDisconnected 메서드는 서비스와의 연결이 종료되는 경우 무조건 호출되는 것이 아니라 비정상적으로 종료되는 경우에만 호출되므로 isConService와 같이 임의의 변수를 하나 생성하여 현재 서비스와 연결 중인지를 판단할 수 있도록 해야 합니다.

var myService:MyService? = null
var isConService = false
val serviceConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        val b = service as MyService.MyServiceBinder
        myService = b.getService()
        isConService = true
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        isConService = false
    }
}

다음으로 서비스의 바인딩을 수행하는 메서드를 추가합니다. 동작 방식은 스타티드와 거의 비슷한데 세 번째 파라메터에서 매개변수를 BIND_AUTO_CREATE로 사용한 이유는 메서드가 호출되는 시점에 서비스가 생성되어 있으면 생성된 서비스로 바인딩을 수행하고 그렇지 않으면 새로운 서비스를 생성하고 난 뒤 해당 서비스로 바인딩을 수행할 수 있게끔 하기 위해서입니다.

fun serviceBind()
{
    val intent = Intent(this, MyService::class.java)
    bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}

서비스와의 연결을 위한 바인딩이 있으면 서비스와의 연결을 종료할 메서드도 필요할 것입니다. 다만 unbindService() 메서드가 호출될 때 만약 서비스가 실행 중이지 않으면 오류를 일으킬 수 있으므로 isConService를 통해 서비스가 실행 중인지를 확인하도록 합니다.

fun serviceUnBind()
{
    if (isConService) {
        unbindService(serviceConnection)
        isConService = false
    }
}

끝으로 서비스에서 임의로 만들어둔 bindServiceTest()를 호출하는 메서드를 만들고

fun callBindServiceTest() {
    if (isConService) {
        val rtn = myService?.bindServiceTest()
        Log.d("test", "value=${rtn}")
    }
    else {
        Log.d("test", "no service")
    }
}

앞서 만들었던 버튼들에 각각 메서드를 호출할 수 있도록 이벤트를 연결하면 됩니다.

private lateinit var btnStart: Button
private lateinit var btnStop: Button
private lateinit var btnCallMethod: Button

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    btnStart = findViewById(R.id.btnServiceStart)
    btnStop = findViewById(R.id.btnServiceStop)
    btnCallMethod = findViewById(R.id.btnCallMethod)

    btnStart.setOnClickListener {
        serviceBind()
    }

    btnStop.setOnClickListener {
        serviceUnBind()
    }
    
    btnCallMethod.setOnClickListener {
        callBindServiceTest()
    }
}

앱을 실행하고 테스트해봅니다.

 

 

728x90

관련글 더보기

댓글 영역