티스토리 뷰

 

이번에 진행한 프로젝트에서 실시간 푸시 메세지가 꼭 필요한 기능이였습니다.
따라서 푸시메세지를 구현하기 위해 FCM을 사용했으며, 이를 사용할때 마주했던 문제들과 이를 해결했던 방법들을 공유해보고자 합니다.

우선, 실시간 푸시 메세지를 구현할 수 있는 방법은 다양한데, 대표적인 기술들을 알아 보도록 하겠습니다.

 

 

 

 

 

실시간 푸시 메세지 구현 기술 종류

  • WebSocket
    • 클라이언트와 서버간 지속적인 연결을 통해 양방향 통신을 할 수 있음.
    • 이 지속적인 연결을 통해 실시간 푸시메세지를 구현할 수 있음.
    • 하지만, 웹소켓 구현이 복잡하며 실시간 푸시메세지 구현을 위해 WebSocket을 쓰는게 알맞지 않음
  •  Message Queue(Pub/Sub)
    • Redis, Kafka 등의 메세지 큐를 통해 메세지 게시 및 구독을 할 수 있음.
    • 이 메세지를 기반으로 실시간 푸시메세지를 구현할 수 있음.
    • 서버를 따로 두어야 하며 이로인해 구현이 복잡해지므로, 실시간 푸시 메세지구현을 위해 Message Queue를 쓰는게 알맞지 않음
  • Firebase Cloud Messaging (FCM)
    • Google의 클라우드 메세징 서비스로, 쉽게 실시간 푸시메세지를 구현할 수 있음
    • FCM 토큰이라는 것으로 사용자를 식별할 수 있으며, 이 토큰만 가지고 있으면 손쉽게 푸시메세지를 발핼 할 수 있음.
    • FCM 서버는 Google이 관리하고 있으므로, 구현이 복잡하지않음.

 

이렇게 대표적으로 3가지의 구현 방식이 있었지만 FCM방식이 제일 알맞은 방법이라고 생각하여 FCM을 통해 푸시메세지를 구현하였습니다.

개발기간이 1달 정도밖에 안남았기 때문에 구현방식이 최대한 간단해야했으며, 높은 안정성을 위해 별도의 서버 구축없이 진행하는 방식이 맞다고 생각을 했기 때문에 FCM을 통해 구현을 하기로 하였습니다.

 

 

 

 

 

 

FCM 푸시메세지 발행 과정

 

우선 FCM구현 과정을 설명하기 이전에, FCM이 어떤원리로 실시간 푸시메세지를 발급할 수 있는지를 먼저 알아 보도록 하겠습니다.

 

FCM을 구현하기 위해선 프론트, 백엔드 둘 다 구현을 해야하는 부분이 있기 때문에 서로 소통을 통해 이 구조에 대해 명확히 하는게 중요합니다. 우선 서로 구현해야하는 부분은 다음과 같습니다.

  • 프론트
    • Service Worker의 MessageListener 구현
    • FCM 토큰 발급 및 저장 로직 구현
  • 백엔드
    • FCM 토큰 저장 API 구현
    • FCM 토큰 관리 로직 구현
    • 푸시메세지 발급 요청 구현

여기서 프론트와 백엔드는 서로 같은 Firebase의 정보를가지고 있어야하며 이 정보를 기반으로 서로 FCM 토큰을 발급 받고 이 FCM 토큰을 기반으로 실시간 푸시메세지를 전송 및 수신 하여야합니다.

이때, Service Worker는 푸시메세지를 수신하기 위해선 무조건 필요합니다. ServiceWorker가 백그라운드로 켜져있어야 사용자가 해당 앱을 사용하지 않을때에도 실시간으로 알림을 받을 수 있기 때문입니다.

구체적인 전체 과정은 다음과 같습니다.

  1. FCM 토큰 요청 및 획득
    - 프론트에서 Firebase 측에 FCM 토큰을 발급 요청을 합니다. 이때 발급요청이 성공적으로 이루어지면, FCM 토큰을 획득할 수 있고, 이 토큰이 해당 사용자의 고유한 FCM 토큰입니다.

  2. 서버에 FCM 토큰 저장
    - 발급받은 FCM 토큰으로 백엔드 서버에 FCM 토큰을 저장해야 합니다. 이때, 백엔드 측에선 FCM 토큰을 저장하는 API를 개발하여야하며, 이는 해당 프로젝트의 비즈니스 요구사항에 따라 API 구현 방식이 달라질 수 있습니다.
    프론트에서는 발급받은 FCM 토큰을 해당 API를 이용해 백엔드 서버에 저장하여야합니다.

  3. FCM 토큰으로 메세지 전송요청
    - 백엔드에선 해당 FCM 토큰을 저장하고 있다가 푸시메세지를 발급해야할때, 해당 FCM 토큰으로 Firebase에 푸시메세지 발급 요청을 합니다. 이때, 해당 FCM 토큰이 정상적인 토큰일 시에 메세지 발급이 요청되며, 그외의 경우는 ErrorCode를 반환합니다.

  4. 메세지 전송
    - 백엔드에서 푸시 메세지 발급이 정상적으로 완료 되면, Firebase에서 FCM 토큰의 정보를 바탕으로 해당 사용자에게 푸시 메세지를 발급합니다. 이때 해당 사용자의 Service Worker에게 전송하게 됩니다.
    이때, ServiceWorker가 정상적으로 백그라운드에서 실행되고 있다면, 푸시메세지를 수신하게 됩니다.

  5. 리스너를 통해 메세지 수신
    - Service Worker를 통해 정상적으로 푸시 메세지를 발급받은 후, 사용자에게 정상적으로 정보를 출력하게 됩니다.

 

 

 

구글 파이어베이스 구현

 

 

우선 먼저 아래링크의 구글 파이어베이스에서 직접 FCM 등록을 해야합니다.

https://firebase.google.com/?hl=ko

 

Firebase | Google's Mobile and Web App Development Platform

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

firebase.google.com

 

 

여기서 FCM 등록을 하게되면, '서비스 계정'이라는게 하나 나오게 되며, 이것을 이용해 백엔드 서버에서 FCM 환경설정을 할 수 있고, 이를 이용해 FCM 푸시메세지를 전송할 수 있기 때문에 이 서비스 계정을 JSON파일로 저장한 이후에 백엔드 서버에 등록을 해주시면 됩니다.

저희의 경우 스프링서버이므로, 아래와 같이 resources 디렉토리 내부에 서비스계정 JSON 정보를 등록하였습니다.

이때 무조건 resources 파일 내에 json 파일을 두어야 서버에서 인식을 할 수 있다고 정보를 본 것 같습니다.
(이건 확실치 않습니다...ㅠㅠ)

 

 

이렇게 json파일을 두고, 서버에서 이 정보를 가지고 FCM 환경설정을 할 것입니다.

 

 

저희의 경우는 global이라는 패키지로 전체 서버 환경을 설정하고 있기때문에 global 패키지 내에 firebase를 두어서 파이어베이스 기능을 구현하였습니다. 물론, FirebaseMessagingService와 FirebaseNotificationService는 따로 도메인으로 빼도 되었지만, 개발 기간이 매우 짧아 이것에 대한 고민은 충분히 하지 못하였으며, 따로 도메인으로 빼기엔 한곳에서 Firebase를 관리하는게 좀 더 편리하다는 점 때문에 이렇게 global에 한곳에 두었습니다.

 

이제 아래는 FirebaseInitializer 코드입니다.

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.annotation.PostConstruct;

@Configuration
public class FirebaseInitializer {

    @Value("${firebase.service-account-file}")
    private String serviceAccountFile;

    @Value("${firebase.database-url}")
    private String databaseUrl;

    @PostConstruct
    public void initialize() {
        try {
            InputStream serviceAccount =
                    new ClassPathResource(serviceAccountFile).getInputStream();

            FirebaseOptions options = new FirebaseOptions.Builder()
                    .setCredentials(GoogleCredentials.fromStream(serviceAccount))
                    .setDatabaseUrl(databaseUrl)
                    .build();

            if (FirebaseApp.getApps().isEmpty()) {
                System.out.println("파이어베이스 초기화");
                FirebaseApp.initializeApp(options);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

생각보다 이 파이어베이스 정보를 기반으로 초기화 하는 코드에 대한 예시가 구글링에 없어서 좀 시간이 걸렸던 기억이 있습니다.
제가 못찾았을 수도 있지만, 구글링으로 찾은 코드들은 다 제 스프링에서 오류가 났었습니다. 또한, 이 코드들이 컴파일 오류가 나는것이 아닌, FCM 실행시 런타임 오류가 났기 때문에 디버깅과정에서 오랜 시간이 걸렸습니다...ㅋㅋㅋ

 

아무튼, 이렇게 스프링에서 파이어베이스를 초기화 하였으며, service-account-file엔 해당 json 파일의 풀네임을 입력하였으며, database-url은 Google 에서 얻은 파이어베이스의 Database URL의 정보를 입력하였습니다.

 

 

 

이제 이 다음으로,이 정보를 기반으로 FCM토큰을 이용해 푸시메세지를 발행하는 코드를 구현해야합니다.
저 같은 경우는 스프링에서 Service라는 것을 이용해 구현하였습니다.

 

@Service
@RequiredArgsConstructor
@Slf4j
public class FirebaseMessagingService {
    private final FcmTokenRepository fcmTokenRepository;

    /**
     * Firebase를 통해 푸시 알림을 전송합니다.
     *
     * @param token   대상 디바이스의 FCM 토큰
     * @param title   알림 제목
     * @param body    알림 내용
     * @return        전송된 메시지의 ID
     * @throws FirebaseMessagingException FCM 전송 중 오류 발생 시
     */
    public String sendNotification(String token, String title, String body) {
        Notification notification = Notification.builder()
                .setTitle(title)
                .setBody(body)
                .build();

        Message message = Message.builder()
                .setToken(token)
                .setNotification(notification)
                .build();

        try {
            return FirebaseMessaging.getInstance().send(message);
        } catch (FirebaseMessagingException e) {
            if (e.getMessagingErrorCode().equals(MessagingErrorCode.INVALID_ARGUMENT)) {
                // 토큰이 유효하지 않은 경우, 오류 코드를 반환
                return e.getMessagingErrorCode().toString();
            } else if (e.getMessagingErrorCode().equals(MessagingErrorCode.UNREGISTERED)) {
                // 재발급된 이전 토큰인 경우, 오류 코드를 반환
                return e.getMessagingErrorCode().toString();
            }
            else { // 그 외, 오류는 런타임 예외로 처리
                throw new RuntimeException(e);
            }
        }
    }
}

 

이 코드를 보시면 FirebaseMessaging.getInstance().send(message); 라는것을 이용해 푸시메세지를 보내는 것을 확인하실 수 있습니다.근데 지금 보면 try-catch문으로 에러처리하고 있는 부분이 있습니다. 이부분은 아래에서 자세히 기술하도록 하겠습니다.

 

 

 

 

 

 

 

 

FCM 토큰 관리방법

 

 

지금 까지 코드량을 보면 거의 50줄도 안되기 때문에 굉장히 구현이 쉽습니다. 저 또한, 이 기술을 구현하는데 그렇게 많은 시간이 들지 않았으며, 실제 동작테스트도 그렇게 많은 시간이 걸리지 않았습니다.

 

다만,실제 동작을 테스트하는 과정에서 비즈니스 로직 상의 버그를 만났으며 이를 해결하기위해 가설을 세우고 하나씩 해결해 나가는 과정을 거쳤습니다.

 

 

우선 저희가 생각한 사용자가 FCM 토큰을 사용할 때 일어날 수 있는 비즈니스 로직 오류는 다음과 같습니다.

 

한 사용자가 한 계정으로 여러 기기에서 동시에 사용

 

 이 경우는 한사용자가 폰, 태블릿, 노트북 등의 기기에서 한 계정을 동시에 사용하는 경우입니다. 우리 주변에서 예를들면 카톡을 여러 기기에서 동시에 사용하고 있는 경우입니다. 
저희는 한 사용자에게 한 개의 FCM 토큰을 가질 수 있게 하였기때문에 새로운 기기에서 로그인을 하게되면 해당 기기에서만 푸시메세지가 발행이 되고, 그 외의 기기에서는 푸시메세지가 전송이 안되는 문제가 발생했습니다.
따라서, 이를 해결하기 위해서는 한 사용자 계정에 각각의 기기마다 다르게 FCM 토큰이 저장이 되어야 했으며, 이걸 위해선 ERD 자체를 수정해 FCM 토큰이라는 테이블을 따로 분리하여 한사용자마다 여러개의 FCM 토큰을 가질 수 있게하였습니다.


따라서, 이를 이용해 한 사용자가 여러기기에서 동시에 사용을 함에도 모든 기기에서 푸시메세지를 받을 수 있도록 구현 하였습니다.

 

 

 

 한 기기에서 여러 계정 사용

 

 이 경우는 전자의 상황과 반대로 한 기기에서 여러 계정을 사용하는 경우입니다. 우리 주변에서 예를 들면 내 계정을 사용하다가 갑자기 친구계정으로 로그인을 하거나, 서브 계정으로 로그인하는 경우입니다.
이 경우엔, 해당 기기의 브라우저안에 FCM토큰의 정보가 남아 있을 수 있으며 이 정보가 남아 있을 경우, 해당 기기에 새로 로그인한 사용자의 푸시메세지 뿐만아니라, 전에 로그인된 사용자의 푸시메세지 까지 발행될 수 있기때문에 비즈니스상 오류가 발생할 수 있었습니다.
따라서, 이를 해결하기위해 로그아웃시에 FCM 토큰을 추가로 서버로 넘기게하여, 로그아웃시 해당 계정의 FCM 토큰을 삭제하는 로직을 추가하였습니다.
이를 통해, 한 기기안에서 중복으로 푸시메세지가 발행되는 문제를 해결하였으며,정상적으로 현재 로그인된 사용자의 FCM 푸시메세지만 받을 수 있게 하였습니다.

 

 

 

 

한 사용자가 오랫동안 사용하지않음

 

 이 경우는 비즈니스상의 심각한 오류는 아니지만, FCM 토큰이 테이블에 쌓이게 되면 이후에 FCM 토큰 조회시에 성능이 저하 될 것이며, 실시간 푸시메세지 발행에 있어 성능 장애가 생길 수 있을 것이라 판단하였습니다.

즉, 오랫동안 사용하지 않는 사람들의 FCM 토큰을 계속 저장하게 되면 성능 저하가 발생하기 때문에 이 사용자들의 FCM 토큰은 주기적으로 체크 후 삭제하는게 맞다는 판단을 하였습니다.
따라서, FCM 토큰에 신선도 라는 로직을 추가하였고, 최종 사용 시각이 2달이 지난 사용자는 자동으로 삭제할 수 있게 스케줄러를 설정하여 구현하였습니다.

 

 

이외의 예기치 않은 FCM 토큰 오류

 

이외에 예기치 않은 FCM 토큰 오류가 있었고 내용은 다음과 같습니다.

  • 사용중에 예기치않게 프론트에서 FCM 토큰이 재발급되는 경우
  • 의도치않게 서버에 이상한 FCM 토큰이 저장되는 경우

테스트 도중 이렇게 의도치않게 FCM 토큰이 변경 및 수정되는 경우가 있었으며 이렇게 잘못된 FCM 토큰이 서버에[ 저장이 될 경우, 푸시메세지 발행시에 FirebaseMessagingException이 발생하였습니다. 이것을 해결하기 위해선 직접 Try-Catch를 통해 Exception처리를 하여 해당 토큰을 삭제하도록 하였습니다.

이때 FirebaseMessagingException의 종류는 다음과 같습니다.

 

많은 Exception이 있지만 여기서 저희가 주로 판단해야하는 상황은 INVALID_ARGUMENT, UNREGISTERED 이렇게 두가지의 경우가 있었습니다.

  • INVALID_ARGUMENT
    - 아예 유효하지 않은 토큰의 경우일때 오류 코드를 반환하는 것으로, 아예 잘못된 토큰일 경우 발생합니다.
    FCM 메세지를 송신할 때, 이 메세지가 떴다면 해당 토큰을 그냥 DB에서 삭제하는 방식으로 구현을 하였습니다.

  • UNREGISTERED
    - 이전엔 유효하였으나, 재발급되어서 새로운 토큰으로 바뀐 경우입니다. 따라서 아예 잘못된 토큰은 아니고, 토큰이 만료된 경우입니다.
    FCM 메세지를 송신할 때, 이 메세지가 떴다면 해당 토큰을 그냥 DB에서 삭제하는 방식으로 구현을 하였습니다. \

 

 

이렇게 저희의 프로젝트에서 FCM토큰을 구현하고, 저희의 비즈니스 로직에 맞게 변환시켜 실시간 푸시메세지를 구현하였습니다.

이번에 FCM을 이용해보면서 푸시메세지 구현이 굉장히 손쉽게 구현될 수 있다는 것을 알았으나, 저희의 프로젝트는 대량의 푸시메세지가 필요하지않았기에 FCM으로 구현이 가능했으며, FCM의 경우 한번에 발급할 수 있는 메세지의 최대치가 정해져있어 동시에 10000건 등의 대량 메세지 발급에는 부적합하다는 결론이 나왔습니다.

따라서, 저희의 프로젝트가 더 업데이트 되거나, 사용자가 많아질 경우 FCM이 아닌 MessageQueue를 도입하는 방식 등을 고민해봐야할 것 같습니다 ㅎㅎ
(이건 정말 나중에 하는걸로....ㅎㅎ) 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함