실버케어 플랫폼 프로젝트

[FireBase]FCM 앱-스프링 연동

gotopm 2025. 4. 8. 15:07
구현 이유

 

지난번 포스트를 통해, 헬스커넥트의 데이터를 가지고 와서 스프링서버로 전송하는 자체 앱개발을 통하여

스마트워치를 통해 쌓은 헬스,수면데이터를 스프링서버로 가지고와서 웹서비스로 활용 할 수 있는 구조를 만들었다.

그런데 사실 웹서비스에서 실시간으로 바뀌는 내  건강데이터를  조회하려고 할 때마다 앱에서 스프링서버로 건강데이터를 보내야한다. 그러나 내가 개발한 앱은 화면에서 버튼을 누르면 스프링서버로 데이터가 전송되는 구조다.

따라서, 사용자가 서비스에서 건강데이터를 조회하려고 하면 먼저 스프링서버가 앱에게 '데이터를 달라' 라고 요청을 하고,

앱이 그에 따라 자동으로 데이터를 스프링서버로 전송하는 구조를 만들어야했다.

그런데 서버와 서버간에 통신을 하는 것처럼 서버와 앱은 통신할 수 없다.

앱은 서버처럼 계속 돌아가는 것이 아니라 종료상태에 있거나 백그라운드 상태에 있거나 절전모드에 있거나 하기 때문이다.

 

그렇다면 나의 스프링서버는 어떻게 앱에게 데이터를 보내달라고 요청을 보낼 수 있을까?

그것은 서버에서 메시지를 보내 앱을 깨울 수 있게 하는 용도로 Firebase Cloud Messaging을 활용하면 된다.

(Firebase Cloud Messaging : 구글이 제공하는 무료 푸시 메시지 전송 서비스)

 

즉,

스프링 서버가 ->파이어베이스에게 앱에게 데이터를 달라라고 알림을 보내면 -> 앱은 파이어베이스의 알림을 받는다.

그리고 앱은 이 파이어베이스의 알림을 트리거로 서버로 데이터를 보내도록 설계한다.

 


 

먼저 앱과 파이어베이스를 연동한다.

 

1.먼저 파이어베이스의 클라우드 메시징 탭에 들어가 프로젝트 시작하기를 누른다.

(파이어베이스 홈페이지 -> 상단 메뉴바의 '실행' -> CloudMessaging->프로젝트 시작하기)

 1-1.프로젝트 생성 시 구글 애널리틱스 사용은 비활성화에 체크한다.

 

 

2. 프로젝트 생성 후 안드로이드 아이콘 클릭하여 앱에 Firebase 추가 절차를 진행한다.

  2-1. 안드로이드 패키지 이름은 안드로이드 스튜디오의 build.gradle.kts(Module :app) 파일에에 적혀 있는 applicationId이름과 동일하게 설정해야 한다.->앱 닉네임, 디버그 서명인증서는  비워두고 앱등록을 클릭한다.

 

 2-2. 그리고 아래 화면에서 google-service.json을 다운받아 json파일을 내 앱 루트폴더에 위치시킨다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

3. 앱 코드 수정

 

우리는 위에서 프로젝트에 대한 정보가 담긴 json파일을 앱에 추가했다.
이 파일을 토대로 FirebaseSDK가 해당 프로젝트에 이 앱을 연결시킨다.
그리고 FirebaseMessagingService를 상속받은 클래스를 만들면
FirebaseSDK가 자동으로 디바이스 고유의 토큰을 만들어낸다.
토큰은 서버가 알림메세지를 특정 디바이스에 보내고자 할 때, 그 보낼 디바이스를 특정할 수 있게 해주는 요소다.

 

 

3-1. 따라서 먼저 앱에 FireBase SDK를 설치한다.

먼저 프로젝트 수준의 build.gradle(build.gradle.kts(Project:~))의 plugin{}부분에  
id("com.google.gms.google-services")~ 부분과 dependencies{} 에 아래의 코드를 추가해준다.

plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.kotlin.compose) apply false
    //추가한 부분
    // Add the dependency for the Google services Gradle plugin
    id("com.google.gms.google-services") version "4.4.2" apply false

}
.
.
.
dependencies {
        classpath("com.android.tools.build:gradle:7.4.0")  // 최신 Gradle 버전 사용
        classpath ("com.google.gms:google-services:4.4.2")
    }
}

모듈 수준의 build.gradle(build.gradle.kts(Module :app))의 plugins{} 부분과 dependecies{}부분에도 아래의 코드를 추가해준다.

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
    // Add the Google services Gradle plugin
    id("com.google.gms.google-services")

}
.
.
.
dependencies{
// Import the Firebase BoM
    implementation(platform("com.google.firebase:firebase-bom:33.12.0"))
    implementation("com.google.firebase:firebase-messaging")
    }

 

3-2. 그런 다음, FirebaseMessagingService를 상속받는 클래스를 만든다. 

그럼으로써,  파이어베이스 SDK가 자동으로 토큰을 만들면 앱은 이 토큰을 스프링 서버에 전달해야한다.

이것을 정의하는 것이 아래의 onNewToken 함수.(파이어베이스 SDK가 토큰을 생성하면 내부적으로 바로 이 함수가 실행된다)

그러면 스프링 서버는 이 토큰을 받아 저장해놓았다가,

향후 이 토큰을 가진 모바일 기기에 특정 알림 메시지를 보내달라고 파이어베이스에 요청 할 수 있는 것이다.

그리고 그러한 알림 메시지가 왔을때, 어떤 동작을 취하도록 설계하는 것이 아래의  onMessageReceived 함수.

package com.example.silverpotion

import android.util.Log
import com.example.silverpotion.network.RetrofitClient
import com.example.silverpotion.network.TokenRequest
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch


class MyFirebaseMessagingService : FirebaseMessagingService() {



    override fun onNewToken(token: String) {
        //token을 서버로 전송
        // 예시: Retrofit으로 Spring 서버에 POST 요청 보내기
        val phoneNumber = "01012345678" /사용자를 식별하기 위한 용도
        val request = TokenRequest(token,phoneNumber) //TokenRequest라는 데이터클래스를 미리 정의해야함
        CoroutineScope(Dispatchers.IO).launch {
            try {
                val response = RetrofitClient.apiService.sendDeviceToken(request) //sendDeviceToken은 ApiService.kt에 정의
                if (response.isSuccessful) {
                    Log.d("FCM", "서버에 토큰 전송 성공")
                } else {
                    Log.e("FCM", "서버에 토큰 전송 실패: ${response.code()}")
                }
            } catch (e: Exception) {
                Log.e("FCM", "서버 전송 중 에러", e)
            }
        }
    }

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        val action = remoteMessage.data["action"]
        Log.d("FCM", "메시지 수신됨!")
        if(action == "request_health_data"){
            HealthDataSender.fetchAndSend(applicationContext)
            //수신한 메시지를 처리
        }

    }

}

즉, 여기서는 파이어베이스SDK가 이 앱이 깔린 모바일 디바이스에 고유의 토큰을 생성하고

토큰이 생성되자마자 onNewToken에 의해 서버로 이 토큰이 보내진다.

나는 토큰과 전화번호를 함께 보내도록 설계해놓았는데, 이는 서버에서 전화번호를 받으면 해당 전화번호를 가진 유저를 찾아서 그 유저의 파이어베이스 토큰값을 세팅하게 하기 위함이다.

(헬스커넥트 데이터를 가지고와서 서버로 보내주기 위한 용도로 간단히 만든 앱이기에 전화번호를 하드코딩했다.)

 

그리고 스프링서버에서 파이어베이스에 요청해 이 기기에 어떠한 알림메시지를 보내면 이 기기의  onMessageReceived 함수가 발동된다. 이때 스프링서버에서 건강데이터 전송해줘 라고 보내는 요청 알림 메시지에 action이라는 키 값에 request_health_data라는 값을 넣어 보내도록 설계할 것인데, 만약 앱이 받은 알림 메세지 중  action이라는 키값에 request_health_data라는 value라면

앱이 건강데이터를 보내도록 onMessageReceived 함수로직을 짠 것이다.

 

 

이제 서버와 파이어베이스를 연동한다.

 

1.파이어베이스 콘솔에 접속해  비공개 키를 생성한다.

다시 파이어베이스 -> CloudMessaging -> 프로젝트에 들어와  아래사진처럼 프로젝트 개요->프로젝트 설정(톱니바퀴)->서비스 계정-> 아래에 자바를 클릭하고 새 비공개 키 생성을 누른다.

 

2.비공개 키를 스프링 서버파일에 추가한다.

이때 비공개 키 파일은 src-main-resources폴더 바로 밑에 위치시키도록 한다. 

이 비공개 키 파일은 파이어베이스가 자신의 프로젝트에 속하는 서버가 맞는지 검증할 수 있도록 해주는 키다.

우리는 파이어베이스에게 메시지를 요청할 때 이 비공개 키로 이 서버가 해당 프로젝트에 속하는 서버라는 것을 검증받을 수 있는 것이다.

 

3.서버 코드 수정.

3-1. build.grage에 파이어베이스 의존성을 추가한다.

//	파이어베이스 라이브러리 의존성 주입
	implementation 'com.google.firebase:firebase-admin:9.2.0'

 

3-2.FCM과 연결하기 위한  설정 클래스를 만듬

@Configuration
public class FireBaseFcmConfig {


   @PostConstruct //스프링이 서버시작 시 자동 실행
    public void initialize(){
        try {
            FirebaseOptions options = FirebaseOptions.builder() // Firebase Admin SDK를 초기화하기 위한 옵션을 만드는 빌더
                    .setCredentials( //Firebase에 접속하기 위해 서비스 계정 키(JSON)을 설정
                            //JSON키 파일을 파일 스트림으로 읽어옴. newClassPathResource의 인자에는 리소스 폴더 밑에 있는 파일을 명시
                            GoogleCredentials.fromStream(new ClassPathResource("resources폴더 밑에 있는 비공개키 파일이름").getInputStream())
                    ).build();
           //위에서 만든 옵션으로 sdk를 초기화. 이게 있어야 서버에서 FCM에 메세지를 보낼 수 있음
            FirebaseApp.initializeApp(options);
            System.out.println("Fcm 설정 성공");
        } catch (IOException e){
            System.out.println("FCM 연결 오류");
        }

    }

 

 

3-3.앱 쪽 코드를 보면 스프링서버로 토큰을 보내는 로직이 있었다. 따라서 이 토큰을 받는 api가 필요하다.

앱에서 String 타입의 토큰 값과 String타입의 전화번호를 보내도록 했으므로 이를 받는 dto인 TokenRequest클래스를 먼저 만들고 인자로 받았다.

@RestController
@RequestMapping("silverpotion/firebase")
public class FireBaseController {
    private final FireBaseService fireBaseService;

    public FireBaseController(FireBaseService fireBaseService) {
        this.fireBaseService = fireBaseService;
    }

//    1.앱으로 부터 파이어베이스 토큰 전송 받는 url(해당 토큰을 해당 유저에 저장)
    @PostMapping("/token")
    public void saveToken(@RequestBody TokenRequest request){
        fireBaseService.saveTokenToUser(request);
    }

3-4.

1번 함수는 이 토큰에 있는 전화번호로 유저를 찾아 이 유저의 파이어베이스 토큰 속성에 앱으로부터 받은 token을 저장하는 로직이다. 

2번 함수는 토큰값을 매개변수로 받아 메세지를 세팅하여 파이어베이스에 메세지 전송을 요청하는 함수다.

@Service
@Transactional
public class FireBaseService {
    private final UserRepository userRepository;

    public FireBaseService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

//  1.앱으로부터 파이어베이스 토큰 전송받는 url
    public void saveTokenToUser(TokenRequest tokenRequest){
        String phNumber = tokenRequest.getPhoneNumber();
        User user = userRepository.findByPhoneNumberAndDelYN(phNumber, DelYN.N).orElseThrow(()->new EntityNotFoundException("없는 회원입니다"));
        user.getFireBaseToken(tokenRequest);

    }

//  2.앱으로부터 알림메세지 보내는 로직(특정 유저의 파이어베이스 토큰을 받아 해당 유저의 앱에 푸시메시지를 보내겠다는 것)
    public void sendHealthSyncReq(String firebaseToken){
        Message message = Message.builder()
                .setToken(firebaseToken) //유저의 firebaseToken을 지정하면 해당 디바이스로 전송됨
                .putData("action","request_health_data") //메시지 안에 커스텀데이터를 넣는 부분. 앱이서 이 걸 보고 어떤 알림인지 인식하기 위한 용도
                .build();
    try {
        String response = FirebaseMessaging.getInstance().send(message); //Firebase SDK를 통해 메세지를 전송. 성공시 메시지 id가 문자열로 반환됨
        System.out.println("FCM메세지 전송 성공" + response);

    } catch (Exception e){
        System.out.println("FCM 전송 실패" + e.getMessage());
    }
    }


}

 

3-5. 나의 프로젝트에서는 사용자가 건강데이터를 조회하는 화면에 들어가면 아래 함수가 작동하도록 했다.

A사용자가 건강데이터를 조회하면 A사용자의 토큰값을 매개변수로 sendHealthSyncReq()함수가 호출된다. 

이 함수로 파이어베이스에 A사용자의 토큰값을 가지고 있는 모바일 디바이스에 건강데이터를 보내달라고 알림 요청메세지를 보내게되고 해당 토큰값을 가지고 있는 모바일 디바이스(당연히 A사용자의 모바일기기가 될 것이다)의 앱은 건강데이터를 스프링서버에게 보냄으로써 A사용자는 자신의 건강데이터를 웹에서도 볼 수 있도록 한 것이다.

//  1. 사용자의 앱에 헬스데이터 보내달라고 요청하는 api
    public void sendHealthDataReq(String loginId){
        User user = userRepository.findByLoginIdAndDelYN(loginId,DelYN.N).orElseThrow(()->new EntityNotFoundException("없는 회원입니다"));
        fireBaseService.sendHealthSyncReq(user.getFireBaseToken()); //유저의 파이어베이스 토큰을 매개로 유저의 디바이스에 헬스데이터보내달라고 알림을 보냄
    }

 

 


 

아직 서버를 배포하기 전이라 아래와 같이 포스트맨으로 테스트를 해보았다.

로그를 보면  sendHealthSyncReq() 함수가 잘 작동하여 파이어베이스에 요청 메세지를 보냈고,

 

앱에서도 파이어베이스로부터 메시지를 잘 전달받은 것을 로그를 보면 확인할 수 있다.

728x90