Spring

[kerdy] Spring Boot + Firebase 를 통해서 알림 기능 만들어보기

자바생 2023. 7. 31. 17:18
728x90

글을 쓰게 된 이유

 

 

이번에 우아한테크코스에서 커디라는 팀으로 프로젝트를 시작하게 됐습니다.

 

개발 관련 컨퍼런스들을 조회할 수 있고, 다른 사람들에게 같이 가기 요청을 할 수 있습니다.

이때, '같이 가기 요청'에서 알림 기능이 필요했습니다.

 

 

알림 기능을 구현하면서 Firebase를 왜 사용하게 됐는지, 어떻게 사용하는지 등

 

팀원들에게 공유하기 위해서 해당 글을 작성했습니다. 

 

 

왜 firebase인가?

 

 

알림 기능을 구현하는 방법은 웹소켓, SSE, Polling 등이 있지만 왜 firebase를 사용했을까요?

 

 

현재 우테코의 데모데이 간격은 2주이기 때문에 새롭게 기능을 만들기에는 일정을 맞출 수 없다고 생각했습니다.

 

 

그래서 새로운 바퀴를 만들기보다는 존재하고 있는 바퀴를 아름답게 사용하기 위해서 firebase를 사용했습니다.

 

 

처음에 firebase를 사용하면서 제일 맘에 걸렸던 문제는 생각보다 시간이 오래 걸릴 수 있다는 점이었습니다.

 

 

안드로이드 분들이 했었을 때, 꽤 시간이 걸린다고 했었고, firebase 자체에서 테스트 해볼 때도 알림을 보내고 나서 도착하는데 시간이 꽤 걸렸었습니다.

 

하지만 지금은 시간보다는 기능 구현이 먼저이기 때문에 러닝커브가 낮은 firebase를 사용했습니다.

 

 

firebase를 사용할 때 필요한 선수지식

 

 

firebase를 사용하면서 가장 헷갈렸던 점은 두 개의 토큰이 존재하는데, 이 두 토큰의 사용처가 처음에는 헷갈렸습니다.

 

그래서 이 글을 보신 분들은 헷갈리지 않기 위해 먼저 정리해보도록 하겠습니다.

 

 

FCM token

 

FCM 토큰은 사용자의 기기를 구분하기 위한 토큰입니다.

 

생각해보면 우리가 알림을 보낼 때, 어떤 기기에 보내야할지 모르기 때문에 이를 구분하기 위해서 사용하는 토큰입니다.

 

 

해당 FCM 토큰은 안드로이드 분들이 FCM(Firebase Cloud Message 이하 firebase) 서버에서 얻어야하는 값입니다.

 

그리고 얻은 FCM 토큰 값을 서버에 저장 요청을 하여 서버에서는 알림을 보낼 때 누구에게 보내야할지 식별할 수 있습니다.

 

 

Access Token

 

공식 문서를 보면 FCM 서버에 요청을 보내기 위해서 필요한 3가지 전략들입니다.

 

 

 

이 중 액세스 토큰이 존재하고, 결국 액세스 토큰은 FCM 서버에 보내기 위해서 필요한 값으로 온전한 서버인지 확인하는 토큰이라고 생각하시면 됩니다.

 

 

 

이제 선수지식은 학습이 되었으니 전체적인 상황을 한 번 살펴보도록 하겠습니다.

 

 

과정

 

 

 

 

먼저 기기가 앱을 설치하고 사용했을 경우에 발생합니다.(주황색 선)

 

1. 기기가 등록될 경우 Client는 FCM으로부터 FCM 토큰 발급

2. Client -> Server로 FCM 토큰 저장 요청

 

이제 사용자가 알림 요청할 경우의 과정을 보겠습니다.(파랑색 선)

 

1. Client -> Server로 알림 생성 요청

2. Server는 FCM서버로부터 accessToken 발급

3. 발급받은 accessToken과 관련 키 값 및 보내려는 메시지의 데이터를 FCM 서버에 알람 요청하기

4. FCM 서버는 Server로부터 받은 값이 올바른지 확인 후 Client에게 푸시 알림 보내기

 

 

전체적인 프로세스를 알았으니 이제 어떻게 구현했는지 알아보도록 하겠습니다.

 

 

 

 

dependency

 

spring boot 2.7.13

java 11

 

implementation 'com.google.firebase:firebase-admin:9.2.0'

 

 

key 값 및 project id 설정

 

https://firework-ham.tistory.com/111

 

 

 

위 글을 보시면 firebase 의 키 값 및 기본 설정들을 할 수 있는 설명이 잘 되어있습니다.

 

저는 코드에서 어떻게 작성했는지 알아보도록 하겠습니다.

 

 

메세지 보내기

 

public void sendMessageTo(final Long receiverId, final Notification notification) {

  //알림 요청 받는 사람의 FCM Token이 존재하는지 확인
  final FcmToken fcmToken = fcmTokenRepository.findByMemberId(receiverId)
      .orElseThrow(() -> new NotificationException(NOT_FOUND_FCM_TOKEN));

  //메시지 만들기
  final String message = makeMessage(fcmToken.getToken(), notification);

  final HttpHeaders httpHeaders = new HttpHeaders();
  httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
  //OAuth 2.0 사용
  httpHeaders.add(HttpHeaders.AUTHORIZATION, PREFIX_ACCESS_TOKEN + getAccessToken());

  final HttpEntity<String> httpEntity = new HttpEntity<>(message, httpHeaders);

  final String fcmRequestUrl = PREFIX_FCM_REQUEST_URL + projectId + POSTFIX_FCM_REQUEST_URL;

  final ResponseEntity<String> exchange = restTemplate.exchange(
      fcmRequestUrl,
      HttpMethod.POST,
      httpEntity,
      String.class
  );

  if (exchange.getStatusCode().isError()) {
    log.error("firebase 접속 에러 = {}", exchange.getBody());
  }
}

 

makeMessage()

 

private String makeMessage(final String targetToken, final Notification notification) {

  final Long senderId = notification.getSenderId();
  final Member sender = memberRepository.findById(senderId)
      .orElseThrow(() -> new MemberException(MemberExceptionType.NOT_FOUND_MEMBER));

  final Data messageData = new Data(
      sender.getName(), senderId.toString(),
      notification.getReceiverId().toString(), notification.getMessage(),
      sender.getOpenProfileUrl()
  );

  final Message message = new Message(messageData, targetToken);

  final FcmMessage fcmMessage = new FcmMessage(DEFAULT_VALIDATE_ONLY, message);

  try {
    return objectMapper.writeValueAsString(fcmMessage);
  } catch (JsonProcessingException e) {
    log.error("메세지 보낼 때 JSON 변환 에러", e);
    throw new NotificationException(CONVERTING_JSON_ERROR);
  }
}

 

여기서 중요한 부분은 firebase에서 정해놓은 JSON 타입을 지켜야합니다.

 

공식문서

 

 

그래서 안드분과 협의한 후에 필요한 데이터는 "data"에 key:value 를 통해 전달하기로 했습니다.

 

 

@RequiredArgsConstructor
@Getter
public class FcmMessage {

  @JsonProperty("validate_only")
  private final boolean validateOnly;
  private final Message message;

  @RequiredArgsConstructor
  @Getter
  public static class Message {

    private final Data data;
    private final String token;
  }

  @RequiredArgsConstructor
  @Getter
  public static class Data {

    private final String senderName;
    private final String senderId;
    private final String receiverId;
    private final String message;
    private final String openProfileUrl;
  }
}

 

여기서도 주의할 점은 validate_only 값은 false로 해야하는데, 테스트용인지 아닌지 확인하는 변수입니다.

 

true로 하게 되면 실제 알림 요청을 보내지 않게 되므로 false로 설정해줘야합니다.

 

 

getAccessToken()

 

 

private String getAccessToken() {
  final String firebaseConfigPath = FIREBASE_KEY_PATH;

  try {
    final 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) {
    log.error("구글 토큰 요청 에러", e);
    throw new NotificationException(GOOGLE_REQUEST_TOKEN_ERROR);
  }
}

 

 

앞서 사전지식에서 말씀드린 FCM 서버에 요청하기 위해 필요한 AccessToken을 발급하는 과정입니다.

 

문서에서도 이용시간이 짧은 accessToken이라고 했으므로 요청마다 accessToken을 요청하는게 좋지 않을까 생각합니다.

 

 

결론

 

 

이번 글은 딥다이브나 새롭게 알게 된 점이 아닌 프로젝트 중에 팀원들에게 개발했던 기능에 대해 쉽게 설명하기 위해 작성된 글입니다.

 

그래서 최대한 이해하기 쉽게, 설정 부분보다는 어떤 과정으로 해당 알림 서비스를 구현했는지 이해하도록 작성했습니다,, 

 

이상

 

 

REFERENCES

 

https://firebase.google.com/docs/cloud-messaging/concept-options?hl=ko#notification-messages-with-optional-data-payload

https://firebase.google.com/docs/cloud-messaging/understand-delivery?hl=ko&platform=android

728x90