Spring Boot와 FCM으로 푸시 알림 서비스 구축하기 (A to Z 가이드)

이동휘·2025년 6월 30일
1

매일매일 블로그

목록 보기
37/49

안녕하세요! 이번 글에서는 Spring BootFirebase Cloud Messaging(FCM)을 연동하여, 안드로이드, iOS, 웹 애플리케이션에 푸시 알림을 보내는 서비스의 전체 구현 과정을 A to Z로 상세하게 다뤄보겠습니다.

FCM의 기본 개념부터 실제 개발 환경 구축, 핵심 기능인 토큰 및 토픽 관리, 그리고 성능 효율화 방안까지, 푸시 알림 시스템을 구축하는 데 필요한 모든 것을 이 글 하나에 담았습니다. 이 가이드가 여러분의 알림 서비스 개발 여정에 든든한 동반자가 되기를 바랍니다.


1. FCM(Firebase Cloud Messaging)이란 무엇일까요?

FCM은 Google에서 개발한 클라우드 기반의 백엔드 서비스로, 모바일 및 웹 애플리케이션 개발을 위한 강력한 플랫폼인 Firebase의 핵심 기능 중 하나입니다.

  • 주요 역할: 안드로이드, iOS, 웹 애플리케이션에 푸시 메시지를 안정적으로 전송하여, 사용자에게 새로운 콘텐츠나 중요한 업데이트를 알리는 역할을 합니다.
  • 핵심 장점: 가장 큰 장점은 크로스 플랫폼(Cross-Platform)을 지원한다는 점입니다. 개발자는 특정 플랫폼(Android, iOS 등)에 종속되지 않고, 단일 서버 코드와 API를 통해 여러 플랫폼에 일관된 방식으로 푸시 메시지를 보낼 수 있습니다.

FCM 메시지의 종류: 알림(Notification) vs. 데이터(Data)

FCM이 보내는 메시지는 크게 두 가지 유형으로 나뉩니다. 이 둘의 차이를 이해하는 것이 FCM 활용의 첫걸음입니다.

  1. 알림 메시지 (Notification Message):
    • 특징: 사용자에게 직접적으로 표시되는 메시지입니다. (예: 스마트폰 상단 바에 뜨는 알림, 앱 아이콘의 배지)
    • 동작 방식 (앱 상태에 따라 다름):
      • 백그라운드 상태: 앱이 꺼져 있거나 백그라운드에서 실행 중일 때는, 메시지가 시스템의 알림 트레이(Notification Tray)로 자동 전송됩니다. 사용자가 알림을 탭하면 앱이 실행됩니다.
      • 포그라운드 상태: 앱이 활성화되어 사용 중일 때는, 메시지 수신 시 앱 내의 특정 콜백 함수가 호출되어 개발자가 정의한 로직에 따라 메시지를 처리하게 됩니다. (예: 인앱 팝업 표시)
  2. 데이터 메시지 (Data Message):
    • 특징: 사용자에게 직접 보이지 않고, 개발자가 정의한 Key-Value 형식의 커스텀 데이터를 담고 있는 메시지입니다.
    • 동작 방식: 앱의 상태(포그라운드, 백그라운드)와 상관없이 항상 클라이언트 앱의 콜백 함수로 전달됩니다. 이를 통해 개발자는 백그라운드에서도 특정 데이터 업데이트, UI 변경, 동기화 등 원하는 로직을 자유롭게 수행할 수 있습니다.

💡 일반적인 사용 패턴:

실제 서비스에서는 알림 메시지와 데이터 메시지를 함께 조합하여 사용하는 경우가 많습니다. 사용자에게는 titlebody를 가진 알림 메시지를 보여주고, 이 알림 메시지에 data 페이로드를 함께 담아 보냅니다. 사용자가 알림을 클릭했을 때, 클라이언트 앱은 데이터 메시지에 담긴 정보(예: 특정 게시글 ID, 이동할 페이지 URL)를 바탕으로 특정 페이지로 이동시키거나 특별한 동작을 수행하게 만드는 방식입니다.

메시지 전송 방식: 누구에게 보낼 것인가?

  1. 단일 기기 / 기기 그룹 전송 (FCM 토큰 사용):
    • FCM 토큰: 앱이 설치된 디바이스마다 발급되는 고유한 등록 토큰(Registration Token)입니다. 서버는 이 토큰을 데이터베이스에 저장해두고, 특정 기기(사용자)에게만 메시지를 보내고 싶을 때 이 토큰을 사용합니다.
    • 기기 그룹: 여러 개의 FCM 토큰(최대 500개)을 하나의 그룹으로 묶어, 한 번의 API 호출로 같은 메시지를 보낼 수도 있습니다.
  2. 주제 구독 (Topic) 방식:
    • 토픽(Topic): '공지사항', '이벤트', '스포츠'와 같이 특정 주제를 미리 만들어 놓고, 사용자들이 이 주제를 구독하도록 하는 방식입니다.
    • 서버는 특정 토픽을 구독한 모든 사용자에게 한 번에 메시지를 보낼 수 있습니다. (예: '공지사항' 토픽으로 메시지 전송)
    • 클라이언트는 subscribeToTopic()unsubscribeFromTopic() 메서드를 호출하여 특정 토픽을 자유롭게 구독하거나 구독을 취소할 수 있습니다.

전체 아키텍처 및 로직 흐름

[클라이언트 앱]  <----(알림 수신)---->  [Firebase 서버]  <----(알림 요청)----  [Spring Boot 서버]
     ^                                                                      |
     |                                                                      |
     +------------(FCM 토큰 전달)------------> [Spring Boot 서버] ----(토큰 저장)----> [데이터베이스]
  1. [클라이언트] 토큰 발급 요청: 사용자가 앱에서 알림을 허용하면, 클라이언트 앱(예: React, Android, iOS)은 Firebase SDK를 통해 FCM 서버에 FCM 토큰 발급을 요청합니다.
  2. [서버] 토큰 저장: 클라이언트 앱은 발급받은 FCM 토큰을 Spring Boot 서버로 전달합니다. 서버는 사용자 정보(ID 등)와 FCM 토큰을 매핑하여 데이터베이스에 저장합니다.
  3. [서버] 알림 전송 로직: 특정 이벤트(예: 새로운 댓글 작성, 주문 완료)가 발생하여 푸시 알림을 보내야 할 때, 서버는 데이터베이스에서 알림을 받을 사용자의 FCM 토큰을 조회합니다.
  4. [서버 → Firebase] 알림 전송 요청: 서버는 조회한 FCM 토큰(또는 토픽 이름)과 보낼 메시지 내용을 담아 Firebase 서버로 API 요청을 보냅니다.
  5. [Firebase → 클라이언트] 최종 알림 전송: Firebase는 전달받은 요청을 처리하여, 해당 FCM 토큰을 가진 디바이스(사용자의 스마트폰 등)로 최종적으로 푸시 알림을 전송합니다.

2. 개발 환경 구축하기: Firebase와 Spring Boot 설정

이제 실제 개발을 위한 환경을 구축해 보겠습니다.

Firebase 환경 설정

Spring Boot 서버가 FCM과 통신하려면, 먼저 Firebase 프로젝트를 생성하고 서버가 자신을 인증할 수 있도록 비공개 키를 발급받아야 합니다.

  1. Firebase 프로젝트 생성:
    • Firebase 콘솔에 접속하여 '프로젝트 추가'를 통해 새로운 Firebase 프로젝트를 생성합니다.
    • FCM 사용 통계나 모니터링을 위해 Google 애널리틱스 사용을 활성화하는 것이 좋습니다.
  2. 서비스 계정 키 발급:
    • 생성된 프로젝트의 설정(톱니바퀴 아이콘) > 서비스 계정 탭으로 이동합니다.
    • 'Java' 탭을 선택한 후, '새 비공개 키 생성' 버튼을 클릭하여 .json 형식의 서비스 계정 키 파일을 다운로드합니다.
    • ⚠️ 중요: 이 JSON 파일은 서버의 모든 권한을 담고 있는 매우 민감한 정보이므로, 절대 외부에 노출되어서는 안 됩니다.

Spring Boot 프로젝트 설정

이제 Spring Boot 프로젝트를 생성하고, 위에서 발급받은 키를 이용해 Firebase와 연동할 준비를 합니다.

  1. 프로젝트 생성:
    • Spring Initializr를 통해 새로운 Spring Boot 프로젝트를 생성합니다. (Spring Boot 3.x, Java 17 이상 권장)
    • 기본 의존성으로 'Spring Web''Lombok'을 추가합니다.
  2. Firebase 비공개 키 적용:
    • 다운로드했던 Firebase 서비스 계정 .json 파일을 프로젝트의 src/main/resources/ 디렉터리 아래에 firebase/와 같은 폴더를 만들어 그 안에 복사합니다.
    • ⚠️ 보안 설정:.json 파일은 민감 정보이므로, 반드시 .gitignore 파일에 추가하여 Git과 같은 버전 관리 시스템에 커밋되지 않도록 해야 합니다.
  3. build.gradle 파일 수정 (의존성 추가):
    • Spring Boot에서 FCM 기능을 사용하기 위해 Firebase Admin SDK 라이브러리를 build.gradle (또는 pom.xml) 파일에 추가합니다. 이 라이브러리는 Java 서버 환경에서 Firebase의 다양한 서비스를 제어할 수 있게 해주는 도구입니다.
    // build.gradle
    implementation 'com.google.firebase:firebase-admin:9.1.1' // 버전은 최신으로 확인
  4. application.yml 파일 설정:
    • src/main/resources/ 폴더에 application.yml 파일을 생성하고, 프로젝트에 추가한 Firebase 비공개 키 파일의 경로를 설정합니다.
    # application.yml
    fcm:
      firebase:
        config:
          path: "classpath:firebase/내-서비스-계정-키-파일명.json"
    • ⚠️ 보안 설정: application.yml 파일에도 민감 정보가 포함될 수 있으므로, 이 파일 역시 .gitignore에 추가하고, 실제 운영 환경에서는 환경 변수나 외부 설정 관리 도구를 통해 값을 주입하는 것이 안전합니다.
  5. FCM 초기화 클래스 생성:
    • Spring Boot 애플리케이션이 시작될 때 Firebase와의 연결을 초기화하는 컴포넌트를 생성합니다.
    • @PostConstruct 어노테이션을 사용하면, Spring Bean이 생성되고 의존성 주입이 완료된 후 단 한 번만 Firebase 초기화 코드가 실행되도록 보장할 수 있습니다.
// FcmInitializer.java
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.InputStream;

@Slf4j
@Component
public class FcmInitializer {

    @Value("${fcm.firebase.config.path}")
    private String firebaseConfigPath;

    @PostConstruct
    public void initialize() {
        try {
            ClassPathResource resource = new ClassPathResource(firebaseConfigPath);
            try (InputStream stream = resource.getInputStream()) {
                FirebaseOptions options = FirebaseOptions.builder()
                        .setCredentials(GoogleCredentials.fromStream(stream))
                        .build();

                if (FirebaseApp.getApps().isEmpty()) {
                    FirebaseApp.initializeApp(options);
                    log.info("Firebase app has been initialized successfully.");
                }
            }
        } catch (IOException e) {
            log.error("Error initializing Firebase app", e);
        }
    }
}

이 모든 과정이 완료되면, Spring Boot 서버는 FCM으로 메시지를 보낼 준비를 마치게 됩니다.

🤔 꼬리 질문: @PostConstruct를 사용한 초기화 방식의 장점은 무엇이며, 만약 초기화에 실패했을 경우 애플리케이션을 어떻게 처리하는 것이 안전할까요? (예: 애플리케이션 실행 중단, 재시도 로직 등)


3. 토큰 및 토픽 관리: 안정적인 알림 전송의 핵심

실제 서비스에서는 FCM 토큰을 코드에 하드코딩하는 것이 아니라, 데이터베이스 등을 이용해 체계적으로 관리해야 합니다.

FCM 토큰의 특징과 갱신 주기

  • FCM 토큰은 영구적이지 않습니다. 앱 삭제/재설치, 데이터 초기화, 사용자가 오랜 기간 앱을 사용하지 않는 등 다양한 상황에서 갱신될 수 있습니다.
  • 따라서 FCM 관리의 핵심은 토큰의 신선도(Freshness)를 유지하고, 더 이상 유효하지 않은 토큰(Stale Token)을 정리하는 것입니다.

Firebase 공식 문서가 권장하는 관리 방법

  1. 토큰 저장 및 타임스탬프 기록: 클라이언트 앱은 시작될 때마다 현재 FCM 토큰을 서버로 전송합니다. 서버는 이 토큰을 DB에 저장하거나, 이미 존재한다면 타임스탬프(마지막 접속 시간)를 현재 시간으로 업데이트하여 신선도를 유지합니다.
  2. 비활성 토큰 정기적 삭제: 2개월 이상 접속 기록이 없는 오래된 토큰은 불필요한 리소스 낭비를 막기 위해 스케줄링된 작업을 통해 주기적으로 삭제합니다.
  3. 유효하지 않은 토큰 즉시 삭제: FCM 서버로 메시지를 보냈을 때, 응답으로 '유효하지 않은 토큰' 관련 에러(예: UNREGISTERED, INVALID_ARGUMENT)가 반환되면, 즉시 데이터베이스에서 해당 토큰을 삭제해야 합니다.
  4. Google 애널리틱스 활용: 실제 발송된 알림 수와 애널리틱스에 집계된 수신 수를 비교하여 토큰 관리 상태를 점검하고 정제율을 파악할 수 있습니다.

관계형 DB를 활용한 관리 예시 (MySQL)

  • 테이블 스키마: User(사용자), FcmToken(FCM 토큰), Topic(토픽) 테이블을 설계합니다. UserFcmToken은 1:N 관계(한 명의 사용자가 여러 기기에서 로그인 가능)로 구성하고, TopicSubscription과 같은 매핑 테이블을 통해 구독 정보를 관리합니다.
  • 로그인 시 토큰 관리 로직:
    1. 사용자가 로그인할 때 클라이언트로부터 FCM 토큰을 함께 받습니다.
    2. 서버는 해당 토큰이 DB에 이미 있는지 확인합니다.
    3. 기존 토큰이면: 타임스탬프를 현재 시간으로 업데이트합니다.
    4. 새로운 토큰이면: DB에 새로 저장하고, 이 사용자가 기존에 구독하고 있던 토픽이 있다면 새로운 토큰도 해당 토픽들을 모두 구독하도록 처리합니다.
  • 만료/잘못된 토큰 삭제 로직:
    • 스케줄링: @Scheduled 어노테이션을 사용하여 매일 특정 시간에 오래된 토큰을 삭제하는 배치 작업을 실행합니다.
    • 예외 처리: FCM 메시지 전송 시 FirebaseMessagingExceptiontry-catch 문으로 잡아, 예외 종류에 따라 해당 토큰을 즉시 DB에서 삭제하는 로직을 구현합니다.

4. 알람 기능 및 성능 효율화 방안

예약 알림 기능 구현 (스케줄링 활용)

FCM 자체에는 "N월 N일 N시에 알림을 보내라"와 같은 예약 전송 기능이 없습니다. 하지만 Spring Boot의 스케줄링 기능을 활용하면 손쉽게 구현할 수 있습니다.

  1. @EnableScheduling 어노테이션으로 스케줄링 기능을 활성화합니다.
  2. @Scheduled 어노테이션을 사용하여 특정 시간마다(예: 매 분, 매 10초) 특정 메서드가 실행되도록 설정합니다.
  3. 이 스케줄링된 메서드가 DB를 조회하여, 현재 시간에 보내야 할 알람이 있는지 확인하고 푸시 알림을 보내는 방식으로 알람 기능을 구현할 수 있습니다.

성능 효율화 방안

FCM HTTP v1 API는 기본적으로 동기(Synchronous) 방식으로 동작하여, 요청마다 응답을 기다리기 때문에 많은 사용자에게 동시에 보낼 때 비효율적일 수 있습니다. 다음과 같은 방법으로 성능을 개선할 수 있습니다.

  1. 비동기(Asynchronous) 구현: Firebase Admin SDK가 제공하는 sendAsync(), subscribeToTopicAsync()Async 접미사가 붙은 메서드를 사용하면, 응답을 기다리지 않고 다음 작업을 즉시 처리하여 전체적인 처리량을 높일 수 있습니다. (CompletableFuture 등을 활용)
  2. 일괄 전송 (Batch Sending): 최대 500개의 메시지를 하나의 API 호출로 묶어서 보낼 수 있는 sendEach() 또는 sendAll()과 같은 일괄 전송 기능을 활용하면 네트워크 왕복 횟수를 크게 줄여 성능을 향상시킬 수 있습니다.
  3. Redis 활용: 빈번하게 조회되는 FCM 토큰 정보를 MySQL과 같은 디스크 기반 DB 대신 Redis와 같은 인메모리(In-Memory) DB에 캐싱하면, 토큰 조회 속도를 획기적으로 개선하여 알림 전송 로직의 전체 성능을 높일 수 있습니다.

🤔 꼬리 질문: 대량의 사용자에게 알림을 보내야 할 때, 일괄 전송(Batch Sending) 시 발생할 수 있는 부분 실패(Partial Failure)는 어떻게 처리하는 것이 좋을까요? (예: 성공한 토큰과 실패한 토큰을 분리하여 실패한 건에 대해서만 재시도)


결론: 안정적인 알림 서비스의 기반 다지기

지금까지 Spring Boot와 FCM을 연동하여 푸시 알림 서비스를 구축하는 전반적인 과정을 살펴보았습니다. 성공적인 알림 시스템의 핵심은 단순히 메시지를 보내는 것을 넘어, FCM 토큰의 생명주기를 얼마나 잘 관리하고, 대량 발송 시의 성능을 어떻게 최적화하며, 다양한 예외 상황에 얼마나 안정적으로 대처하느냐에 달려있습니다.

이 글에서 다룬 개념과 예시 코드를 바탕으로 여러분의 서비스에 맞는 견고하고 효율적인 알림 시스템을 구축하시기를 바랍니다. 긴 글 읽어주셔서 감사합니다

0개의 댓글