안녕하세요! 이번 글에서는 Spring Boot와 Firebase Cloud Messaging(FCM)을 연동하여, 안드로이드, iOS, 웹 애플리케이션에 푸시 알림을 보내는 서비스의 전체 구현 과정을 A to Z로 상세하게 다뤄보겠습니다.
FCM의 기본 개념부터 실제 개발 환경 구축, 핵심 기능인 토큰 및 토픽 관리, 그리고 성능 효율화 방안까지, 푸시 알림 시스템을 구축하는 데 필요한 모든 것을 이 글 하나에 담았습니다. 이 가이드가 여러분의 알림 서비스 개발 여정에 든든한 동반자가 되기를 바랍니다.
FCM은 Google에서 개발한 클라우드 기반의 백엔드 서비스로, 모바일 및 웹 애플리케이션 개발을 위한 강력한 플랫폼인 Firebase의 핵심 기능 중 하나입니다.
FCM이 보내는 메시지는 크게 두 가지 유형으로 나뉩니다. 이 둘의 차이를 이해하는 것이 FCM 활용의 첫걸음입니다.
💡 일반적인 사용 패턴:
실제 서비스에서는 알림 메시지와 데이터 메시지를 함께 조합하여 사용하는 경우가 많습니다. 사용자에게는
title
과body
를 가진 알림 메시지를 보여주고, 이 알림 메시지에data
페이로드를 함께 담아 보냅니다. 사용자가 알림을 클릭했을 때, 클라이언트 앱은 데이터 메시지에 담긴 정보(예: 특정 게시글 ID, 이동할 페이지 URL)를 바탕으로 특정 페이지로 이동시키거나 특별한 동작을 수행하게 만드는 방식입니다.
subscribeToTopic()
과 unsubscribeFromTopic()
메서드를 호출하여 특정 토픽을 자유롭게 구독하거나 구독을 취소할 수 있습니다.[클라이언트 앱] <----(알림 수신)----> [Firebase 서버] <----(알림 요청)---- [Spring Boot 서버]
^ |
| |
+------------(FCM 토큰 전달)------------> [Spring Boot 서버] ----(토큰 저장)----> [데이터베이스]
이제 실제 개발을 위한 환경을 구축해 보겠습니다.
Spring Boot 서버가 FCM과 통신하려면, 먼저 Firebase 프로젝트를 생성하고 서버가 자신을 인증할 수 있도록 비공개 키를 발급받아야 합니다.
.json
형식의 서비스 계정 키 파일을 다운로드합니다.이제 Spring Boot 프로젝트를 생성하고, 위에서 발급받은 키를 이용해 Firebase와 연동할 준비를 합니다.
.json
파일을 프로젝트의 src/main/resources/
디렉터리 아래에 firebase/
와 같은 폴더를 만들어 그 안에 복사합니다..json
파일은 민감 정보이므로, 반드시 .gitignore
파일에 추가하여 Git과 같은 버전 관리 시스템에 커밋되지 않도록 해야 합니다.build.gradle
파일 수정 (의존성 추가):build.gradle
(또는 pom.xml
) 파일에 추가합니다. 이 라이브러리는 Java 서버 환경에서 Firebase의 다양한 서비스를 제어할 수 있게 해주는 도구입니다.// build.gradle
implementation 'com.google.firebase:firebase-admin:9.1.1' // 버전은 최신으로 확인
application.yml
파일 설정:src/main/resources/
폴더에 application.yml
파일을 생성하고, 프로젝트에 추가한 Firebase 비공개 키 파일의 경로를 설정합니다.# application.yml
fcm:
firebase:
config:
path: "classpath:firebase/내-서비스-계정-키-파일명.json"
application.yml
파일에도 민감 정보가 포함될 수 있으므로, 이 파일 역시 .gitignore
에 추가하고, 실제 운영 환경에서는 환경 변수나 외부 설정 관리 도구를 통해 값을 주입하는 것이 안전합니다.@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
를 사용한 초기화 방식의 장점은 무엇이며, 만약 초기화에 실패했을 경우 애플리케이션을 어떻게 처리하는 것이 안전할까요? (예: 애플리케이션 실행 중단, 재시도 로직 등)
실제 서비스에서는 FCM 토큰을 코드에 하드코딩하는 것이 아니라, 데이터베이스 등을 이용해 체계적으로 관리해야 합니다.
UNREGISTERED
, INVALID_ARGUMENT
)가 반환되면, 즉시 데이터베이스에서 해당 토큰을 삭제해야 합니다.User
(사용자), FcmToken
(FCM 토큰), Topic
(토픽) 테이블을 설계합니다. User
와 FcmToken
은 1:N 관계(한 명의 사용자가 여러 기기에서 로그인 가능)로 구성하고, TopicSubscription
과 같은 매핑 테이블을 통해 구독 정보를 관리합니다.@Scheduled
어노테이션을 사용하여 매일 특정 시간에 오래된 토큰을 삭제하는 배치 작업을 실행합니다.FirebaseMessagingException
을 try-catch
문으로 잡아, 예외 종류에 따라 해당 토큰을 즉시 DB에서 삭제하는 로직을 구현합니다.FCM 자체에는 "N월 N일 N시에 알림을 보내라"와 같은 예약 전송 기능이 없습니다. 하지만 Spring Boot의 스케줄링 기능을 활용하면 손쉽게 구현할 수 있습니다.
@EnableScheduling
어노테이션으로 스케줄링 기능을 활성화합니다.@Scheduled
어노테이션을 사용하여 특정 시간마다(예: 매 분, 매 10초) 특정 메서드가 실행되도록 설정합니다.FCM HTTP v1 API는 기본적으로 동기(Synchronous) 방식으로 동작하여, 요청마다 응답을 기다리기 때문에 많은 사용자에게 동시에 보낼 때 비효율적일 수 있습니다. 다음과 같은 방법으로 성능을 개선할 수 있습니다.
sendAsync()
, subscribeToTopicAsync()
등 Async
접미사가 붙은 메서드를 사용하면, 응답을 기다리지 않고 다음 작업을 즉시 처리하여 전체적인 처리량을 높일 수 있습니다. (CompletableFuture
등을 활용)sendEach()
또는 sendAll()
과 같은 일괄 전송 기능을 활용하면 네트워크 왕복 횟수를 크게 줄여 성능을 향상시킬 수 있습니다.🤔 꼬리 질문: 대량의 사용자에게 알림을 보내야 할 때, 일괄 전송(Batch Sending) 시 발생할 수 있는 부분 실패(Partial Failure)는 어떻게 처리하는 것이 좋을까요? (예: 성공한 토큰과 실패한 토큰을 분리하여 실패한 건에 대해서만 재시도)
지금까지 Spring Boot와 FCM을 연동하여 푸시 알림 서비스를 구축하는 전반적인 과정을 살펴보았습니다. 성공적인 알림 시스템의 핵심은 단순히 메시지를 보내는 것을 넘어, FCM 토큰의 생명주기를 얼마나 잘 관리하고, 대량 발송 시의 성능을 어떻게 최적화하며, 다양한 예외 상황에 얼마나 안정적으로 대처하느냐에 달려있습니다.
이 글에서 다룬 개념과 예시 코드를 바탕으로 여러분의 서비스에 맞는 견고하고 효율적인 알림 시스템을 구축하시기를 바랍니다. 긴 글 읽어주셔서 감사합니다