FCM

FCM은 플랫폼의 제약 없이 메시지를 전송할 수 있는 솔루션이다. 사용 방법에 따라서 특정 디바이스 또는 다수의 디바이스(한 번 요청에 최대 500건)에 앱 알림, 마케팅 메시지 등을 보낼 수 있다. 이 솔루션을 통해서 안드로이드나 IOS등의 다양한 플랫폼을 고려하지 않고 동일하게 메시지 처리를 할 수 있다는 장점이 있다. FCM을 왜 사용하는 지 다룬 블로그 포스팅이 있어서 공유한다. ([Firebase]FCM에 대해서 알아보자)

요청 흐름은 다음과 같다.

FCM 요청 흐름

프로젝트에 FCM 구현하기

이미 FCM 라이브러리를 구글에서 만들어서 제공하기 때문에 구현 자체는 어렵지 않았다.

사전 설정

FCM 비공개 키 파일 저장

FCM을 구현하기 위해서는 파이어베이스 콘솔로 가서 프로젝트를 만들고 프로젝트 설정의 서비스 계정에서 새 비공개 키 생성으로 json 파일 하나를 받아서 작업중인 프로젝트에 넣어줘야 한다.

FCM 설정

프로젝트에 파일 설정

나는 resources/fcm에 파일을 넣었다.

의존성 추가

// fcm
implementation group: 'com.google.firebase', name: 'firebase-admin', version: '9.4.2'

커스텀 변수 추가

설정 파일을 담고 있는 위치를 설정 파일에 저장해두었다. 사실 FCM 관련 코드 통틀어서 하나만 사용하기 때문에 해당 클래스에 작성해도 상관 없을 것 같긴하다.

fcm:
  config-path: {CONFIG_PATH}

FirebaseApp

메시지 처리를 할 때 FirebaseApp 이라는 객체를 사용한다. 따라서 이를 스프링 빈으로 만들어 관리하도록 하였다.

@Configuration
public class FcmConfig {
    @Value("${fcm.config-path}")
    private String fcmConfigPath;

    @Bean
    public FirebaseApp firebaseApp() throws IOException {
        GoogleCredentials credentials = GoogleCredentials
                .fromStream(new ClassPathResource(fcmConfigPath).getInputStream());
        FirebaseOptions options = FirebaseOptions.builder().setCredentials(credentials).build();

        return FirebaseApp.initializeApp(options);
    }
}

코드 작성

이제 코드를 작성해보자. 사실 구글에서 라이브러리를 잘 만들어줘서 작성할 것이 많이 없다.

컨트롤러 설계

클라이언트에서 FCM 메시지를 보내달라고 요청할 앤드포인트를 설계한다.

@RestController
@RequestMapping("/api/fcm")
@RequiredArgsConstructor
public class FcmController {
    private final FcmService fcmService;

    @PostMapping("/send")
    public ResponseEntity<JSONResponse<Object>> pushMessage(@RequestBody FcmSendRequest request) {
        fcmService.sendMessage(request.toDto());
        return ResponseEntity.ok(JSONResponse.of(SuccessCode.REQUEST_SUCCESS, null));
    }
}

public record FcmSendRequest(
        String token,
        String title,
        String body
) {
    public FcmInfoDto toDto() {
        return new FcmInfoDto(token, title, body);
    }
}

서비스 설계

@Slf4j
@Service
public class FcmService {
    public void sendMessage(FcmInfoDto info) {
        try {
            Notification notification = Notification.builder()
                                            .setTitle(info.title())
                                            .setBody(info.body())
                                            .build();

            Message message = Message.builder()
                    .setToken(info.token())
                    .setNotification(notification)
                    .putData("title", "Fore ground Message")
                    .putData("body", "success")
                    .build();

            FirebaseMessaging.getInstance().send(message);

        } catch (FirebaseMessagingException e) {
            e.printStackTrace();
            throw new GoogleServerException(ErrorCode.GOOGLE_SERVER_ERROR);
        }
    }
}

public record FcmInfoDto(
        String token,
        String title,
        String body
) {
}

서비스에서는 클라이언트에서 요청한 정보를 기반으로 NotificationMessage객체를 정의하고, 이를 FirebaseMessaging객체를 통해서 보내는 것이 전부다. 라이브러리를 몰랐을 때는 요청 형식을 보고 WebClient 객체로 요청을 보내 보기도 했는데 잘 안됐다. 아무튼 여기서 중요한 것은 Notification은 시스템 UI에 표시되는 역할이고 putData()로 정의하는 메시지는 앱이 직접 처리하도록 한다는 것이다.

테스트

테스트용 클라이언트는 chatGPT의 힘을 빌려서 만들었다. 그냥 이런게 있다 정도로 보자.

요청 형식은 다음과 같다.

POST /api/fcm/send HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Content-Length: 85

{
    "token": "exampleClientToken12345",
    "title": "알림 제목 예시",
    "body": "알림 내용 예시"
}

디바이스 토큰을 발급받고 요청해보면 잘 나오는 것을 볼 수 있다.

토큰 발급

메시지 전송 결과


간단하게 FCM을 구현할 수 있었다. 하지만 웹 환경에서는 캐시 초기화나 브라우저 변경(크롬 → 파이어폭스) 시 디바이스 토큰을 재발급받아야 하는 불편함이 있다. 그래서 유튜브의 알림처럼 장기적인 브라우저 이용이 전제된 경우에 주로 활용될 것으로 보인다.

반면 앱 환경에서는 삭제 후 재설치할 때 디바이스 토큰을 다시 보내주면 되기 때문에 토큰 관리가 훨씬 안정적이다. 이런 이유로 FCM은 특히 앱에서 더 유용하게 사용될 것으로 판단된다.

현재는 컨트롤러와 서비스만 구현했지만, 향후 리포지토리를 추가하여 디바이스 토큰을 사용자별로 저장하고 관리하면 다수의 사용자에게 알림을 효율적으로 전송할 수 있을 것이다.

전체 코드 참고

참고자료

Firebase Cloud Messaging

React Native Push Notifications

[Firebase]FCM에 대해서 알아보자

카테고리:

업데이트:

댓글남기기