Backend/Spring Framework

[Spring Boot] 푸시 알림 전송 기능 구현하기 (FCM)

mopil 2023. 1. 24. 00:15
반응형

이번에는 FCM (Firebase Cloud Message)라는 무료 메시징 서비스를 통해서 앱,웹으로 푸시 알림을 보내는 API 예제를 작성하는 방법에 대해 알아본다.

 

# Firebase, Spring Boot 설정

다음 일련의 과정을 따라한다.

이름은 자유롭게 짓는다.

스프링 부트에 넣을 json 키를 생성해야 한다. 생성된 json 파일을 잘 모셔둔다.

그리고 스프링 부트 resources 디렉토리 밑에 그대로 넣어준다.

 

그리고 Gradle에 다음 의존성들을 추가한다.

implementation("com.google.firebase:firebase-admin:6.8.1")
implementation("com.squareup.okhttp3:okhttp:4.9.1")

okhttp는 파이어베이스에 토큰값을 요청할 때 사용할 예정이다.

 

# 전반적인 흐름도

먼저 용어 몇 가지를 알아야한다.

 

FCM 토큰

파이어베이스가 푸시 알림을 보내기 위한 특정 값 (주소라고 생각하면 된다.) 이 값은 파이어베이스 프로젝트를 생성하면 안드로이드, 웹, iOS 각각 디바이스에 1:1로 매핑되는 값이다.

 

타겟/토픽

FCM 토큰과 동일하나, 타겟은 1:1 토픽은 1:다 로 메시징이 가능하다. (그치만 둘 다 보내는 로직은 동일하게 FCM 토큰값으로 메시지를 보내면 된다.)

 

1. 파이어베이스 AccessToken을 받아온다. (아까 저장한 json 키 파일로)

2. 받아온 AccessToken을 이용해서 원하는 타겟 (FCM Token)으로 푸시 알림을 보낸다.

 

# FirebaseCloudMessageService

AccessToken 가져오기

private fun getAccessToken(): String {
    val firebaseConfigPath = "firebase/firebase_service_key.json"
    val credentials = GoogleCredentials
        .fromStream(ClassPathResource(firebaseConfigPath).inputStream)
        .createScoped(listOf("https://www.googleapis.com/auth/cloud-platform"))
    credentials.refreshIfExpired()
    return credentials.accessToken.tokenValue
}

메시지를 보내기 위해 필요한 AccessToken을 먼저 가져오는 로직을 작성한다.

 

Message 만들기

private fun makeMessage(targetToken: String, title: String, body: String): String {
    val notification = Notification(title = title, body = body)
    val message = Message(token = targetToken, notification = notification)
    return objectMapper.writeValueAsString(FcmMessage(message = message))
}

그 다음에 FCM 규격에 맞게 메시지를 작성 (요청 JSON 바디 값) 하는 함수를 작성한다.

규격은 파이어베이스 공식 홈페이지에 자세히 명시되어 있으나, 일단 푸시 알림을 보내기 위해서 필수적으로 갖춰야 하는 규격은 다음과 같다.

 

FcmMessage.kt

data class FcmMessage(
    val validate_only: Boolean = false,
    val message: Message
)

data class Message(
    val notification: Notification,
    val token: String
)

data class Notification(
    val title: String,
    val body: String
)

 

푸시 알림 보내기

fun sendDirectTo(targetToken: String, title: String, body: String) {
    val message = makeMessage(targetToken, title, body)

    val client = OkHttpClient()
    val requestBody = message
        .toRequestBody("application/json; charset=utf-8".toMediaType())
    val request = Request.Builder()
        .url(API_URL)
        .post(requestBody)
        .addHeader("Authorization", "Bearer ${getAccessToken()}")
        .build()
    val response = client.newCall(request).execute()
}

주의 할 점은 FCM 토큰 값이 잘못 되더라도 일단 푸시 알림 요청은 보내진다. 

제대로 전송되었는지 확인할려면 다른 단말기 (AOS, iOS 등)으로 직접 보내보는 수 밖에 없다.

 

FirebaseCloudMessageService

위 설명했던 코드를 하나의 서비스로 만들면 다음과 같다. 추가적으로 전체 유저에게 푸시알람을 비동기적으로 전송하는 메서드를 구현해봤다.

@Service
class FirebaseCloudMessageService(
    private val objectMapper: ObjectMapper,
    private val userRepository: UserRepository,
) {
    val log = logger()
    companion object {
        const val API_URL = "https://fcm.googleapis.com/v1/projects/test-maoman/messages:send"
    }

    private fun getAccessToken(): String {
        val firebaseConfigPath = "firebase/firebase_service_key.json"
        val credentials = GoogleCredentials
            .fromStream(ClassPathResource(firebaseConfigPath).inputStream)
            .createScoped(listOf("https://www.googleapis.com/auth/cloud-platform"))
        credentials.refreshIfExpired()
        return credentials.accessToken.tokenValue
    }

    fun sendDirectTo(targetToken: String, title: String, body: String) {
        val message = makeMessage(targetToken, title, body)

        val client = OkHttpClient()
        val requestBody = message
            .toRequestBody("application/json; charset=utf-8".toMediaType())
        val request = Request.Builder()
            .url(API_URL)
            .post(requestBody)
            .addHeader("Authorization", "Bearer ${getAccessToken()}")
            .build()
        val response = client.newCall(request).execute()

        println(response.body!!.string())
    }

    fun sendTopicTo(topicToken: String, title: String, body: String) {
        sendDirectTo(topicToken, title, body)
    }

    @Async
    fun sendAllUsers(title: String, body: String) {
        userRepository.findAll()
            .forEach {
                log.info("Sending to ${it.fcmToken}")
                sendDirectTo(it.fcmToken, title, body)
            }
    }

    private fun makeMessage(targetToken: String, title: String, body: String): String {
        val notification = Notification(title = title, body = body)
        val message = Message(token = targetToken, notification = notification)
        return objectMapper.writeValueAsString(FcmMessage(message = message))
    }
}

 

# FcmController

@RestController
class FcmController(private val firebaseCloudMessageService: FirebaseCloudMessageService) {

    @PostMapping("/send")
    fun send(@RequestBody messageDto: MessageDto): MessageDto {
        firebaseCloudMessageService.sendDirectTo(
            messageDto.targetToken,
            messageDto.title,
            messageDto.body
        )
        return messageDto
    }

    @PostMapping("/sendAll")
    fun sendAll(@RequestBody messageDto: MessageDto): MessageDto {
        firebaseCloudMessageService.sendAllUsers(
            messageDto.title,
            messageDto.body
        )
        return messageDto
    }
}

 

# 전체코드

https://github.com/mopil/spring-boot-kotlin-fcm-example

 

GitHub - mopil/spring-boot-kotlin-fcm-example: FCM 활용 간단 푸시 알람 예제

FCM 활용 간단 푸시 알람 예제. Contribute to mopil/spring-boot-kotlin-fcm-example development by creating an account on GitHub.

github.com

 

반응형