클라이언트: FCM 토큰을 생성하고 백엔드로 전송하는 모바일/웹 애플리케이션
백엔드 API: FCM 토큰을 업데이트하고 알림을 트리거하는 Spring Boot API
알림 서비스: 알림 유형별 비즈니스 로직을 처리하는 NotificationService
FCM 서비스: Firebase Admin SDK를 활용해 FCM 메시지를 전송하는 FCMService
알림 저장소: 알림 기록을 저장하고 조회하는 NotificationRepository
FCM은 Google Firebase에서 제공하는 무료 크로스 플랫폼 메시징 솔루션이다. 모바일 앱(Android, iOS)과 웹 앱에 메시지를 안정적으로 전송할 수 있다.
FCM의 기본 동작 방식은 다음과 같다:
@PostMapping("/fcm-token")
public ResponseEntity<Void> updateFCMToken(
@Parameter(description = "FCM 토큰 업데이트 요청 데이터", required = true)
@RequestBody FcmTokenRequestDTO request
) {
log.info("updateFCMToken request: {}", request);
memberService.updateFCMToken(request.getEmail(), request.getFcmToken());
return ResponseEntity.ok().build();
}
이 API는 클라이언트로부터 받은 FCM 토큰을 사용자 계정과 연결하는 역할을 한다. MemberService에서는 이 토큰을 데이터베이스에 저장한다.
@Slf4j
@Service
@Transactional
public class FCMService {
public void sendNotification(String targetEmail, String token, String title, String body, NotificationType type) {
log.info("sendNotification token: {}, title: {}, body: {}, type: {}", token, title, body, type);
Message message = Message.builder()
.putData("type", type.name())
.putData("targetEmail", targetEmail)
.putData("title", title)
.putData("body", body)
.putData("timestamp", LocalDateTime.now().toString())
.setToken(token)
.build();
try {
String response = FirebaseMessaging.getInstance().send(message);
log.info("Successfully sent notification: {}", response);
} catch (Exception e) {
log.error("Failed to send notification", e);
}
}
// 단체 알림 발송
public void sendMulticastNotification(List<String> tokenList, String title, String body, NotificationType type) {
MulticastMessage message = MulticastMessage.builder()
.setNotification(Notification.builder()
.setTitle(title)
.setBody(body)
.build())
.putData("type", type.name())
.putData("title", title)
.putData("body", body)
.putData("timestamp", LocalDateTime.now().toString())
.addAllTokens(tokenList)
.build();
try {
BatchResponse response = FirebaseMessaging.getInstance().sendMulticast(message);
log.info("Multicast 메시지 전송 완료: 성공={}, 실패={}",
response.getSuccessCount(), response.getFailureCount());
} catch (FirebaseMessagingException e) {
log.error("FCM 멀티캐스트 메시지 전송 실패", e);
}
}
}
이 서비스는 두 가지 주요 기능을 제공한다:
개별 알림 전송: 단일 사용자에게 메시지 전송
다중 알림 전송: 여러 사용자에게 동시에 메시지 전송
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class NotificationService {
private final NotificationRepository notificationRepository;
private final FCMService fcmService;
private final MemberRepository memberRepository;
// 개인 알림 전송
@Transactional
public void sendNotificationToMember(Member member, String title, String body, NotificationType type) {
log.info("sendNotificationToMember: member email {}", member.getEmail());
Notification notification = Notification.of(
type,
member,
title,
body,
member.getFcmToken()
);
notificationRepository.save(notification);
this.fcmNotificationSender(member, title, body, type);
}
// 대량 알림 전송
@Transactional
public void sendBulkNotification(List<Member> memberList, String title, String body, NotificationType type) {
log.info("sendBulkNotification 시작: {} 명의 회원에게 알림 전송", memberList.size());
for (Member member : memberList) {
if (member.getFcmToken() != null && !member.getFcmToken().isBlank()) {
// DB에 알림 저장
Notification notification = Notification.of(
type,
member,
title,
body,
member.getFcmToken()
);
notificationRepository.save(notification);
// FCM 알림 전송
fcmService.sendNotification(
member.getEmail(),
member.getFcmToken(),
title,
body,
type
);
log.info("알림 전송 완료: {}", member.getEmail());
}
}
log.info("sendBulkNotification 종료됨");
}
// FCM 알림 전송 헬퍼 메서드
private void fcmNotificationSender(Member member, String title, String body, NotificationType type) {
if (member.getFcmToken() != null) {
log.info("FCM 알림 전송됨! member.getFcmToken(): {}", member.getFcmToken());
fcmService.sendNotification(
member.getEmail(),
member.getFcmToken(),
title,
body,
type
);
}
}
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Notification extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
private NotificationType type;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
private String title;
private String body;
private String token;
private boolean isRead;
// 정적 팩토리 메서드
public static Notification of(NotificationType type, Member member, String title, String body, String token) {
Notification notification = new Notification();
notification.type = type;
notification.member = member;
notification.title = title;
notification.body = body;
notification.token = token;
notification.isRead = false;
return notification;
}
}
public void sendChattingMessage(String targetEmail, ChatMessageDTO chatMessageDTO, Member member) {
Member target = memberRepository.getWithRoles(targetEmail)
.orElseThrow(() -> new IllegalArgumentException("해당 이메일을 가진 회원이 없습니다."));
String title = member.getName() + "님 에게 새 메세지가 도착하였습니다!";
String body = chatMessageDTO.getMessage();
Notification notification = Notification.of(
NotificationType.NEW_MESSAGE,
target,
title,
body,
target.getFcmToken()
);
notificationRepository.save(notification);
this.fcmNotificationSender(target, title, body, NotificationType.NEW_MESSAGE);
}
// 역경매 관련 알림
public void sendOpenActionToTrainer(Long localId) {
// 해당 지역에서 활동하는 트레이너 회원들을 조회
List<Member> trainersInLocal = memberRepository.findMemberTrainerInLocal(localId);
String title = "새로운 역경매 신청이 들어왔습니다";
String body = "지금 입찰 신청을 해보세요";
sendBulkNotification(trainersInLocal, title, body, NotificationType.OPEN_AUCTION);
}
// 예약 관련 알림
java복사public void bookingDayNotification(String targetEmail) {
Member member = memberRepository.getWithRoles(targetEmail)
.orElseThrow(() -> new IllegalArgumentException("해당 이메일을 가진 회원이 없습니다."));
String title = "오늘은 예약 당일입니다";
String body = "늦지 않게 방문해주세요";
Notification notification = Notification.of(
NotificationType.BOOKING_DAY,
member,
title,
body,
member.getFcmToken()
);
notificationRepository.save(notification);
this.fcmNotificationSender(member, title, body, NotificationType.BOOKING_DAY);
}