1. 개요
DoEatFit 서비스에 사용자의 참여를 극대화하기 위해 웹 브라우저/모바일 PWA 환경에서도 OS 네이티브 수준의 푸시 알림을 보낼 수 있는 FCM(Firebase Cloud Messaging) 기술을 성공적으로 도입한 과정과 그 이면의 아키텍처 및 보안 연동에 대한 회고록입니다.
FCM은 구글이 제공하는 무료 푸시 메시지 중계 서버입니다. 우리가 모든 사용자의 각기 다른 브라우저와 기기(아이폰, 안드로이드, 윈도우 등)의 통신 방식을 맞춰가며 직접 메시지를 쏴주는 것은 현실적으로 불가능합니다. 따라서 **“우리가 편지(알림)를 써서 구글 우체국(FCM)에 던져주면, 구글 우체부가 알아서 전 세계의 기기들을 찾아가 편지를 배달해 주는 마법”**과 같은 기술입니다.
2. 기술 설명 및 서비스 구현 방법
FCM 시스템은 크게 편지를 보내는 쪽(백엔드)과 편지를 받는 쪽(프론트엔드)으로 나뉘어 구현됩니다.
2-1. 프론트엔드 (편지 수신자 세팅)
프론트엔드(Next.js)에서는 우편함을 튼튼하게 세우고, 내가 누구인지 구글 우체국에 등록해 고유 식별 번호(Token)를 받아야 합니다.
- Service Worker (
sw.ts): 브라우저 탭이 꺼져있거나 백그라운드에 있을 때도 우체부가 편지를 몰래 우편함에 넣고 가면, 이 서비스 워커가 “알림 왔어요!” 하고 OS에 팝업을 띄워주는 24시간 경비원 역할을 수행합니다. - VAPID Key 인증: 무분별한 스팸 우편을 막기 위해, 구글 우체국에 내 웹앱이 정상적인 수신처임을 인증하는 공개키 쌍입니다. 보안을 위해 소스 코드에 하드코딩하지 않고 환경 변수로 은닉했습니다.
# .env (보안 처리됨) NEXT_PUBLIC_FIREBASE_API_KEY={YOUR_API_KEY} NEXT_PUBLIC_FIREBASE_VAPID_KEY={YOUR_VAPID_KEY}
2-2. 백엔드 (편지 발송자 세팅)
백엔드(Spring Boot)는 구글 우체국에 “이 편지를 이 사람들에게 배달해!” 라고 명령을 내리는 주체이므로 막강한 권한이 필요합니다.
- 비공개 서비스 계정 키 (Service Account Key): 이것은 구글 우체국에 명령을 내릴 수 있는 **마스터 키(Master Key)**입니다.
FcmService비동기 발송: 푸시 알림 발송은 외부 API를 호출하는 네트워크 I/O 작업이므로, 기존 사용자 서비스(DB 저장 등)가 느려지는 것을 방지하기 위해 Spring의@Async를 부여해 별도의 스레드에서 조용히 백그라운드로 발송되도록 최적화했습니다.
3. 어떻게 호출해서 사용하는가? (단일 진입점 아키텍처)
가장 심혈을 기울인 아키텍처 포인트는 “기존 코드를 건드리지 않고 어떻게 시스템 전체에 푸시 알림을 끼워 넣을 것인가?” 였습니다. DoEatFit 서비스 내에는 친구 요청, 조건 달성, 로그 작성 등 무수히 많은 알림 발생 위치가 존재합니다. 이를 일일이 찾아가 FCM 발송 코드를 넣는 것은 비효율의 극치입니다.
- Single Entry Point (단일 진입점)
기존 서비스의 모든 비즈니스 로직은 단 하나의 공통 메서드인
NotificationService.createNotification()을 거쳐 DB에 알림을 저장하고 있었습니다. - 해결책: 바로 이
createNotification()메서드 내부 가장 마지막 줄에 단 한 줄,fcmService.sendPushToUser(...)를 삽입했습니다. - 결과: 기존의 수많은 비즈니스 로직들은 자기가 FCM을 호출하는지도 모른 채 단순히 알림을 DB에 저장했을 뿐인데, 내부적으로 자동으로 구글 우체국에 편지가 위탁되는 마법 같은 결합도를 달성했습니다. 앞으로 새로운 알림 로직이 추가되어도 FCM 연동을 잊어버릴 일이 없습니다.
4. 클라이언트 로컬 푸시 알림과 사용자 설정 (UX 개선)
서버(백엔드)를 거쳐 오는 푸시 알림 외에도, 폰에서 독립적으로 돌아가는 휴식 시간 타이머와 같은 기능은 서버를 거칠 필요가 전혀 없습니다.
이를 위해 프론트엔드 자체적으로 브라우저 Notification API와 Service Worker를 직접 호출하여 로컬 PWA 푸시 알림을 발송하도록 구현했습니다.
[사용자 경험(UX) 최적화 포인트] 단순히 알림을 일방적으로 쏘는 것은 훌륭한 서비스가 아닙니다. “사용자가 알림을 제어할 권리”를 주기 위해 업계 표준인 마이페이지(프로필) 내비게이션에 [앱 설정] 카드를 신설했습니다.
- 로컬 스토리지 기반 토글: 휴식 타이머 알림은 기기(스마트폰)에 종속적인 설정이므로 백엔드 DB가 아닌 프론트엔드의
localStorage를 활용하여 온/오프 상태를 관리했습니다. - 동적 권한 요청: 사용자가 마이페이지에서 스위치를 ‘On’으로 켜는 순간, 백그라운드에서 브라우저의 권한(
Notification.requestPermission())을 검사하고 거부되어 있다면 기기 설정으로 유도하는 똑똑한 UX를 제공합니다.
5. 보안과 CI/CD 자동화 구축 (GitHub Actions)
백엔드가 구글 우체국과 통신하기 위한 마스터 키(serviceAccountKey.json)는 깃허브(GitHub)에 절대 올라가서는 안 되는 치명적인 보안 파일입니다. 이를 안전하게 다루기 위해 **“GitHub Actions Secrets를 활용한 동적 파일 주입 기법”**을 채택했습니다.
- 마스터 키의 은닉:
JSON 키 파일 내용을 깃허브 리포지토리의 보안 창고(
Settings -> Secrets)에{FIREBASE_SERVICE_ACCOUNT}라는 이름으로 봉인해 두었습니다. 로컬 코드 저장소에는 파일이 아예 존재하지 않습니다. - 동적 주입 파이프라인:
빌드 자동화 도구인 GitHub Actions 워크플로우에 다음 단계를 추가하여, CI/CD 배포가 도는 순간에만 허공에서 파일을 만들어내어 백엔드 품에 쏙 넣어주고 빌드를 진행하게 설계했습니다.
# .github/workflows/deploy.yml 중 일부 - name: Create Firebase Service Account Key JSON run: | echo '${{ secrets.FIREBASE_SERVICE_ACCOUNT }}' > src/main/resources/serviceAccountKey.json
결과적으로, 인프라의 서버(리눅스/도커) 환경에 수동으로 마스터 키 파일을 들고 다니며 관리해야 하는 고통을 완전히 제거했고, 소스코드의 완벽한 무결성과 보안을 동시에 이루어낸 우수 사례(Best Practice)로 자리 잡았습니다.
6. 트러블슈팅과 보안 강화 (코드 리뷰 피드백 반영)
FCM 푸시 알림 시스템을 1차적으로 도입한 후, 더 견고하고 안전한 서비스를 만들기 위해 코드 리뷰를 진행하고 다음과 같은 보안/UX 취약점을 수정했습니다.
- 기기 공유 시 개인정보 유출 방지 (Token Ownership)
동일한 스마트폰(기기)에서 기존 사용자가 로그아웃하고 다른 계정으로 로그인할 경우, 기기의 FCM 토큰은 동일하지만 소유자가 바뀌어야 합니다. 이를 방치하면 새 사용자에게 이전 사용자의 사적인 알림이 가는 치명적 결함이 생깁니다.
- 해결: 백엔드의 토큰 등록 API(
registerToken)에서 기존에 등록된 토큰이라면 현재 로그인한 유저와 소유자를 비교하고, 다를 경우changeUser메서드를 통해 토큰의 소유권을 강제로 새 사용자에게 이전하도록 보완했습니다.
- 해결: 백엔드의 토큰 등록 API(
- 로그아웃 연동 및 토큰 해제 검증
사용자가 프론트엔드에서 로그아웃 시, 단순히 로컬 인증 정보만 지워서는 안 됩니다. 브라우저가 여전히 알림을 수신할 수 있으므로 백엔드의
unregisterFcmTokenAPI를 호출해 서버 목록에서 즉시 지워야 합니다.- 해결: 로그아웃 흐름(
handleLogout)에 토큰 해제 API 호출을 추가했습니다. 또한 악의적인 사용자가 남의 토큰을 지우지 못하도록 백엔드에서@AuthenticationPrincipal을 이용해 본인 소유의 토큰인지 검증하는 로직을 추가했습니다.
- 해결: 로그아웃 흐름(
- 무분별한 권한 요청으로 인한 침해적 UX 개선
기존엔 로그인하자마자 브라우저 기본 알림 권한 팝업을 띄워 사용자에게 피로감을 주었습니다.
- 해결: 앞서 구현한 “마이페이지 > 앱 설정”에서 사용자가 푸시 알림 토글을 직접 켤 때만 명시적 맥락 하에 권한을 요청하도록 변경하여 UX의 질을 대폭 향상시켰습니다.