Skip to content

[Feat] FCM 푸시 알림 구현#12

Merged
howooyeon merged 11 commits into
developfrom
feat/#11-fcm
May 7, 2026
Merged

[Feat] FCM 푸시 알림 구현#12
howooyeon merged 11 commits into
developfrom
feat/#11-fcm

Conversation

@JangInho

@JangInho JangInho commented May 5, 2026

Copy link
Copy Markdown
Contributor

🧾 요약

  • FCM 푸시 알림 시스템을 도입
  • 백엔드 어디서든 notificationService.send() 한 줄로 알림을 발송할 수 있고, 한 유저의 여러 기기에 동시 발송됩니다.
  • 발송은 비동기로 처리되며, 추후 Redis/Kafka 등 큐 시스템으로 갈아끼울 수 있도록 NotificationDispatcher 인터페이스로 추상화했습니다.
  • 알림 이력은 DB에 저장되어 추후 알림함 화면에서 활용 가능합니다.

🔗 이슈

✨ 변경 내용

인프라

  • firebase-admin:9.8.0 의존성 추가
  • FirebaseConfig: 앱 시작 시 Firebase 초기화 (서비스 계정 JSON 로드)
  • AsyncConfig: 알림 전용 스레드풀 (notificationTaskExecutor) + @EnableAsync
  • FcmService: FirebaseMessaging.send() 래퍼 (notification + data 페이로드 지원)

도메인

  • NotificationType enum: 알림 종류별 title/body 템플릿 + 필수 파라미터 검증
  • 현재 COMMUNITY_COMMENT 테스트 이넘 넣어둠
  • FcmToken 엔티티: 한 유저당 여러 기기 토큰 저장 (token unique)
  • Notification 엔티티: 알림 이력 (extras는 jsonb)

발송 시스템

  • NotificationDispatcher 인터페이스 + NotificationDispatchCommand (record)
  • AsyncNotificationDispatcher: @Async 기반 구현체. 만료 토큰(UNREGISTERED/INVALID_ARGUMENT) 자동 삭제
  • NotificationService: 팀원 입구. 받는 사람/파라미터 검증 → 이력 저장 → Dispatcher 위임

API

  • POST /api/fcm/tokens — 토큰 등록 (JWT 인증, 같은 토큰이면 소유자/디바이스 갱신)
  • DELETE /api/fcm/tokens — 토큰 삭제 (로그아웃 시)
  • POST /api/notifications/test — 테스트 발송 (@Profile("local") 로 운영 자동 비활성화)

응답 코드

  • FCM_200_1 / FCM_200_2 / NOTIF_200_1 — 성공
  • USER_404_1 — 받는 사람 없음
  • NOTIF_400_1 — 필수 파라미터 누락

🧪 테스트 방법

  1. 테스트 로그인으로 JWT 발급
    POST /api/auth/test/login

  2. 클라이언트(웹/앱)에서 FCM 토큰 발급

  3. 토큰 등록
    POST /api/fcm/tokens

  4. 알림 발송 (본인에게 푸시 도착 확인)
    POST /api/notifications/test

📌 사용 예시

도메인 서비스에서 아래 함수로 전송 가능

notificationService.send(
 receiverId,
 NotificationType.COMMUNITY_COMMENT,
 Map.of(
     "actorName", commenter.getName(),
     "commentPreview", comment.getContent(),
     "postId", post.getId(),
     "commentId", comment.getId(),
     "actorId", commenter.getId()
 )
); 
  • 새 알림 타입 추가 시 NotificationType enum에 한 줄 추가만 하면 됩니다.

✅ 확인

  • 빌드 OK
  • 테스트 OK

📝 참고

  • 구현 방식, FCM 개념 등 세모산 노션 문서에 정리되어 있습니다.
  • 트래픽 증가 시 AsyncNotificationDispatcher를 Redis/Kafka 기반 구현체로 교체 가능 (NotificationService 코드는 변경 없음)
  • 유저-FCM토큰 연관관계는 @manytoone 대신 Long userId로 분리 (도메인 결합도 낮춤 -> 좋은지는 모르겠음)

@JangInho
JangInho requested review from howooyeon and pooreumjung May 5, 2026 08:39

@howooyeon howooyeon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨슴다 😁
머지 전에 미사용 import 이런거 함 싹 확인해주심 될듯요

Comment on lines +63 to +72
// 6. 비동기 발송 위임
dispatcher.dispatch(new NotificationDispatchCommand(
notification.getId(),
receiverId,
type,
title,
body,
params,
tokens
));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@transactional 메서드 안에서 dispatcher.dispatch()를 호출하는 구조인데 DB 커밋 전에 비동기 스레드가 FCM을 발송할 수 있습니다. 트랜잭션이 롤백되면 Notification 이력은 없는데 알림이 보내진 상태가 될거같습니다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TransactionSynchronization 사용해서 aftercommit 하거나 TransactionalEventListener 패턴은 어떠신가요~

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞네요! TransactionalEventLisener로 커밋 후에만 푸시 발송되도록 수정하겠슴다

Comment on lines +16 to +17
@Transactional
void deleteByToken(String token);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

derived delete 쿼리는 내부적으로 findByToken과 delete를 실행하고 Repo에서 직접 Transactional 선언할 시에 서비스 레이어의 트랜젝션과는 별도 동작할 수 있기 때문에 문제가 될 거 같습니다.

서비스 레이어에서 관리를 하거나 @Modifying @Query("DELETE FROM FcmToken f WHERE f.token = :token")로 벌크 삭제 어떤가요?!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다! 레포지토리의 @transactional 제거 + 트랜잭션은 서비스에서만 관리하게 하고,
@Modifyin @query로 변경해서 SELECT 없이 DELETE 되게 수정하겠슴다

@Configuration
public class FirebaseConfig {

private static final String SERVICE_ACCOUNT_PATH = "firebase/semosan-b2593-firebase-adminsdk-fbsvc-0d55d3b4e9.json";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 하드코딩 되어있는 거 외부화 어떨까용

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다! 우선 yaml로 뺴두는걸로 수정해두겠습니당

Comment on lines +34 to +39
@DeleteMapping("/tokens")
public ResponseEntity<ApiResponse<Void>> delete(
@Valid @RequestBody FcmTokenDeleteRequest request
) {
fcmTokenService.delete(request.token());
return ApiResponse.success(SuccessStatus.FCM_TOKEN_DELETE_SUCCESS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 인증없이 토큰 삭제할 수 있을 거 같아서
@AuthenticationPrinciapl 이거 추가해야 할 거 같아요

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞네요 수정하겠습니다!

Comment on lines +66 to +67
// 이거 삭제해야할 듯 -> fcm_tokens 테이블을 따로 뺌
// 충돌날까봐 아직 건드리진 않음

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

충돌 나면 고친다는 생각으로 수정 ㄱㄱ

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고고고

executor.setThreadNamePrefix("notif-async-");
// 큐 가득 차면 호출 스레드가 직접 실행 → 메시지 유실 방지
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스프링 빈이 알아서 초기화 해준다고는 해서 수정 안해도 되긴 하지만
이중 호출이래요

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러면 initialize는 뺴는게 좋겠네요 수정해두겠슴다

@pooreumjung pooreumjung left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니당~
호연 누나가 리뷰한것들만 고쳐서 머지하면 될 것 같아요

@howooyeon
howooyeon self-requested a review May 7, 2026 10:27
@howooyeon
howooyeon merged commit 4ecdd51 into develop May 7, 2026
@howooyeon howooyeon added the enhancement New feature or request label May 8, 2026
@howooyeon
howooyeon deleted the feat/#11-fcm branch May 17, 2026 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] FCM 푸시 알림 구현

3 participants