fcm은 firebase cloud message를 의미한다
클라이언트에게 푸시 알림을 보낼 수 있는 플랫폼인데,
간단하게 말하면 서버가 메세지를 생성해서 fcm 백엔드에 보내기 요청을 하면 클라이언트에게 알림이 간다
파이어베이스 콘솔로 접속해서 프로젝트를 만들어야 한다
또 해당 프로젝트에 FE 멤버도 초대해야함
프로젝트 설정 > 서비스 계정 > 서비스 계정 만들기
새 비공개 키 생성
스크린샷과 같이 .json 키 파일이 다운로드된다
프로젝트 설정 > 일반
프로젝트 ID를 기억하자
spring project에도 기본 세팅을 해줘야 한다
firebase DOCS 를 참고했다
위에서 다운로드 받은 키 파일을
src/main/resources
하위에 import 해주기
build.gradle
implementation 'com.google.firebase:firebase-admin:9.1.1'
application.yml
firebase:
project-id: fir-d61fb
key-path: fir-d61fb-firebase-adminsdk-58xi7-8db4ff70b2.json
FcmInitializer.java
서버가 firebase 서비스 계정임을 인증하는 작업이 필요하다
@Component
: 서버 띄울 때 기본적으로 필요한 작업임으로 빈으로 등록해줬고,
@PostConstruct
: WAS가 올라가면서 bean이 생성될 때 한번 초기화하도록 설정했다
@Slf4j
@Component
public class FcmInitializer {
@Value("${firebase.key-path}")
String fcmKeyPath;
@PostConstruct
public void getFcmCredential(){
try {
InputStream refreshToken = new ClassPathResource(fcmKeyPath).getInputStream();
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(refreshToken)).build();
FirebaseApp.initializeApp(options);
log.info("Fcm Setting Completed");
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
}
}
아마 서버가 started 되기 전에 아래와 같은 로그가 뜰 것이다
일단 서비스 dto 작성
클라이언트 측으로 보내고 싶은 정보를 다 담았다
FcmServiceDto.java
@Getter
@NoArgsConstructor
public class FcmServiceDto {
private String username;
private Long contentId;
private NotifyType type;
private String title;
private String content;
public static FcmServiceDto of(String username, Long contentId, NotifyType type, String title, String content){
FcmServiceDto dto = new FcmServiceDto();
dto.username = username;
dto.contentId = contentId;
dto.type = type;
dto.title = title;
dto.content = content;
return dto;
}
}
메세지 생성해서 fcm 서버로 보내는 부분이다
firebase-cloud-messaging DOCS 를 참고했다
클라이언트 측에서 푸시 알림 클릭시 액션을 구현할 수 있도록 관련 데이터와
연결된 동작을 담아 보낸다
연결된 동작은 ios는 category, aos는 click_action으로 지정할 수 있음
public void sendByToken(FcmServiceDto dto){
String token = getToken(dto.getUsername());
Message message = Message.builder()
.setToken(token)
.setNotification(
Notification.builder()
.setTitle(dto.getTitle())
.setBody(dto.getContent())
.build()
)
.setAndroidConfig(
AndroidConfig.builder()
.setNotification(
AndroidNotification.builder()
.setTitle(dto.getTitle())
.setBody(dto.getContent())
.setClickAction("push_click")
.build()
)
.build()
)
.setApnsConfig(
ApnsConfig.builder()
.setAps(Aps.builder()
.setCategory("push_click")
.build())
.build()
)
.putData("type",dto.getType().name())
.putData("contentId",dto.getContentId().toString())
.build();
try {
String response = FirebaseMessaging.getInstance().send(message);
log.info("FCMsend-"+response);
} catch (FirebaseMessagingException e) {
log.info("FCMexcept-"+ e.getMessage());
}
}
fcm 서버에 전달될 json은 결국 아래와 같을 것이다
POST https://fcm.googleapis.com/v1/projects/fir-d61fb/messages:send HTTP/1.1
Content-Type: application/json
Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA
{
"message":{
"token":"fpO9B9HkQs-5nUrb0t-2de:APA91bHSK_JgiUJDL4re0dSfEyVAgNwp7C6Cugc05vUoXxg3WiHv6fubih7_czuJggVq6Yl4DUwwQIlzvz2qkVHh7IGPwQnQprd6ZN-6MXmo1jfogRX1t4WQK5ABmT-XaKOkVw_Wa39q",
"data":{
"type":"COMMENT",
"contentId":"2"
},
"notification":{
"title":"타이틀",
"body":"콘텐츠"
},
"android":{
"notification":{
"title":"타이틀",
"body":"콘텐츠"
"click_action":"push_click"
}
},
"apns":{
"payload":{
"aps":{
"category" : "push_click"
}
}
}
}
getToken() 부분은 클라이언트가 등록한 fcmToken을 레파지토리에서 가져오는 부분이다
따라서 FcmService.java
는 아래와 같다
@Slf4j
@Service
public class FcmService {
private final TokenRepository tokenRepository;
@Autowired
public FcmService(TokenRepository tokenRepository) {
this.tokenRepository = tokenRepository;
}
public void sendByToken(FcmServiceDto dto){
String token = getToken(dto.getUsername());
Message message = Message.builder()
.setToken(token)
.setNotification(
Notification.builder()
.setTitle(dto.getTitle())
.setBody(dto.getContent())
.build()
)
.setAndroidConfig(
AndroidConfig.builder()
.setNotification(
AndroidNotification.builder()
.setTitle(dto.getTitle())
.setBody(dto.getContent())
.setClickAction("push_click")
.build()
)
.build()
)
.setApnsConfig(
ApnsConfig.builder()
.setAps(Aps.builder()
.setCategory("push_click")
.build())
.build()
)
.putData("type",dto.getType().name())
.putData("contentId",dto.getContentId().toString())
.build();
try {
String response = FirebaseMessaging.getInstance().send(message);
log.info("FCMsend-"+response);
} catch (FirebaseMessagingException e) {
log.info("FCMexcept-"+ e.getMessage());
}
}
private String getToken(String username) {
Token token = tokenRepository.findByUsername(username).orElse(null);
return token.getTokenValue();
}
}
api를 만들어서 테스트해봤다
이게 참 힘든게 ㅎ 서버 쪽에서 클라이언트 토큰 없이 테스트할 수 있는 방법이 없다
@GetMapping("/push")
public void fcmTest(@AuthenticationPrincipal UserDetailsImpl userDetails){
FcmServiceDto dto = FcmServiceDto.of(userDetails.getUsername(),1L, NotifyType.NOTICE,"제목", "콘텐츠");
fcmService.sendByToken(dto);
}
토큰값을 null로 두고 테스트한 경우
토큰이나 토픽이 하나라도 지정되어야 한다는 exception이 난다
java.lang.IllegalArgumentException: Exactly one of token, topic or condition must be specified
만료된 토큰으로 테스트 한 경우
Requested entity was not found.
가 뜬다
서비스에 등록하지 않은 클라이언트 토큰일 경우 나는 에러인데,
만료된 토큰이니까 당연하다
두 경우 다 서버 쪽에서는 fcm 백엔드에 메세지를 잘 전달한 것이므로 받으면 잘 구현되었다는 걸로 생각하자
이건 동일한 코드로 진행했을 때 AOS 측으로 전달된 fcm 메세지이다