프로젝트 마지막 즈음에 push 알림 기능을 추가했다. 기획 의도와 잘 맞아서 좋았지만, 개발 기간이 짧아서 다양한 방법들 중에 fcm을 선택했다.
https://firebase.google.com/?hl=ko
Firebase | Google's Mobile and Web App Development Platform
개발자가 사용자가 좋아할 만한 앱과 게임을 빌드하도록 지원하는 Google의 모바일 및 웹 앱 개발 플랫폼인 Firebase에 대해 알아보세요.
firebase.google.com
위는 firebase 설정을 할 수 있는 곳인데, 설정 방법은 아래에 걸어놓은 포스팅을 참고해주자.
FCM이란?
Firebase Cloud Messaging은 Google의 Firebase 플랫폼에서 제공하는 클라우드 메시징 서비스로, 개발자는 이를 통해 간편히 안드로이드, iOS, 웹 애플리케이션에 푸시 알림을 보낼 수 있다.
장점
- Firebase 플랫폼과 통합되어 있어 설정과 사용이 간편하다. Firebase 콘솔에서 설정을 쉽게 관리할 수 있다.
- 안드로이드, iOS, 웹을 포함한 여러 플랫폼에서 사용 가능하다.
- Google의 인프라를 기반으로 하여, 대량의 메시지를 안정적으로 처리할 수 있다.
- FCM은 무료로 제공되며, 추가 비용 없이 사용할 수 있다.
- 주제 기반 메시징, 메시지 우선순위 설정, 다양한 메시지 타입 지원(알림 메시지, 데이터 메시지) 등 다양한 기능을 제공한다.
단점
- 메시지 전송 및 전달 상태에 대한 세부적인 제어가 제한적이다. ex) 재시도 로직에 대한 상세한 제어가 어렵다.
- FCM은 Google 서비스에 의존적이므로, Google 서비스가 제한된 지역에서는 사용이 어려울 수 있다.
- 사용자 기기에서 푸시 알림을 비활성화하면, 메시지를 전달할 수 없다.
- 고급 기능을 구현하기 위해 서버 측에서 추가적인 로직을 처리해야 할 수 있다.
공식문서
https://firebase.google.com/docs/cloud-messaging?hl=ko
Firebase 클라우드 메시징
Firebase 클라우드 메시징(FCM)은 무료로 메시지를 안정적으로 전송할 수 있는 크로스 플랫폼 메시징 솔루션입니다.
firebase.google.com
FCM 초기 세팅
https://persi0815.tistory.com/102
FCM 프로젝트 만들기 (Firebase 최신버전)
https://firebase.google.com/?hl=ko Firebase | Google's Mobile and Web App Development Platform개발자가 사용자가 좋아할 만한 앱과 게임을 빌드하도록 지원하는 Google의 모바일 및 웹 앱 개발 플랫폼인 Firebase에 대해
persi0815.tistory.com
혹은
스프링- 푸시 알림 구현예제(FCM 사용)
나는 푸시 알림 구현을 공부하기 전에는 왜 필요한지 이해하지 못했다.아니 내가 8시마다 똑같은 공지를 받는 것을 서버가 뭘 해줘야 해?그냥 클라이언트 분들이 알아서 8시 되면 공지를 띄우면
velog.io
을 따라가보자. 첫번째 포스팅은 내가 간략히 작성한 FCM 초기 설정 부분이다. (firebase 최신버전)
FCM 알림 서버 작동 방식
위에 명시해놓은 포스팅을 따라가다보면 앱등록을 하고, 프론트 json 파일, 백엔드 json 파일을 다운로드 받아 프로젝트 설정을 해놨을 것이다. 해당 부분이 바로 아래에 나오는 1번 부분이다.
1. 앱 등록 및 토큰 발급: 앱이 처음 실행될 때 FCM SDK는 디바이스를 FCM 서버에 등록하고 고유한 등록 토큰을 발급받는다. 이 토큰은 디바이스를 식별하는 데 사용된다.
* 프론트엔드에서 삽입했던 아래와 같은 google-services.json 파일(안드로이드) 때문에 위와 같은 과정이 가능하다.
2. 서버에 토큰 저장: 앱 서버는 디바이스 토큰을 수집하여 저장해야 한다. 이전에 만들었던 서비스의 경우, 한 사용자당 한 디바이스를 등록할 수 있었기에 회원가입 완료 시 fcm 토큰을 프론트엔드에게 받아 user DB에 저장하여 사용을 용이하게 했다. 해당 토큰을 통해 특정 디바이스에 푸시 알림을 보낼 수 있다.
한 사용자당 여러개의 디바이스 등록을 하게끔 하고 싶다면? 테이블 따로 만들어주면 된다.
그다음 로직도 한 계정당 몇개까지의 디바이스 토큰을 등록 가능하게 할 것인지, 개수가 초과되면 이전 디바이스 해제 로직은 어떻게 할지 결정해주면 된다.
(관련 포스팅 작성중..)
3. 서버에서 FCM 서버로 메시지 전송: 요청이 들어오면 앱 서버는 다음 과정을 거쳐 FCM 서버에 메시지를 전송한다.
첫째, 서버는 대상 디바이스의 토큰과 메시지 데이터를 포함한 메시지를 구성한다
둘째, 서버는 Firebase Admin SDK의 비공개 키(위에서 설정했던 백엔드 json 파일)를 참조하여 Bearer 토큰을 발급받는다.
셋째, 발급받은 Bearer 토큰을 사용하여 FCM 서버에 인증된 요청을 보낸다.
* Bearer 토큰은 Firebase 프로젝트의 서버 키(=비공개 키)로부터 생성된 JWT(Json Web Token)로, HTTP 헤더에 포함된다.
* 백엔드에서 삽입했던 아래와 같은 json 형식의 Firebase Admin SDK의 비공개 키가 여기서 사용되는 것이다.
* 강조하지만, 프론트의 json 파일과 백엔드의 json 파일은 다르다!
4. FCM 서버에서 디바이스로 메시지 전송: FCM 서버는 전달받은 메시지를 해당 디바이스로 전송한다. 디바이스가 온라인 상태라면 즉시 메시지가 전달되고, 오프라인 상태라면 디바이스가 온라인이 되었을 때 메시지가 전달된다.
5. 디바이스에서 메시지 수신: 디바이스는 FCM SDK를 통해 푸시 알림을 수신하고, 이를 사용자에게 표시한다. 메시지는 앱의 백그라운드 또는 포그라운드 상태에 따라 다르게 처리될 수 있다.
기본 작동 로직
기본 로직에서는 request body가 아래 형식으로 요청을 받았을 때 알람이 보내지도록 controller를 만들었는데, 다른 로직에 푸시 알람 기능을 적용할 때에는 따로 컨트롤러가 필요 없다. 이에 대한 예시는 아래에 적어두었다.
* 디바이스 토큰은 요청으로 일일이 받는 것이 아닌 user DB에 넣어두었고, title과 body도 미리 설정해두었다.
{
"deviceToken": "string",
"title": "string",
"body": "string"
}
Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/fcm")
public class FcmController {
private final FcmService fcmService;
// 1. client가 server로 알림 생성 요청
@PostMapping("/pushMessage")
public ApiResponse<String> pushMessage(@RequestBody FcmRequestDto requestDTO) throws IOException {
System.out.println(requestDTO.getDeviceToken() + " "
+requestDTO.getTitle() + " " + requestDTO.getBody());
fcmService.sendMessageTo(
requestDTO.getDeviceToken(),
requestDTO.getTitle(),
requestDTO.getBody());
return ApiResponse.onSuccess(SuccessCode.FCM_SEND_SUCCESS, "fcm alarm success");
}
}
FcmMessage
/*
메시지 구조
image: 선택사항
validateOnly: 메시지를 실제로 보내는 대신, 유효성 검사만 실행하려면 true로 설정 -> 보통 false로 고정함
*/
@Builder
@AllArgsConstructor
@Getter
public class FcmMessage {
private boolean validateOnly;
private Message message;
@Builder
@AllArgsConstructor
@Getter
public static class Message {
private Notification notification;
private String token;
}
@Builder
@AllArgsConstructor
@Getter
public static class Notification {
private String title;
private String body;
private String image;
}
}
validateOnly: 메시지를 검증 모드로만 보낼지 여부를 나타냄
- false: 실제로 메시지를 전송
- true: 전송하지 않고 유효성만 확인
message: FCM 메시지의 세부 내용을 포함
- token: 메시지를 보낼 대상 디바이스의 토큰
- notification: 알림 메시지의 제목, 본문, 이미지 URL과 같은 알림 관련 정보
- title: 알림의 제목
- body: 알림의 본문 내용
- image: 알림과 함께 표시할 이미지 URL
RequestDto
/*
프론트엔드가 디바이스 토큰과 title과 body를 넘겨 주는 걸로 되어있는데,
실제로는 미리 토큰을 db 컬럼에 저장해두었다.
*/
@Getter
@Setter
public class FcmRequestDto {
private String deviceToken;
private String title;
private String body;
}
Service - 가장 중요한 부분
1. makeMessage(): 서버는 대상 디바이스의 토큰과 메시지 데이터를 포함한 메시지를 구성한다
2. getAccessToken(): 서버는 Firebase Admin SDK의 비공개 키를 참조하여 Bearer 토큰을 발급받는다.
3. sendMessageTo(): 발급받은 Bearer 토큰을 사용하여 FCM 서버에 인증된 요청을 보낸다.
API_URL은 메세지 전송을 위해 요청하는 주소이다.
"https://fcm.googleapis.com/v1/projects/{프로젝트 ID}/messages:send"
@Slf4j
@Service
@RequiredArgsConstructor
public class FcmService {
private final String API_URL = "https://fcm.googleapis.com/v1/projects/reborn-bbf41/messages:send";
private final ObjectMapper objectMapper;
// 메시지를 구성하고 토큰을 받아서 FCM으로 메시지를 처리한다.
public void sendMessageTo(String targetToken, String title, String body) throws IOException {
String message = makeMessage(targetToken, title, body);
OkHttpClient client = new OkHttpClient();
RequestBody requestBody = RequestBody.create(message, // 만든 message body에 넣기
MediaType.get("application/json; charset=utf-8"));
Request request = new Request.Builder()
.url(API_URL)
.post(requestBody)
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken()) // header에 포함
.addHeader(HttpHeaders.CONTENT_TYPE, "application/json; UTF-8")
.build();
Response response = client.newCall(request).execute(); // 요청 보냄
System.out.println(response.body().string());
}
// FCM 전송 정보를 기반으로 메시지를 구성한다. (Object -> String)
private String makeMessage(String targetToken, String title, String body) throws com.fasterxml.jackson.core.JsonProcessingException { // JsonParseException, JsonProcessingException
FcmMessage fcmMessage = FcmMessage.builder()
.message(FcmMessage.Message.builder()
.token(targetToken)
.notification(FcmMessage.Notification.builder()
.title(title)
.body(body)
.image(null)
.build()
).build()).validateOnly(false).build();
return objectMapper.writeValueAsString(fcmMessage);
}
// Firebase Admin SDK의 비공개 키를 참조하여 Bearer 토큰을 발급 받는다.
private String getAccessToken() throws IOException {
final String firebaseConfigPath = "firebase/serviceAccountKey.json";
try {
final GoogleCredentials googleCredentials = GoogleCredentials
.fromStream(new ClassPathResource(firebaseConfigPath).getInputStream())
.createScoped(List.of("https://www.googleapis.com/auth/cloud-platform"));
googleCredentials.refreshIfExpired();
log.info("access token: {}",googleCredentials.getAccessToken());
return googleCredentials.getAccessToken().getTokenValue();
} catch (IOException e) {
throw new GeneralException(ErrorCode.GOOGLE_REQUEST_TOKEN_ERROR);
}
}
}
만들었던 Service의 함수들을 가지고 아래와 같이 다양한 곳에서 알람 기능을 사용할 수 있다.
1. 댓글 달릴 때마다 게시물 작성자에게 알람
@PostMapping("/{board-id}/create")
public ApiResponse<Long> create(
@PathVariable(name = "board-id") Long boardId,
@RequestBody CommentRequestDto.CommentDto commentDto,
@AuthenticationPrincipal CustomUserDetails customUserDetails
) throws IOException {
User user = userService.findUserByUserName(customUserDetails.getUsername());
Long commentId = commentService.createComment(boardId, commentDto, user);
Board board = boardService.findById(boardId);
String title = String.valueOf(board.getBoardType());
String body = "새로운 댓글: " + commentDto.getCommentContent();
fcmService.sendMessageTo(board.getUser().getDeviceToken(), title, body); // 이렇게
return ApiResponse.onSuccess(SuccessCode.COMMENT_CREATED,commentId);
}
2. 기념일 알람
@Scheduled(cron = "0 0 9 * * ?") // 매일 오전 9시에 실행
@Transactional
public void sendAnniversaryNotifications() {
LocalDate today = LocalDate.now();
List<Pet> pets = petRepository.findAll();
for (Pet pet : pets) {
if (today.equals(LocalDate.parse(pet.getAnniversary()))) {
User user = pet.getUser();
String token = user.getDeviceToken();
String title = "Anniversary";
String body = "오늘은 " + pet.getPetName() + "의 기일 입니다.";
try {
fcmService.sendMessageTo(token, title, body); // 이렇게
} catch (IOException e) {
log.error("Failed to send FCM notification", e);
}
}
}
}
FCM Token Redis에 넣어보기 (DB에 넣는 로직 포함)
https://persi0815.tistory.com/32
'Backend > Spring Boot' 카테고리의 다른 글
[Spring/Java] 민감한 개인정보 AES 알고리즘으로 암호화/복호화하기 (1) | 2024.11.14 |
---|---|
[Spring Boot] 로깅이란? 3가지 Logging 방식 소개 (0) | 2024.11.11 |
[Spring/Java] CORS란? 해결법은? (0) | 2024.07.07 |
[Spring/Java] 동시성 제어 통한 게시물 좋아요 및 댓글 수 관리 (비관적 락, 반정규화, 세마포어) (0) | 2024.07.07 |
[Sping/Java] JPQL vs QueryDSL (0) | 2024.07.07 |