새소식

ETC

FCM 푸시 알림 개발기...찮다

  • -

안녕하세요

 

사실 귀찮은 건 아니구요

 

머리 한 움큼 쥐어뜯으면서 개발했습니다

 

렛츠고

 

문제는 그렇게 시작됐습니다

 

??? : 경제 리포트 서비스에 리포트만 있으면 뭐 앱에 들어오나요

 

바로 설득돼서 무슨 기능 넣을까 하다가 생각한 게

요약된 뉴스 푸시 알림 기능입니다

 

 

그래도 사실 막 어렵진 않았습니다. 이미 뉴스 크롤링과 요약 모듈이 존재했거덩요

 

문제는 FCM <- 이놈을 어떻게 적용할지였습니다

 

가장 중요한건 기기마다 할당되는 FCM 토큰을 어떻게 관리할지였는데, 사실 뭐가 어렵냐고 생각할 수도 있습니다

 

그냥 유저 테이블에 필드 하나 추가하면 되는거 아니냐고요?

 

네 맞습니다. 근데 우리 서비스는 유저 테이블이 없어요

 

간단한 서비스 만들려고 유저 관리도 안하고 인증도 안하고

 

그래서 생각한 방법이 이겁니다

 

앱 최초 접속시 UUID로 ID를 부여하고 그걸 유저처럼 쓰자!

 

public class Member {

    @Id
    private String id;

    private String fcmToken;

 

그리고 유저들이 구독하는 주제를 만들고 다대다 관계로 연결해줬습니다

 

public class Watchlist {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String stockName;
    private String stockKeyword;
    
public class MemberWatchlist {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "watchlist_id")
    private Watchlist watchlist;

 

FCM 관리는 이제 해결했고 이제 알림을 전송해봅시다

 

 

https://firebase.google.com

 

Firebase | Google's Mobile and Web App Development Platform

개발자가 사용자가 좋아할 만한 앱과 게임을 빌드하도록 지원하는 Google의 모바일 및 웹 앱 개발 플랫폼인 Firebase에 대해 알아보세요.

firebase.google.com

 

사이트에 들어가서 프로젝트 등록을 해줍니다.

 

{
  "type": "",
  "project_id": "",
  "private_key_id": "",
  "private_key": "",
  "client_email": "",
  "client_id": "",
  "auth_uri": "",
  "token_uri": "",
  "auth_provider_x509_cert_url": "",
  "client_x509_cert_url": "",
  "universe_domain": ""
}

 

글고 resources/firebase/firebase_service_key.json에 저장해줬습니다

 

위 파일은 플젝 등록하면서 받을 수 있습니다

 

public void sendMessageTo(String fcmToken, String title, String body) {
    String message = makeMessage(fcmToken, title, body);

    OkHttpClient client = new OkHttpClient();

    RequestBody requestBody = RequestBody.create(message,
            MediaType.get(MEDIA_TYPE));
    Request request = makeRequest(requestBody);

    sendMessage(fcmToken, client, request);
}

 

전송도 사실 크게 어렵지 않습니다

 

1. 메시지 만들고

2. 요청 만들어서

3. 쏘기

 

이게 다 인데요 한번 세부적으로 보겠습니다

 

일단 메시지 만들고 objectmapper로 string으로 만들어줍니다

 

그리고 그 메시지를

 

private final String MEDIA_TYPE = "application/json; charset=utf-8";

public void sendMessageTo(String fcmToken, String title, String body) {
    String message = makeMessage(fcmToken, title, body);

    OkHttpClient client = new OkHttpClient();

    RequestBody requestBody = RequestBody.create(message,
            MediaType.get(MEDIA_TYPE));
    Request request = makeRequest(requestBody);

    sendMessage(fcmToken, client, request);
}

private Request makeRequest(RequestBody requestBody) {
    Request request = new Request.Builder()
            .url(API_URL)
            .post(requestBody)
            .addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken())
            .addHeader(HttpHeaders.CONTENT_TYPE, MEDIA_TYPE)
            .build();
    return request;
}

private String getAccessToken() {
    try {
        String firebaseConfigPath = "firebase/firebase_service_key.json";

        GoogleCredentials googleCredentials = GoogleCredentials
                .fromStream(new ClassPathResource(firebaseConfigPath).getInputStream())
                .createScoped(List.of("https://www.googleapis.com/auth/cloud-platform"));

        googleCredentials.refreshIfExpired();
        return googleCredentials.getAccessToken().getTokenValue();
    } catch (IOException e) {
        throw new UncheckedIOException("FCM AccessToken을 받아오지 못했습니다.", e);
    }
}

 

포함해서 요청을 만들어줍니다. 이때 access token과 content-type 등도 같이 지정해줍니다. 형식은 json + utf-8로 인코딩 해줬습니다

 

private void sendMessage(String fcmToken, OkHttpClient client, Request request) {
    try (Response response = client.newCall(request).execute()) {
        if (!response.isSuccessful()) {
            throw new IOException("FCM 요청 실패: " + response.code() + " - " + response.message());
        }
    } catch (IOException e) {
        log.warn("FCM Error Log | fcmToken: {} | message: {}", fcmToken, e.getMessage());
        memberRepository.deleteByFcmToken(fcmToken);
    }
}

 

마지막에 모든것을 묶어 메시지 전송을 해줬는데요

 

이때 처음에 따로 fcm 요청 후에 OkHttpClient가 HTTP 연결을 닫지 않고 방치하여 연결 누수가 발생하고 있었습니다

 

그래서 try-with-resources로 요청 처리 후 자동으로 연결이 닫히게 하였습니다

 

try-with-resources 적용 시

  • finally에서 예외 발생 시에도 안전하게 자원 반환
  • 에러 스택 트레이스가 누락되지 않음
  • 가독성 향상

등의 장점이 있습니다

 

어쨌든 그럼 이렇게 잘 전송됩니다

 

예~~~~~~~~~

마치겠습니다

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.