💻 개발/Android

[Android] 백그라운드에서 소켓 통신으로 이벤트 수신 후 알림

고도고도 2022. 6. 8. 01:48

구현 문제

졸업프로젝트를 진행하면서 서버에서 특정한 이벤트를 수신하면 이벤트 종류에 따라 서로 다른 2개의 알림을 띄어주는 기능을 구현해야 했다. 소켓 통신으로 이벤트를 수신하는데 Activity 이동이나 Fragment 이동에 관계 없이 서버와 연결 가능한 소켓 통신이 필요했다.

해결 방법

MVVM과 단일 Activity를 사용했기에 Activity는 MainActivity 한 개 뿐이었고 MainActivity 에서 소켓 통신을 구현하면 되는 문제였다. 하지만 앱을 사용하지 않는 상황에서도 알림을 수신해야 할 필요가 있었고 이를 위해 Service를 사용했다. 물론 MainActivity 안에 해당 기능을 구현할 수 있다. 하지만 소켓 통신이 MainActivity 자체에 종속되는 상황이 발생하기에 Service 를 사용해서 로직을 분리했다.

코드

- MainActivity

우선 MainActivity 부터 살펴보자. 기능과 관련 없는 코드들은 지워버렸다.

class MainActivity(override val ACTIVITY_TAG: String = "MAIN_ACTIVITY") :
    BaseActivity<ActivityMainBinding>(R.layout.activity_main) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        initViews()
    }

    override fun onDestroy() {
        super.onDestroy()
        val intent = Intent(this@MainActivity, ReceiverService::class.java)
        baseContext.stopService(intent)
    }

    private fun initViews() {
        val intent = Intent(this@MainActivity, ReceiverService::class.java)
        baseContext.startService(intent)
    }
}

MainActivity가 onCreate 됐을 때 Service를 실행시킨다. onDestory 됐을 때는 Service를 중지하여 메모리릭을 방지한다.

- ReceiverService

다음은 Service 를 살펴보자. Service 를 상속하는 ReceiverService 를 구현하였고 이 때 onBind 를 오버라이딩 해야 한다. 찾아보니 onBind 의 경우 다른 어플리케이션과 연결할 때 사용한다고 한다. 이 프로젝트에서는 사용하지 않을 듯 싶다. 안드로이드 26 (Android Oreo) 이상부터는 백그라운드에서 앱을 실행할 경우 사용자에게 백그라운드에서 알림을 띄어줘야 한다. 

class ReceiverService : Service() {
    override fun onBind(intent: Intent?): IBinder? {
        TODO("Not yet implemented")
    }

    override fun onCreate() {
        super.onCreate()

        receiveEvent()
    }

    // 이벤트 수신을 위해 백그라운드에서 작동
    private fun receiveEvent() {
        val builder = NotificationCompat.Builder(this@ReceiverService, NOTIFICATION_CHANNEL[0])
            .setSmallIcon(R.drawable.ic_baseline_poop_solid_icon)
            .setContentTitle("이벤트 수신 중")
            .setContentText("이벤트 수신을 위해 백그라운드에서 작동 중입니다.")
            .setPriority(NotificationCompat.FOREGROUND_SERVICE_DEFAULT)

        // Android 26 이상부터는 NotificationChannel 등록 필요
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel =
                NotificationChannel(
                    NOTIFICATION_CHANNEL[0],
                    "백그라운드에서 실행",
                    NotificationManager.IMPORTANCE_DEFAULT
                )
            val manager =
                applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            manager.createNotificationChannel(channel)
        }

        startForeground(NOTIFICATION_RECEIVE_MODE, builder.build())
    }
}​

결과적으로 백그라운드에서 작동하지만 사용자에게 이를 알려 포그라운드로 실행하는 느낌이다. 또한 알림을 구현할 때 알림 채널을 등록해야한다.

알림 채널

알림 채널을 등록하면 위 사진처럼 앱 설정에서 채널 이름을 확인할 수 있다.

 

사용자에게 백그라운드 서비스가 실행되고 있는 것을 알렸으니 이제 백그라운드에서 소켓 통신을 진행하면 된다. 앞서 말한 것처럼 소켓 통신을 통해 서버로부터 이벤트를 수신하고 이벤트의 종류에 따라 각기 다른 알림을 띄워야했다. 우선 간단하게 서버에서 1을 전송하면 이벤트 발생 알림, 2를 전송하면 이미지 선택이라는 알림을 띄워주도록 구현했다.

    private fun connectSocket() {
        var socket: Socket? = null
        var inputStream: InputStream?

        CoroutineScope(Dispatchers.IO).launch {
            try {
                socket = Socket(IP_ADDRESS, PORT_NUMBER)
                do {
                    inputStream = socket?.getInputStream()
                    inputStream?.read().let { data ->
                        when (data) {
                            1 -> occurEventNotification()
                            2 -> selectImageNotification()
                        }
                    }
                    delay(DELAY_TIME)
                } while (true)
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                socket?.close()
            }
        }
    }

요청에 따른 작업을 진행하고 delay 를 통해 일정 시간 대기한다.

 

이벤트 발생 알림과 이미지 선택에 대한 메소드는 아래와 같다. 전체적인 코드는 위에서 봤던 백그라운드 실행을 위해 알림을 띄어주는 것과 유사하다. bulider 로 알림에 대한 옵션을 설정하고 알림 채널을 등록한다.

	// 이벤트를 수신했을 경우 사용자에게 알림 전송
    private fun occurEventNotification() {
        val bitmap = BitmapFactory.decodeStream(URL(IMAGE_URL).openConnection().getInputStream())
        val intent = Intent(this@ReceiverService, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
            putExtra("Fragment", "eventDetailFragment")
        }

        val pendingIntent: PendingIntent =
            PendingIntent.getActivity(this@ReceiverService, 0, intent, 0)

        val builder = NotificationCompat.Builder(this@ReceiverService, NOTIFICATION_CHANNEL[1])
            .setSmallIcon(R.drawable.ic_baseline_poop_solid_icon)
            .setContentTitle("배변 이벤트 수신")
            .setContentText("Device 1에서 배변 이벤트가 수신되었습니다.")
            .setLargeIcon(bitmap)
            .setStyle(
                NotificationCompat.BigPictureStyle()
                    .bigPicture(bitmap)
                    .bigLargeIcon(null)
            )
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)

        // Android 26 이상부터는 NotificationChannel 등록 필요
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel =
                NotificationChannel(
                    NOTIFICATION_CHANNEL[1],
                    "이벤트 발생",
                    NotificationManager.IMPORTANCE_HIGH
                )
            val manager =
                applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            manager.createNotificationChannel(channel)
        }

        NotificationManagerCompat.from(this@ReceiverService)
            .notify(NOTIFICATION_OCCUR_EVENT, builder.build())

        wakeUp()
    }

    private fun selectImageNotification() {
        val intent = Intent(this@ReceiverService, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
            putExtra("Fragment", "deviceDetailFragment")
        }

        val pendingIntent: PendingIntent =
            PendingIntent.getActivity(this@ReceiverService, 1, intent, 0)

        val builder = NotificationCompat.Builder(this@ReceiverService, NOTIFICATION_CHANNEL[2])
            .setSmallIcon(R.drawable.ic_baseline_image_search_24)
            .setContentTitle("학습용 이미지 선택")
            .setContentText("모델 정확도 향상을 위해 학습용 이미지를 선택해주세요.")
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)

        // Android 26 이상부터는 NotificationChannel 등록 필요
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel =
                NotificationChannel(
                    NOTIFICATION_CHANNEL[2],
                    "학습용 이미지 선택",
                    NotificationManager.IMPORTANCE_HIGH
                )
            val manager =
                applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            manager.createNotificationChannel(channel)
        }

        NotificationManagerCompat.from(this@ReceiverService)
            .notify(NOTIFICATION_SELECT_IMAGE, builder.build())

        wakeUp()
    }

 

서버는 Python 을 사용했다. 실제로 서버에서 파이썬을 사용하고 있어서 로컬에서 테스트를 진행하기 위햔 서버 역시 Python 으로 구현했다. 해당 코드는 다른 블로그에 있는 코드를 참고했다.

 

[Python] 소켓 통신하여 채팅 하기

안녕하세요! 오늘은 서버와 소켓 통신 하여 클라이언트 간 채팅을 구현해보도록 하겠습니다. 테스트 환경 - Ubuntu 18.04.5 LTS - Python 3.6.9 1. 소켓(Socket)이란?  소켓(Socket)이란 네트워크상에서 동작하

stickode.tistory.com

import socket
from _thread import *

client_sockets = []  # 서버에 접속한 클라이언트 목록


# 쓰레드에서 실행되는 코드입니다.
# 접속한 클라이언트마다 새로운 쓰레드가 생성되어 통신을 하게 됩니다.
def threaded(client_socket, addr):
    print('>> Connected by :', addr[0], ':', addr[1])

    # 클라이언트가 접속을 끊을 때 까지 반복합니다.
    try:
        while True:
            print("1 : 이벤트 발생\n2 : 이미지 수신")
            command = int(input()).to_bytes(1, byteorder="little", signed=True)
            client_socket.send(command)

    except EOFError:
        print(">> 명령을 종료합니다.")

    client_socket.close()


# 서버 IP 및 열어줄 포트
HOST = '127.0.0.1'
PORT = 9999

# 서버 소켓 생성
print('>> Server Start')
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((HOST, PORT))
server_socket.listen()

# 클라이언트가 접속하면 accept 함수에서 새로운 소켓을 리턴합니다.

# 새로운 쓰레드에서 해당 소켓을 사용하여 통신을 하게 됩니다.

try:
    while True:
        print('>> Wait')
        client_socket, addr = server_socket.accept()
        client_sockets.append(client_socket)
        start_new_thread(threaded, (client_socket, addr))
        print("참가자 수 : ", len(client_sockets))
except Exception as e:
    print('에러는? : ', e)

finally:
    server_socket.close()

결과

이벤트 종류에 따라 서로 다른 알림을 띄움

정상적으로 작동하는 것을 볼 수 있다.

느낀 점

정상적으로 작동하긴 하지만 엄밀히 따지면 완벽한 구현은 아니다. 카카오톡이나 다른 어플처럼 앱이 실행되고 있지 않은 상황에서도 알림을 수신해야하지만 지금은 앱이 실행되고 있는 상태(onDestory를 제외한 상태)에서만 알림이 발생한다. FCM을 사용하거나 onDestory에서 Service를 해제하지 않는 방법도 있지만 내가 원하는 방법이 아니다. 찾아보니 WorkManager를 사용하면 구현이 가능할 듯 싶다. 일단은 이렇게 구현해두고 나중에 Migration 해야겠다.

 

백그라운드 처리 가이드  |  Android 개발자  |  Android Developers

백그라운드 처리 가이드 백그라운드 데이터 처리는 사용자의 기대에 부응하고 사용자에게 도움이 되는 Android 애플리케이션을 개발하는 데 있어 중요한 부분입니다. 이 가이드에서는 백그라운

developer.android.com