SSE (Server-Sent-Events) Kotlin으로 구현해보기

Purang·2024년 8월 14일

프로젝트 중 알림 기능이 필요해져 검색하던 와중 SSE에 대해 알게 되었다.

SSE vs 웹소켓
간단하게!

웹소켓 : 양방향으로 서버와 클라이언트 간의 데이터를 주고 받을 수 있다, 자동재접속은 안됨
SSE : 클라이언트는 데이터를 받을 수만 있게 된다 (일방향), 자동재접속 가능

장점

  • 구현이 쉽다
  • 연결이 끊겼을 때 자동으로 재연결

단점

  • 단방향으로 GET 방식만 지원
  • 파라미터를 보내는데 한계가 존재한다
    등 더 많은 장점과 단점이 존재하지만 간단하게만 작성해보겠습니다.

이제 안드로이드 스튜디오에서 SSE를 구현해보겠습니다.

dependencies {
    implementation("com.launchdarkly:okhttp-eventsource:4.1.0")
}

먼저 dependencies를 implementation 해준다

class SseEventHandler : BackgroundEventHandler {

    override fun onOpen() {
        // SSE 연결 성공시 처리 로직 작성
    }

    override fun onClosed() {
        // SSE 연결 종료시 처리 로직 작성
    }

    override fun onMessage(event: String, messageEvent: MessageEvent) {
        // SSE 이벤트 도착시 처리 로직 작성
        
        // event: String = 이벤트가 속한 채널 또는 토픽 이름
        // messageEvent.lastEventId: String = 도착한 이벤트 ID
        // messageEvent.data: String = 도착한 이벤트 데이터
    }

    override fun onComment(comment: String) {
    }

    override fun onError(t: Throwable) {
        // SSE 연결 전 또는 후 오류 발생시 처리 로직 작성
    	
        // 서버가 2XX 이외의 오류 응답시 com.launchdarkly.eventsource.StreamHttpErrorException: Server returned HTTP error 401 예외가 발생
        // 클라이언트에서 서버의 연결 유지 시간보다 짧게 설정시 error=com.launchdarkly.eventsource.StreamIOException: java.net.SocketTimeoutException: timeout 예외가 발생
        // 서버가 연결 유지 시간 초과로 종료시 error=com.launchdarkly.eventsource.StreamClosedByServerException: Stream closed by server 예외가 발생
    }
}
// EventSource 오브젝트 생성
val eventSource: BackgroundEventSource = BackgroundEventSource
    .Builder(
        SseEventHandler(),
        EventSource.Builder(
            ConnectStrategy
                .http(URL("{server-url}"))
                // 커스텀 요청 헤더를 명시
                .header(
                    "Authorization",
                    "Bearer {token}"
                )
                .connectTimeout(3, TimeUnit.SECONDS)
                // 최대 연결 유지 시간을 설정, 서버에 설정된 최대 연결 유지 시간보다 길게 설정
                .readTimeout(600, TimeUnit.SECONDS)
        )
    )
    .threadPriority(Thread.MAX_PRIORITY)
    .build()

// EventSource 연결 시작
eventSource.start()

위 코드에서 오류가 발생하면 Logcat에서 오류를 확인할 수 있습니다.
서버 연결 유지 시간은 모두 끝나거나 실패 시 자동으로 재접속합니다

핸들러 같은 경우 알림 발생 시 notification을 띄우고 싶다면

import com.launchdarkly.eventsource.MessageEvent
import com.launchdarkly.eventsource.background.BackgroundEventHandler

class SseHandler(private val context: Context) : BackgroundEventHandler {
    private val channelId = "channelId"
    private val channelName = "Channel Name"
    private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

    override fun onOpen() {
        // SSE 연결 성공시 처리 로직 작성
        Log.d("SSE", "SSE연결 성공")
    }

    override fun onClosed() {
        // SSE 연결 종료시 처리 로직 작성
        Log.d("SSE", "SSE 안전 종료")
    }

    override fun onMessage(event: String?, messageEvent: MessageEvent?) {

        val messageData = messageEvent?.data ?: return
        Log.e("SSE", "Received data: $messageData")

        val eventId = messageEvent.lastEventId
        val eventType = messageEvent.eventName
        val eventData = messageEvent.data

        println("Event ID: $eventId")
        println("Event Type: $eventType")
        println("Event Data: $eventData")

        val comment = if (eventData.contains(":")) {
            messageEvent.data.split(":").map { it.toString() }.last()
        } else {
            eventData
        }

        Log.e("SSE Comment", comment.toString())
        if (!comment.contains("EventStream")) {
            // Android 8.0 이상에서는 Notification Channel을 설정해야 합니다.
            val tapResultIntent = Intent(context, LoginActivity::class.java).apply {
                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
            }

            val pendingIntent = PendingIntent.getActivity(
                context,
                0,
                tapResultIntent,
                PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
            )


            val importance = NotificationManager.IMPORTANCE_DEFAULT
            val channel = NotificationChannel(channelId, channelName, importance).apply {
                description = "descriptionText"
            }
            val notificationManager: NotificationManager =
                context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(channel)

            val builder = NotificationCompat.Builder(context, channelId)
                .setSmallIcon(R.drawable.top_icon_vector)
                .setContentTitle("알림")
                .setContentText(comment)
                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                .setContentIntent(pendingIntent)
                .setAutoCancel(true)

            try {
                notificationManager.notify(1, builder.build())
                Log.d("Notification", "Notification created successfully")
            } catch (e: Exception) {
                Log.e("Notification", "Failed to create notification: ${e.message}")
            }
        }
    }

    override fun onComment(comment: String?) {
    }

    override fun onError(t: Throwable?) {
        Log.d("SSE", "SSE연결 실패")
        Log.e("SSE", t.toString())
        //java.net.SocketTimeoutException: timeout
    }
}

들어오는 messageEvent의 이벤트를 잘 확인하고 처리하는 방식을 신경써서 들어오는 메세지 대로 알림을 띄우는 방식입니다!


참조
black_han26님의 velog
surviveasdev.tistory.com
json-object.github.io/Implementing-SSE-logic
medium.com/@anugrahasb1997/implementing-server-sent-events-sse-in-android

profile
몰입의 즐거움

0개의 댓글