안녕하세요
사실 귀찮은 건 아니구요
머리 한 움큼 쥐어뜯으면서 개발했습니다
렛츠고
문제는 그렇게 시작됐습니다
??? : 경제 리포트 서비스에 리포트만 있으면 뭐 앱에 들어오나요
바로 설득돼서 무슨 기능 넣을까 하다가 생각한 게
요약된 뉴스 푸시 알림 기능입니다
그래도 사실 막 어렵진 않았습니다. 이미 뉴스 크롤링과 요약 모듈이 존재했거덩요
문제는 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에서 예외 발생 시에도 안전하게 자원 반환
- 에러 스택 트레이스가 누락되지 않음
- 가독성 향상
등의 장점이 있습니다
어쨌든 그럼 이렇게 잘 전송됩니다
예~~~~~~~~~
네
마치겠습니다