본문 바로가기
etc.

Spring Boot 프로젝트에서 FCM을 이용한 웹 푸시 구현하기

by 케로베로 2021. 5. 4.

 현재 진행중인 프로젝트 Shoe-Auction에 웹 푸시를 이용하여 본인이 등록한 거래가 성사되었을 때 알림을 받을 수 있는 기능을 구현하였다. 메세지 전송 플랫폼으로는 FCM을 사용하였다. FCM에 대한 내용과 구현 예제들은 공식 문서에 꽤 친절하게 설명되어 있기 때문에 간단한 설명과 구현 과정만 가볍게 포스팅 해보고자 한다. 

 

 

🤔 FCM은 무엇이고 왜 사용할까?

 FCM(Firebase Cloud Messaging)이란 메세지를 안정적으로 클라이언트 인스턴스에게 전송할 수 있는 교차 플랫폼 메시징 솔루션이다. 이러한 서비스는 현재 무료로 제공되고 있기 때문에 부담없이 적용해보기 좋다.

 

 전체적인 흐름을 보면 애플리케이션 서버(또는 GUI)에서 전송된 메세지가 클라이언트에게 직접 전송되는 것이 아니라 FCM backend를 거쳐서 클라이언트에게 전송되는 방식이다. 그렇다면 서버와 클라이언트 사이에 FCM backend가 끼어듬으로써 얻는 이점에는 무엇이 있을까?

 

 우선 FCM이 교차 플랫폼 메시징 서비스이기 때문에 메세징을 클라이언트의 플랫폼(Web, IOS, Android) 환경 별로 개발할 필요가 없어지므로 플랫폼에 종속되지 않고 메세지를 전송할 수 있어 구현의 복잡성을 낮춰준다는 장점이 있다.

 

 또한 만약 클라우드 메세징 서버가 없이 애플리케이션 서버 -> 클라이언트로 메세지를 전송하는 구현에서 클라이언트가 계속해서 서버에 접속해 있어야 하기 때문에 발생하는 디바이스의 전력 사용과 네트워크 효율 문제를 어느정도 해결해 줄 수 있다는 점 또한 장점이다.

 

❗ FCM의 구성 요소와 작동 원리

 우선 FCM의 작동에는 두 가지 주요 구성요소가 필요하다.

  1. Firebase용 Cloud Functions 또는 앱 서버와 같이 메시지를 작성, 타겟팅, 전송할 수 있는 신뢰할 수 있는 환경
  2. 해당 플랫폼별 전송 서비스를 통해 메시지를 수신하는 iOS, Android 또는 웹(자바스크립트) 클라이언트 앱

 1번 요소는 메세지를 작성하고 FCM backend에 전송할 애플리케이션 서버를 의미하며, 2번 요소는 FCM backend로부터 메세지를 수신할 클라이언트의 앱을 의미한다.

 

 동작원리는 크게 복잡하지 않다. 우선 클라이언트가 FCM 서버에서 디바이스 별 고유하게 발급되는 FCM 토큰을 발급받는다. 서버는 클라이언트로부터 해당 토큰을 전달받고 저장한다. 이후 특정 상황에 메세지에 토큰을 담아서 FCM backend에 전송하면 메세지를 수신한 FCM backend는 토큰을 발급받은 클라이언트 앱에 메세지를 전송하는 것이다.

 

 본 프로젝트에는 아직 api 서버만 구현되어있기 때문에 2번의 웹 푸시 메세지를 받을 클라이언트 앱은 간단한 테스트 페이지로 작성하였으며, 구현 예제는 공식문서에 쉽게 작성되어 있다.

 

💻FCM을 Spring boot 프로젝트에 적용하기

 이제 본격적으로 애플리케이션 서버에서 FCM을 적용하기 위한 과정을 적어보도록 하겠다.

 

 우선 FCM을 사용할 프로젝트에 firebase-admin 의존성을 추가해준다.

 

build.gradle

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

 

 그 후 Firebase 콘솔에 접속해서 로그인 한 후 프로젝트를 생성하고, 프로젝트 설정 -> 서비스 계정 항목에서 새 비공개 키 생성으로 비공개 키를 생성한다. 생선된 admin sdk는 json 파일로 생성되며, 생성된 파일을 프로젝트의 resource 디렉토리로 이동한다.

 

 

 이 후 어플리케이션이 시작될 때 비공개 키 파일의 인증정보를 이용해 FirebaseApp을 초기화하도록 FCMInitializer를

구현한다.

 

common/firebase/FCMInitializer.java

@Slf4j
@Component
public class FCMInitializer {

    @Value("${fcm.certification}")
    private String googleApplicationCredentials;

    @PostConstruct
    public void initialize() throws IOException {
        ClassPathResource resource = new ClassPathResource(googleApplicationCredentials);

        try (InputStream is = resource.getInputStream()) {
            FirebaseOptions options = FirebaseOptions.builder()
                .setCredentials(GoogleCredentials.fromStream(is))
                .build();

            if (FirebaseApp.getApps().isEmpty()) {
                FirebaseApp.initializeApp(options);
                log.info("FirebaseApp initialization complete");
            }
        }
    }
}

 

 서버에서는 유저 로그인 시 FCM 토큰을 받아서 데이터베이스에 저장하도록 구현하였으며, 로그아웃 시에는 데이터베이스 내의 토큰을 제거하였다. 만약 로그아웃 시 토큰을 데이터베이스에 남겨둔다면 이미 로그아웃된 디바이스에서 유저 관련 메세지를 수신할 수도 있기 때문이다.

 

dao/FCMTokenDao.java

@RequiredArgsConstructor
@Repository
public class FCMTokenDao {

    private final StringRedisTemplate tokenRedisTemplate;

    public void saveToken(LoginRequest loginRequest) {
        tokenRedisTemplate.opsForValue()
            .set(loginRequest.getEmail(), loginRequest.getToken());
    }

    public String getToken(String email) {
        return tokenRedisTemplate.opsForValue().get(email);
    }

    public void deleteToken(String email) {
        tokenRedisTemplate.delete(email);
    }

    public boolean hasKey(String email) {
        return tokenRedisTemplate.hasKey(email);
    }
}

 

⦁ controller/UserController.java

@RequiredArgsConstructor
@RequestMapping("/users")
@RestController
public class UserApiController {

//...

    @PostMapping("/login")
    public void login(@RequestBody LoginRequest loginRequest) {
        //...
        fcmService.saveToken(loginRequest);
    }

    @LoginCheck
    @DeleteMapping("/logout")
    public void logout(@CurrentUser String email) {
        //...
        fcmService.deleteToken(email);
    }
}

 

 마지막으로 구매/판매가 성사되었을 경우 거래 등록 유저에게 메세지를 전송하는 로직을 작성하였다.

 

⦁ service/messageService/FCMSerivce.java

@RequiredArgsConstructor
@Service
public class FCMService implements MessageService {

    private final FCMTokenDao fcmTokenDao;

    public void sendSaleCompletedMessage(String email) {
        if (!hasKey(email)) {
            return;
        }

        String token = getToken(email);
        Message message = Message.builder()
            .putData("title", "판매 완료 알림")
            .putData("content", "등록하신 판매 입찰이 낙찰되었습니다.")
            .setToken(token)
            .build();

        send(message);
    }

    public void sendPurchaseCompletedMessage(String email) {
        if (!hasKey(email)) {
            return;
        }

        String token = getToken(email);
        Message message = Message.builder()
            .putData("title", "구매 완료 알림")
            .putData("content", "등록하신 구매 입찰이 낙찰되었습니다.")
            .setToken(token)
            .build();

        send(message);
    }

    public void send(Message message) {
        FirebaseMessaging.getInstance().sendAsync(message);
    }

    // ...
}

 

 fcm 서버로 메세지를 전송할 때 sendAsync()를 사용하여 메세지 전송을 비동기적으로 처리하도록 구현하였으며, 이로써 서버가 전송한 메세지의 응답을 기다리는 동안 블로킹되기 때문에 발생하는 성능 저하를 방지했다.

 

 

 결과적으로 서버에서 메세지가 전송되어 클라이언트의 웹 인스턴스에서 수신하게 되면 아래와 같은 web push 메세지를 확인할 수 있게된다.

 


 

f-lab-edu/shoe-auction

개인 간 신발 거래 서비스. Contribute to f-lab-edu/shoe-auction development by creating an account on GitHub.

github.com

 

'etc.' 카테고리의 다른 글

동기와 비동기 / 블로킹과 논블로킹  (0) 2021.02.10