
์ค๊ตญ์์๋ ๋๊ธฐ์ง ์๋ ํธ์, ์ด๋ ๊ฒ ๋ง๋ค์์ต๋๋ค(์๋๋ก์ด๋)

- #Mqtt
- #Push
- #Android
๋ค์ด๊ฐ๋ฉฐ
์๋
ํ์ธ์. ๋
ธ๋จธ์ค์์ ํ๋กฌ ์๋๋ก์ด๋ ์ฑ ๊ฐ๋ฐ์ ๋ด๋นํ๊ณ ์๋ ๊น๋ฏผ์์
๋๋ค.
์ต๊ทผ ํ๋กฌ ์ฑ์ ์ค๊ตญ ํ์ง์์๋ VPN ์์ด ์ํํ๊ฒ ์๋น์ค๋ฅผ ์ด์ฉํ ์ ์๋ ์ต์ ํ ๊ธฐ๋ฅ์ด ์ถ๊ฐ๋์์ต๋๋ค. ๐
์ด๋ฒ ๊ธ์์๋ ๊ทธ์ค ์๋๋ก์ด๋ ํ์์ MQTT๋ฅผ ํ์ฉํ์ฌ ์์ ์ ์ธ ํธ์(Push) ์์ ์๋น์ค๋ฅผ ๊ตฌ์ถํ ๊ฒฝํ์ ๊ณต์ ํ๊ณ ์ ํฉ๋๋ค.
์ด ๊ธ์์๋ MQTT์ ๊ธฐ๋ณธ ๊ฐ๋ ์ค๋ช ๋ณด๋ค๋, ์ ํฌ ์๋๋ก์ด๋ ํ์์ ์ด๋ค ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํํ๊ณ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋์ง์ ์ง์คํฉ๋๋ค. ์ ํฌ ํ์์ MQTT๋ฅผ ์ ํํ ๋ฐฐ๊ฒฝ์ด ๊ถ๊ธํ์๋ค๋ฉด ์๋ ์๋ฆฌ์ฆ ๊ธ์ ๋จผ์ ์ฝ์ด๋ณด์๋ ๊ฒ์ ์ถ์ฒ๋๋ฆฝ๋๋ค.
- ์ค๊ตญ์์๋ ํ๋กฌ ์ฐ๊ฒ ํด์ฃผ์ธ์. - 1ํธ
- ์ค๊ตญ์์๋ ํ๋กฌ ์ฐ๊ฒ ํด์ฃผ์ธ์. - 2ํธ
- ์ค๊ตญ ํ๊ฒฝ์์ ์ฑ ํธ์ ์์ ์ ์ผ๋ก ๋ฐ์กํ๊ธฐ
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
์ด๋ฌํ ์ ์ฝ์ ์ค๊ตญ ํ๊ฒฝ์์ ๋ ๊ฐ์ง ํฐ ๋ฌธ์ ๋ฅผ ์ผ๊ธฐํฉ๋๋ค.
- Google Play ์คํ ์ด์ ๋ถ์ฌ: ํ์จ์ด(Harmony OS) ๋ฑ ์ค๊ตญ ๋ด์์ฉ ๊ธฐ๊ธฐ์๋ Google Play ์คํ ์ด๊ฐ ํ์ฌ๋์ง ์์ ๊ฒฝ์ฐ๊ฐ ๋ง์ FCM์ ์์ฒ์ ์ผ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
- ์ค๊ตญ ๋ฐฉํ๋ฒฝ(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 ์์ด ์ฌ์ฉํ ์ ์์ด ๋๋ฌด ์ข๋คโ์ ๊ฐ์ ๊ธ์ ์ ์ธ ํผ๋๋ฐฑ์ ๋ฐ์์ ๋, ๋ชจ๋ ๋ ธ๋ ฅ์ ๋ณด์๋ฐ๋ ๋ฏํ ๋ฟ๋ฏํจ์ ๋๊ผ์ต๋๋ค.
์ด ๊ธ์ด ์ค๊ตญ ํ๊ฒฝ์์ ํธ์ ์๋น์ค๋ฅผ ๊ตฌํํ๋ ค๋ ๋ค๋ฅธ ์๋๋ก์ด๋ ๊ฐ๋ฐ์๋ถ๋ค๊ป ์์ ๋์์ด ๋๊ธฐ๋ฅผ ๋ฐ๋๋๋ค.
๊ธด ๊ธ ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค!