์ค‘๊ตญ์—์„œ๋„ ๋Š๊ธฐ์ง€ ์•Š๋Š” ํ‘ธ์‹œ, ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค(์•ˆ๋“œ๋กœ์ด๋“œ)

minseok.kim
  • #Mqtt
  • #Push
  • #Android

๋“ค์–ด๊ฐ€๋ฉฐ

์•ˆ๋…•ํ•˜์„ธ์š”. ๋…ธ๋จธ์Šค์—์„œ ํ”„๋กฌ ์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ ๊ฐœ๋ฐœ์„ ๋‹ด๋‹นํ•˜๊ณ  ์žˆ๋Š” ๊น€๋ฏผ์„์ž…๋‹ˆ๋‹ค.
์ตœ๊ทผ ํ”„๋กฌ ์•ฑ์— ์ค‘๊ตญ ํ˜„์ง€์—์„œ๋„ VPN ์—†์ด ์›ํ™œํ•˜๊ฒŒ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ์ ํ™” ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ‘

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๊ทธ์ค‘ ์•ˆ๋“œ๋กœ์ด๋“œ ํŒ€์—์„œ MQTT๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์•ˆ์ •์ ์ธ ํ‘ธ์‹œ(Push) ์ˆ˜์‹  ์„œ๋น„์Šค๋ฅผ ๊ตฌ์ถ•ํ•œ ๊ฒฝํ—˜์„ ๊ณต์œ ํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” MQTT์˜ ๊ธฐ๋ณธ ๊ฐœ๋… ์„ค๋ช…๋ณด๋‹ค๋Š”, ์ €ํฌ ์•ˆ๋“œ๋กœ์ด๋“œ ํŒ€์—์„œ ์–ด๋–ค ๋ฐฉ๋ฒ•์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ณ  ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ–ˆ๋Š”์ง€์— ์ง‘์ค‘ํ•ฉ๋‹ˆ๋‹ค. ์ €ํฌ ํŒ€์—์„œ MQTT๋ฅผ ์„ ํƒํ•œ ๋ฐฐ๊ฒฝ์ด ๊ถ๊ธˆํ•˜์‹œ๋‹ค๋ฉด ์•„๋ž˜ ์‹œ๋ฆฌ์ฆˆ ๊ธ€์„ ๋จผ์ € ์ฝ์–ด๋ณด์‹œ๋Š” ๊ฒƒ์„ ์ถ”์ฒœ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

1. FCM์˜ ํ•œ๊ณ„

๊ตญ๋‚ด์—์„œ ์„œ๋น„์Šค๋˜๋Š” ๋Œ€๋ถ€๋ถ„์˜ ์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ์€ FCM(Firebase Cloud Messaging)์„ ํ†ตํ•ด ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. FCM์€ ์•ฑ์ด ํฌ๊ทธ๋ผ์šด๋“œ, ๋ฐฑ๊ทธ๋ผ์šด๋“œ, ์‹ฌ์ง€์–ด ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ข…๋ฃŒ๋œ ์ƒํƒœ์—์„œ๋„ ํ‘ธ์‹œ๋ฅผ ์•ˆ์ •์ ์œผ๋กœ ์ˆ˜์‹ ํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ FCM์€ Google Play Service ์œ„์—์„œ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๊ธฐ๊ธฐ์— ๋ฐ˜๋“œ์‹œ Google Play ์Šคํ† ์–ด ์•ฑ์ด ์„ค์น˜๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Firebase ๊ณต์‹ ๋ฌธ์„œ์—์„œ๋„ ์ด ์ ์„ ๋ช…์‹œํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

FCM ํด๋ผ์ด์–ธํŠธ๋Š” Android 5.0 ์ด์ƒ์˜ Google Play ์Šคํ† ์–ด ์•ฑ์ด ์„ค์น˜๋œ ๊ธฐ๊ธฐ ๋˜๋Š” Google API๋กœ Android 5.0์„ ์‹คํ–‰ํ•˜๋Š” ์—๋ฎฌ๋ ˆ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. Google Play ์Šคํ† ์–ด๋ฅผ ํ†ตํ•ด์„œ๋งŒ Android ์•ฑ์„ ๋ฐฐํฌํ•˜๋„๋ก ์ œํ•œ๋˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค.

์ถœ์ฒ˜: https://firebase.google.com/docs/cloud-messaging/android/client?hl=ko

์ด๋Ÿฌํ•œ ์ œ์•ฝ์€ ์ค‘๊ตญ ํ™˜๊ฒฝ์—์„œ ๋‘ ๊ฐ€์ง€ ํฐ ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐํ•ฉ๋‹ˆ๋‹ค.

  1. Google Play ์Šคํ† ์–ด์˜ ๋ถ€์žฌ: ํ™”์›จ์ด(Harmony OS) ๋“ฑ ์ค‘๊ตญ ๋‚ด์ˆ˜์šฉ ๊ธฐ๊ธฐ์—๋Š” Google Play ์Šคํ† ์–ด๊ฐ€ ํƒ‘์žฌ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„ FCM์„ ์›์ฒœ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
  2. ์ค‘๊ตญ ๋ฐฉํ™”๋ฒฝ(GFW): Play ์Šคํ† ์–ด๊ฐ€ ์„ค์น˜๋œ ๊ธฐ๊ธฐ์ผ์ง€๋ผ๋„, ์ค‘๊ตญ์˜ ๋ฐฉํ™”๋ฒฝ ์ •์ฑ…์œผ๋กœ ์ธํ•ด FCM ์„œ๋ฒ„์™€์˜ ์—ฐ๊ฒฐ์ด ๋ถˆ์•ˆ์ •ํ•˜๊ฑฐ๋‚˜ ์ฐจ๋‹จ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ‘ธ์‹œ๋ฅผ ์ •์ƒ์ ์œผ๋กœ ์ˆ˜์‹ ํ•˜๋ ค๋ฉด VPN ์‚ฌ์šฉ์ด ๊ฐ•์ œ๋ฉ๋‹ˆ๋‹ค.

์ƒค์˜ค๋ฏธ(Mi Push), ํ™”์›จ์ด(Push Kit) ๋“ฑ ๊ธฐ๊ธฐ ์ œ์กฐ์‚ฌ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ์ž์ฒด ํ‘ธ์‹œ ์„œ๋น„์Šค๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ๊ณ ๋ คํ–ˆ์ง€๋งŒ, ๋„์ž…์„ ์œ„ํ•œ ์š”๊ฑด์ด ๋ณต์žกํ•˜๊ณ  ์ œ์•ฝ์ด ๋งŽ์•„ ํ˜„์‹ค์ ์ธ ๋Œ€์•ˆ์ด ๋˜๊ธฐ ์–ด๋ ค์› ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ณ  VPN ์—†์ด ์•ˆ์ •์ ์ธ ํ‘ธ์‹œ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด, ์ €ํฌ ํŒ€์€ MQTT๋ฅผ ํ™œ์šฉํ•œ ์ž์ฒด ํ‘ธ์‹œ ๊ตฌํ˜„์„ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

2. ์–ด๋–ค MQTT ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ ํƒํ–ˆ๋Š”๊ฐ€

ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐ ๊ธฐ์ˆ  ๊ฒ€ํ†  ๋‹จ๊ณ„์—์„œ ์„œ๋ฒ„๋Š” AWS IoT Core ๊ธฐ๋ฐ˜์˜ MQTT ๋ธŒ๋กœ์ปค๋ฅผ, ์ธ์ฆ ๋ฐฉ์‹์€ AWS Cognito๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด์— ๋”ฐ๋ผ ์•ˆ๋“œ๋กœ์ด๋“œ ํŒ€์—์„œ๋Š” Cognito ์ธ์ฆ์„ ๊ณต์‹ ์ง€์›ํ•˜๋Š” AWS IoT Core Device SDK Java V2๋ฅผ ์šฐ์„  ๊ฒ€ํ† ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๊ฐœ๋ฐœ ๊ณผ์ •์—์„œ ์—ฌ๋Ÿฌ ๋ฌธ์ œ๋กœ ์ธํ•ด ์ธ์ฆ ๋ฐฉ์‹์ด Custom Authorizer๋กœ ๋ณ€๊ฒฝ๋˜๋ฉด์„œ ์›น์†Œ์ผ“(wss) ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์—์„œ AWS IoT Core Device SDK๊ฐ€ ์•ˆ๋“œ๋กœ์ด๋“œ ํ™˜๊ฒฝ์˜ SSL ์ธ์ฆ์„œ๋ฅผ ์ œ๋Œ€๋กœ ์ฒ˜๋ฆฌํ•˜์ง€ ๋ชปํ•ด ์—ฐ๊ฒฐ์— ์‹คํŒจํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ๊ฒช์—ˆ์Šต๋‹ˆ๋‹ค. ์ œํ•œ๋œ ์‹œ๊ฐ„ ์•ˆ์— ๊ณต์‹ ๋ฌธ์„œ๋‚˜ GitHub Issue์—์„œ ๋ช…ํ™•ํ•œ ํ•ด๊ฒฐ์ฑ…์„ ์ฐพ๊ธฐ ์–ด๋ ค์› ๊ณ , ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋ฉฐ ๋Œ€์•ˆ์„ ๋ชจ์ƒ‰ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๋Ÿฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ฒ€ํ† ํ•œ ๊ฒฐ๊ณผ, ์ตœ์ข…์ ์œผ๋กœ hannesa2/paho.mqtt.android ์˜คํ”ˆ์†Œ์Šค๋ฅผ ์ฑ„ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

paho.mqtt.android ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ๋‘ ์ข…๋ฅ˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

  • eclipse-paho: ์ดํด๋ฆฝ์Šค ์žฌ๋‹จ์—์„œ ๊ฐœ๋ฐœํ•œ MQTT ํด๋ผ์ด์–ธํŠธ SDK์ž…๋‹ˆ๋‹ค. ํ˜„์žฌ๋Š” ๊ฐœ๋ฐœ์ด ๊ฑฐ์˜ ์ค‘๋‹จ๋œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. (GitHub)
  • hannesa2: eclipse-paho๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ตœ์‹  ์•ˆ๋“œ๋กœ์ด๋“œ ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ๊ฐœ์„ ํ•œ ๋ฒ„์ „์ž…๋‹ˆ๋‹ค. ์ง€๊ธˆ๋„ ํ™œ๋ฐœํ•˜๊ฒŒ ์œ ์ง€๋ณด์ˆ˜๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. (GitHub)

3. paho.mqtt.android๋ฅผ ์ด์šฉํ•œ ํ”„๋กฌ ํ‘ธ์‹œ ์„œ๋น„์Šค ๊ตฌํ˜„

์ด์ œ paho.mqtt.android ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ MQTT ์—ฐ๊ฒฐ๋ถ€ํ„ฐ ํ† ํ”ฝ ๊ตฌ๋…๊นŒ์ง€์˜ ํ•ต์‹ฌ ๊ตฌํ˜„ ๊ณผ์ •์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

MqttClient ์ƒ์„ฑ

๊ฐ€์žฅ ๋จผ์ € MQTT ๋ธŒ๋กœ์ปค์™€ ํ†ต์‹ ํ•  MqttAndroidClient ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

class PahoMqttTransport(...) {
    private val callback = object : MqttCallbackExtended {
        override fun connectComplete(reconnect: Boolean, serverURI: String?) {
            // ์—ฐ๊ฒฐ ๋˜๋Š” ์žฌ์—ฐ๊ฒฐ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์„ ๋•Œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
        }

        override fun connectionLost(cause: Throwable?) {
            // ์—ฐ๊ฒฐ ๋Š๊น€ ์›์ธ ๋กœ๊น… ๋“ฑ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
        }

        override fun messageArrived(topic: String?, message: MqttMessage?) {
            // ๊ตฌ๋… ์ค‘์ธ ํ† ํ”ฝ์œผ๋กœ ๋ฉ”์‹œ์ง€๊ฐ€ ๋„์ฐฉํ–ˆ์„ ๋•Œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
            scope.launch {
                _message.emit(message?.payload?.decodeToString())
            }
        }

        override fun deliveryComplete(token: IMqttDeliveryToken?) {
            // ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰(publish)์ด ์™„๋ฃŒ๋˜์—ˆ์„ ๋•Œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. (ํ‘ธ์‹œ ์ˆ˜์‹ ๋งŒ ๊ตฌํ˜„ํ•  ๊ฒฝ์šฐ ๋ถˆํ•„์š”)
        }
    }

    val mqttClient = MqttAndroidClient(
        context = context,     // Application Context
        serverURI = serverUri, // "wss://{your-domain}:443"
        clientId = clientId    // ํด๋ผ์ด์–ธํŠธ๋ฅผ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•œ ๊ณ ์œ  ID (e.g., Device ID)
    ).apply {
        setCallback(callback)
    }
}
  • serverURI: ์›น์†Œ์ผ“ ๊ธฐ๋ฐ˜์œผ๋กœ ์—ฐ๊ฒฐํ•˜๋ฏ€๋กœ wss://{๋„๋ฉ”์ธ}:443 ํ˜•์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • clientId: MQTT ๋ธŒ๋กœ์ปค๊ฐ€ ๊ฐ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•œ ๊ณ ์œ ํ•œ ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค. ์ €ํฌ ํŒ€์€ ๊ธฐ๊ธฐ์˜ Device ID๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋ธŒ๋กœ์ปค ์—ฐ๊ฒฐ

๋‹ค์Œ์œผ๋กœ, MqttConnectOptions๋ฅผ ์„ค์ •ํ•˜์—ฌ ๋ธŒ๋กœ์ปค์— ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.

fun connect(password: String) {
    val options = MqttConnectOptions().apply {
        this.userName = clientId
        this.password = password.toCharArray()
        this.mqttVersion = MqttConnectOptions.MQTT_VERSION_3_1_1
        this.isAutomaticReconnect = true
        this.isCleanSession = false
    }

    mqttClient.connect(options)
}
  • password: ๋ธŒ๋กœ์ปค ์—ฐ๊ฒฐ์— ํ•„์š”ํ•œ ์ธ์ฆ ์ •๋ณด์ž…๋‹ˆ๋‹ค. ํ”„๋กฌ์—์„œ๋Š” ๋ฐฑ์—”๋“œ ์„œ๋ฒ„์—์„œ ๋ฐœ๊ธ‰๋ฐ›์€ ํ† ํฐ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • isAutomaticReconnect: ๋„คํŠธ์›Œํฌ ๋ฌธ์ œ ๋“ฑ์œผ๋กœ ์—ฐ๊ฒฐ์ด ๋Š์–ด์กŒ์„ ๋•Œ ์ž๋™์œผ๋กœ ์žฌ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ• ์ง€ ์—ฌ๋ถ€๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
  • isCleanSession:
    • true๋กœ ์„ค์ •ํ•˜๋ฉด ์—ฐ๊ฒฐ๋งˆ๋‹ค ์ƒˆ๋กœ์šด ์„ธ์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ, ์žฌ์—ฐ๊ฒฐ๋  ๋•Œ๋งˆ๋‹ค ๊ตฌ๋…ํ–ˆ๋˜ ํ† ํ”ฝ์„ ๋‹ค์‹œ ๊ตฌ๋…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    • false๋กœ ์„ค์ •ํ•˜๋ฉด ๊ธฐ์กด ์„ธ์…˜์„ ์œ ์ง€ํ•˜์—ฌ, ์žฌ์—ฐ๊ฒฐ ์‹œ ์ด์ „ ๊ตฌ๋… ์ •๋ณด๊ฐ€ ์ž๋™์œผ๋กœ ๋ณต์›๋ฉ๋‹ˆ๋‹ค.

ํ† ํ”ฝ ๊ตฌ๋…

์—ฐ๊ฒฐ์ด ์™„๋ฃŒ๋˜๋ฉด, ํ‘ธ์‹œ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•  ํ† ํ”ฝ(Topic)์„ ๊ตฌ๋…ํ•ฉ๋‹ˆ๋‹ค.

fun subscribe(topic: String, qos: Int) {
    mqttClient.subscribe(topic = topic, qos = qos)
}
  • ํ”„๋กฌ์—์„œ๋Š” ํ‘ธ์‹œ์šฉ์œผ๋กœ ๋‹จ์ผ ํ† ํ”ฝ์„ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ์—ฌ๋Ÿฌ ํ† ํ”ฝ์„ ๋ฐฐ์—ด๋กœ ์ „๋‹ฌํ•˜์—ฌ ํ•œ ๋ฒˆ์— ๊ตฌ๋…ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ‘ธ์‹œ์˜ QoS๋Š” 0 (At most once)์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ MQTT ํด๋ผ์ด์–ธํŠธ์˜ ๊ธฐ๋ณธ์ ์ธ ์—ฐ๊ฒฐ ๋ฐ ๊ตฌ๋… ์„ค์ •์„ ๋งˆ์ณค์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์„ ์ข…๋ฃŒํ•œ ์ƒํƒœ์—์„œ๋„ ํ‘ธ์‹œ๋ฅผ ๋ฐ›์œผ๋ ค๋ฉด ์ด MQTT ์—ฐ๊ฒฐ์„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๊ณ„์† ์œ ์ง€ํ•ด ์ค„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

4. ์•ˆ์ •์ ์ธ ํ‘ธ์‹œ ์ˆ˜์‹ ์„ ์œ„ํ•œ Immortal Service

์ถœ์ฒ˜: Wikipedia - Immortan Joe, Mad Max

์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์„ ์ข…๋ฃŒํ•œ ํ›„์—๋„ MQTT ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•˜์—ฌ ํ‘ธ์‹œ๋ฅผ ์ˆ˜์‹ ํ•˜๊ธฐ ์œ„ํ•ด, ์ €ํฌ๋Š” ์†Œ์œ„ โ€˜์ฃฝ์ง€ ์•Š๋Š” ์„œ๋น„์Šค(Immortal Service)โ€˜๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ฌผ๋ก  ์•ˆ๋“œ๋กœ์ด๋“œ์˜ ๊ฐ•ํ™”๋œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ œํ•œ ์ •์ฑ…(Doze ๋ชจ๋“œ, Foreground Service ์‹คํ–‰ ์ œํ•œ, ์ œ์กฐ์‚ฌ๋ณ„ ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™” ๋“ฑ) ๋•Œ๋ฌธ์— 100% ์™„๋ฒฝํ•œ ๋ถˆ๋ฉธ์˜ ์„œ๋น„์Šค๋Š” ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ €ํฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ „๋žต๋“ค์„ ์กฐํ•ฉํ•˜์—ฌ ์„œ๋น„์Šค์˜ ์ƒ์กด ๊ฐ€๋Šฅ์„ฑ์„ ์ตœ๋Œ€ํ•œ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

1) Foreground Service ํ™œ์šฉ

์„œ๋น„์Šค๋ฅผ Foreground Service๋กœ ์‹คํ–‰ํ•˜๋ฉด ์ผ๋ฐ˜ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์„œ๋น„์Šค๋ณด๋‹ค ์ข…๋ฃŒ๋  ํ™•๋ฅ ์ด ํ›จ์”ฌ ๋‚ฎ์•„์ง‘๋‹ˆ๋‹ค. ๋˜ํ•œ onStartCommand()์—์„œ START_STICKY๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •ํ•˜๋ฉด, ๋ฉ”๋ชจ๋ฆฌ ๋ถ€์กฑ ๋“ฑ์˜ ์ด์œ ๋กœ ์‹œ์Šคํ…œ์— ์˜ํ•ด ์„œ๋น„์Šค๊ฐ€ ๊ฐ•์ œ ์ข…๋ฃŒ๋˜๋”๋ผ๋„ ์‹œ์Šคํ…œ ์ž์›์ด ํ™•๋ณด๋˜์—ˆ์„ ๋•Œ ์ž๋™์œผ๋กœ ์„œ๋น„์Šค๋ฅผ ์žฌ์‹œ์ž‘ํ•ด ์ค๋‹ˆ๋‹ค.

class FrommMqttMessagingService : Service() {
    // ...

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ์„ ํ‘œ์‹œํ•˜๊ณ  ํฌ๊ทธ๋ผ์šด๋“œ ์„œ๋น„์Šค ์‹œ์ž‘
        startForegroundService()

        // MQTT ์—ฐ๊ฒฐ ๋ฐ ํ† ํ”ฝ ๊ตฌ๋… ๋กœ์ง ์ˆ˜ํ–‰
        pahoMqttTransport.connect(password)
        pahoMqttTransport.subscribe(topic, qos)

        // ์„œ๋น„์Šค ๋น„์ •์ƒ ์ข…๋ฃŒ ์‹œ ์‹œ์Šคํ…œ์ด ์ž๋™์œผ๋กœ ์žฌ์‹œ์ž‘
        return START_STICKY
    }
}

2) ๊ธฐ๊ธฐ ๋ถ€ํŒ… ์‹œ ์„œ๋น„์Šค ์ž๋™ ์‹คํ–‰

BroadcastReceiver๊ฐ€ ๊ธฐ๊ธฐ ๋ถ€ํŒ… ์™„๋ฃŒ(ACTION_BOOT_COMPLETED) ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์ง€ํ•˜๋ฉด, ์„œ๋น„์Šค๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

class BootCompletedReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
            val serviceIntent = Intent(context, FrommMqttMessagingService::class.java)
            ContextCompat.startForegroundService(context, serviceIntent)
        }
    }
}

๋ฌผ๋ก  ์‹ค์ œ ํ”„๋กฌ ์•ฑ์—์„œ๋Š” ํ‘ธ์‹œ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ฑ„ํŒ… ๊ธฐ๋Šฅ์—๋„ MQTT๋ฅผ ํ™œ์šฉํ•˜๊ธฐ์— ๋” ์ •๊ตํ•œ ๊ตฌ์กฐ๋กœ ์„ค๊ณ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์œ„ ์ฝ”๋“œ๋Š” ํ•ต์‹ฌ ๋กœ์ง์„ ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•œ ์˜ˆ์‹œ๋กœ ์ฐธ๊ณ ํ•ด ์ฃผ์‹œ๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

์ตœ์‹  ์•ˆ๋“œ๋กœ์ด๋“œ OS ๋Œ€์‘์˜ ์–ด๋ ค์›€

์•ˆ๋“œ๋กœ์ด๋“œ OS์˜ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ •์ฑ…์€ ๊ณ„์†ํ•ด์„œ ๊ฐ•ํ™”๋˜๊ณ  ์žˆ์–ด ์ถ”๊ฐ€์ ์ธ ๋Œ€์‘์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

  • Android 14: Foreground Service์— ๋ฐ˜๋“œ์‹œ foregroundServiceType์„ ๋ช…์‹œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ €ํฌ๋Š” ํ‘ธ์‹œ ๋ชฉ์ ์ด๋ฏ€๋กœ remoteMessaging ํƒ€์ž…์„ ์ง€์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • Android 15: ๋ถ€ํŒ…(BOOT_COMPLETED) ์งํ›„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ Foreground Service๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์— ์ œํ•œ์‚ฌํ•ญ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ : Android 15 ๋™์ž‘ ๋ณ€๊ฒฝ์‚ฌํ•ญ โ€” ํฌ๊ทธ๋ผ์šด๋“œ ์„œ๋น„์Šค

์ด๋Ÿฌํ•œ ๋ณ€๊ฒฝ์ ์— ๋Œ€์‘ํ•˜๊ณ  ์žˆ์ง€๋งŒ, ์ผ๋ถ€ ๊ธฐ๊ธฐ์—์„œ๋Š” ์—ฌ์ „ํžˆ ๊ฐ„ํ—์ ์œผ๋กœ ์„œ๋น„์Šค๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๋Š” ์‚ฌ๋ก€๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ  ์žˆ์–ด ์ง€์†์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๊ฐœ์„ ์ด ํ•„์š”ํ•œ ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค.

5. FCM๊ณผ MQTT ํ†ตํ•ฉ ๊ด€๋ฆฌ: ์–ด๋Œ‘ํ„ฐ ํŒจํ„ด์œผ๋กœ ๊ตฌ์กฐ ๊ฐœ์„ ํ•˜๊ธฐ

ํ”„๋กฌ ์•ฑ์€ ์ผ๋ฐ˜์ ์ธ ํ™˜๊ฒฝ์—์„œ๋Š” FCM์„, ์ค‘๊ตญ ํ™˜๊ฒฝ์—์„œ๋Š” MQTT๋ฅผ ํ†ตํ•ด ํ‘ธ์‹œ๋ฅผ ์ˆ˜์‹ ํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๊ฒฝ๋กœ๋Š” ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ๋…ธ์ถœํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ์ฒด๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. FCM์€ RemoteMessage ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ˜๋ฉด, MQTT๋Š” JSONObject ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ด์ฒ˜๋Ÿผ ์„œ๋กœ ๋‹ค๋ฅธ ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ผ๊ด€๋œ ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ , ํ–ฅํ›„ ์œ ์ง€๋ณด์ˆ˜์™€ ํ™•์žฅ์ด ์‰ฌ์šด ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์–ด๋Œ‘ํ„ฐ(Adapter) ํŒจํ„ด์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

๋จผ์ € ์•ฑ์—์„œ ๋‹ค๋ฃจ๋Š” ๋ชจ๋“  ํ‘ธ์‹œ ํƒ€์ž…์„ ํ‘œ์ค€ํ™”๋œ PushMessage sealed interface๋กœ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

sealed interface PushMessage {
    data class Chat(...) : PushMessage
    data class Promotion(...) : PushMessage
    data class Channel(...) : PushMessage
    // ... ๊ธฐํƒ€ ํ‘ธ์‹œ ํƒ€์ž…
}

๊ทธ ๋‹ค์Œ, ์–ด๋–ค ๋ฐ์ดํ„ฐ ์†Œ์Šค๋“  ์ด ํ‘œ์ค€ PushMessage ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์—ญํ• ์„ ํ•  PushMessageAdapter ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

interface PushMessageAdapter {
    fun toPushMessage(): PushMessage
}

์ด์ œ ๊ฐ ๋ฐ์ดํ„ฐ ์†Œ์Šค์— ๋งž๋Š” ์–ด๋Œ‘ํ„ฐ ๊ตฌํ˜„์ฒด๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. FcmPushMessageAdapter๋Š” FCM์˜ RemoteMessage๋ฅผ, MqttPushMessageAdapter๋Š” MQTT์˜ JSONObject๋ฅผ ๋ฐ›์•„ ๊ฐ๊ฐ ํ‘œ์ค€ PushMessage๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

// FCM ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์ค€ PushMessage๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์–ด๋Œ‘ํ„ฐ
class FcmPushMessageAdapter(private val message: RemoteMessage) : PushMessageAdapter {
    override fun toPushMessage(): PushMessage {
        // message.data์˜ "action" ๊ฐ’์— ๋”ฐ๋ผ
        // PushMessage.Chat, PushMessage.Promotion ๋“ฑ์œผ๋กœ ํŒŒ์‹ฑํ•˜์—ฌ ๋ฐ˜ํ™˜
        ...
    }
}

// MQTT ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์ค€ PushMessage๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์–ด๋Œ‘ํ„ฐ
class MqttPushMessageAdapter(private val message: JSONObject, ...) : PushMessageAdapter {
    override fun toPushMessage(): PushMessage {
        // message.optString("action") ๊ฐ’์— ๋”ฐ๋ผ
        // PushMessage.Chat, PushMessage.Promotion ๋“ฑ์œผ๋กœ ํŒŒ์‹ฑํ•˜์—ฌ ๋ฐ˜ํ™˜
        ...
    }
}

๋งˆ์ง€๋ง‰์œผ๋กœ, ์‹ค์ œ ์•Œ๋ฆผ ์ฒ˜๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” PushMessageDelegator๊ฐ€ ์ด ๊ตฌ์กฐ๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

@Singleton
class PushMessageDelegator @Inject constructor(...) {
    suspend fun handlePushMessage(pushMessageAdapter: PushMessageAdapter) {
        when (val pushMessageType = pushMessageAdapter.toPushMessage()) {
            is PushMessage.Chat -> showChatNotification(...)
            is PushMessage.Channel -> showChannelNotification(...)
            is PushMessage.Promotion -> showPromotionNotification(...)
            // ...
        }
    }
}

๊ฐ ํ‘ธ์‹œ ์ˆ˜์‹  ์„œ๋น„์Šค๋Š” ์ž์‹ ์˜ ์ถœ์ฒ˜์— ๋งž๋Š” ์–ด๋Œ‘ํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ Delegator์— ์ „๋‹ฌํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

FCM ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•˜๋Š” FrommFirebaseMessagingService์—์„œ๋Š” onMessageReceived๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด, RemoteMessage๋ฅผ FcmPushMessageAdapter๋กœ ๊ฐ์‹ธ PushMessageDelegator์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

@AndroidEntryPoint
class FrommFirebaseMessagingService : FirebaseMessagingService() {

    @Inject
    lateinit var pushMessageDelegator: PushMessageDelegator

    override fun onMessageReceived(message: RemoteMessage) {
        scope.launch {
            pushMessageDelegator.handlePushMessage(
                FcmPushMessageAdapter(...)
            )
        }
    }
}

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, MQTT ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•˜๋Š” FrommMqttMessagingService์—์„œ๋Š” Mqtt ๋กœ๋ถ€ํ„ฐ ๋“ค์–ด์˜จ ๋ฉ”์‹œ์ง€๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ MqttPushMessageAdapter๋กœ ๊ฐ์‹ธ๊ณ , ๋™์ผํ•œ PushMessageDelegator์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

@AndroidEntryPoint
class FrommMqttMessagingService : Service() {

    @Inject
    lateinit var pushMessageDelegator: PushMessageDelegator
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        scope.launch {
            pahoMqttTransport.message.collect { message ->
                pushMessageDelegator.handlePushMessage(
                    MqttPushMessageAdapter(...)
                )
            }
        }
        return START_STICKY
    }
}

์ด๋Ÿฌํ•œ ๊ตฌ์กฐ๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ๋กœ์ง(Adapter)๊ณผ ์•Œ๋ฆผ ์ฒ˜๋ฆฌ ๋กœ์ง(Delegator)์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋•๋ถ„์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์žฅ์ ์„ ์–ป์—ˆ์Šต๋‹ˆ๋‹ค.

  • ์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ: ํ‘ธ์‹œ ๋ฐ์ดํ„ฐ์˜ ํ˜•์‹์ด ๋ณ€๊ฒฝ๋˜๋”๋ผ๋„ ํ•ด๋‹น ์–ด๋Œ‘ํ„ฐ๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ๋˜๋ฏ€๋กœ ๋ณ€๊ฒฝ์˜ ์˜ํ–ฅ ๋ฒ”์œ„๊ฐ€ ์ตœ์†Œํ™”๋ฉ๋‹ˆ๋‹ค.
  • ํ™•์žฅ์„ฑ ํ™•๋ณด: ๋งŒ์•ฝ ๋ฏธ๋ž˜์— ํ™”์›จ์ด ํ‘ธ์‹œ(HMS) ๋“ฑ ์ƒˆ๋กœ์šด ํ‘ธ์‹œ ์„œ๋น„์Šค๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค๋ฉด, ์ƒˆ๋กœ์šด ์–ด๋Œ‘ํ„ฐ ํด๋ž˜์Šค๋งŒ ๊ตฌํ˜„ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. PushMessageDelegator์˜ ์ฝ”๋“œ๋Š” ์ „ํ˜€ ์ˆ˜์ •ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

๋งˆ๋ฌด๋ฆฌ

์ž…์‚ฌ ํ›„ ์ฒซ ์Šค์ฟผ๋“œ ์—…๋ฌด๋กœ ์ฐธ์—ฌํ–ˆ๋˜ ์ค‘๊ตญ ๋Œ€์‘ ํ”„๋กœ์ ํŠธ๊ฐ€ ๋“œ๋””์–ด ์„ฑ๊ณต์ ์œผ๋กœ ๋งˆ๋ฌด๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ์ˆ  ๊ฒ€ํ† ๋ถ€ํ„ฐ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„, ๊ทธ๋ฆฌ๊ณ  ์‹ค์ œ ๊ตฌํ˜„์— ์ด๋ฅด๊ธฐ๊นŒ์ง€ ๋งŽ์€ ๊ณ ๋ฏผ๊ณผ ๋…ธ๋ ฅ์ด ํ•„์š”ํ–ˆ์ง€๋งŒ, ๊ทธ๋งŒํผ ๊ธฐ์ˆ ์ ์œผ๋กœ๋„ ๋งŽ์ด ์„ฑ์žฅํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ์†Œ์ค‘ํ•œ ๊ฒฝํ—˜์ด์—ˆ์Šต๋‹ˆ๋‹ค.

ํŠนํžˆ ๋ฐฐํฌ ํ›„ ์ค‘๊ตญ ์‚ฌ์šฉ์ž๋“ค๋กœ๋ถ€ํ„ฐ โ€œ๋งŒ๋“ค์–ด์ค˜์„œ ๊ณ ๋ง™๋‹คโ€, โ€œVPN ์—†์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด ๋„ˆ๋ฌด ์ข‹๋‹คโ€์™€ ๊ฐ™์€ ๊ธ์ •์ ์ธ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์•˜์„ ๋•Œ, ๋ชจ๋“  ๋…ธ๋ ฅ์„ ๋ณด์ƒ๋ฐ›๋Š” ๋“ฏํ•œ ๋ฟŒ๋“ฏํ•จ์„ ๋А๊ผˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ธ€์ด ์ค‘๊ตญ ํ™˜๊ฒฝ์—์„œ ํ‘ธ์‹œ ์„œ๋น„์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋ ค๋Š” ๋‹ค๋ฅธ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ์ž๋ถ„๋“ค๊ป˜ ์ž‘์€ ๋„์›€์ด ๋˜๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค.

๊ธด ๊ธ€ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ

Art Changes Life

๋…ธ๋จธ์Šค์™€ ํ•จ๊ป˜ ์—”ํ„ฐํ…Œํฌ ์‚ฐ์—…์„ ํ˜์‹ ํ•ด๋‚˜๊ฐˆ ๋ฉค๋ฒ„๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.

์ฑ„์šฉ ์ค‘์ธ ๊ณต๊ณ  ๋ณด๊ธฐ