SpringBoot 3.1.6 FCM 적용 (1)
어떠한 구단이 경기 일정을 생성하였을 때, 상대 구단에서 이를 승낙하면 일정이 생성되도록 로직을 설계하였다. 이때 경기 일정에 대해 상대팀 구단장에게 푸시 알림이 가도록 기획하였으며, 비즈니스 로직은 다음과 같다.
1. 사용자(구단주)가 일정을 생성하기 위해 상대 팀 선택과 위치 선택, 기본 정보들을 기입한다.
2. 일정 생성 버튼을 누르게 되면 상대 팀의 구단주에게 FCM을 통한 푸시알림 요청. 이때 먼저 db에 데이터가 생성되지만 아직 상대 구단주의 승낙이 나지 않았기에 필드 내 승낙에 대한 정보를 false로 저장된다. (post)
3. 상대 팀의 구단주가 승낙 버튼을 누르면 일정이 성공적으로 생성. 승낙에 대한 여부를 true로 변경한다. / 거절 버튼을 누르면 임시 생성된 일정을 삭제한다. (put or delete)
여기서 FCM을 프로젝트에 적용하기 위해 이를 정리해보자 한다.
FCM(Firebase - Cloud - Messaging)
Firebase Cloud Messaging(FCM)은 메시지를 안정적으로 무료 전송할 수 있는 크로스 플랫폼 메시징 솔루션이다. 개발자가 서버와 클라이언트 사이에 효율적인 방식으로 메시지를 전송할 수 있게 해주며, 이를 통해 알림, 메시지 등을 사용자의 디바이스에 직접 전달할 수 있게 된다. 또한 다양한 클라이언트 환경 (안드로이드, iOS, 웹) 등에서 범용성 있게 사용 가능한 장점이 존재한다.
FCM을 이용해 각 유저들에게 푸시메시지를 전송하기 위해선 TOKEN , TOPIC을 활용하여 푸시 메시지를 보낼 수 있다.
TOKEN
- FCM에서 토큰은 FCM 서버와 통신하기 위한 고유 식별자이다. 이를 통해 FCM 서버에서 앱을 식별하고, 메시지 전송을 할 수 있게 된다.
- FCM의 토큰은 앱이 설치된 디바이스마다 고유하다. 앱이 설치된 디바이스를 추가하거나 삭제할때 토큰이 변경될 수 있다. (refresh)
- 서버는 이러한 FCM 토큰을 사용하여 특정 디바이스에 메시지를 전송할 수 있다.
즉, Token은 Firebase에서 관리하는 프로젝트별 접속하는 기기의 고유 ID로 볼 수 있다.
TOPIC
- 토픽(Topic)은 일종의 채널로서, 이를 통해 일련의 수신자들에게 메시지를 전송할 수 있다.
- 구독 및 구독취소 요청 시, FCM은 구독한 유저들을 내부적으로 관리한다.
- subscribe, unsubscribe 메서드를 통해 구독과 구독 취소요청을 FCM에 전송할 수 있다.
- 토픽을 통한 푸시 발송시, 토픽을 구독한 사용자들에게만 메시지를 전송하게 된다.
토픽 사용 시 주의할 점에 대해서는 다음과 같다.
- 토픽의 이름은 알파벳과 숫자로만 구성되어야 하며, 길이는 최대 256자까지 설정 가능하다.
- 특수문자나 공백은 사용이 불가능하다.
- 토픽 이름은 고유해야 한다. 같은 이름의 토픽이 이미 존재한다면 새로운 토픽을 생성할 수 없다.
- 토픽 주제는 한글로 사용할 수 없다.
여기서 이번에 사용할 것은 토큰을 사용한 FCM 로직 구현이다. 이유는 동아리장만이 일정을 생성할 권한을 주도록 하여 토픽 사용보다는 토큰을 사용하는 것이 적절하다고 생각하였기 때문이다. 먼저 이를 개발하기 전에 토큰은 어디에 저장되어 있어야 적절한지 고려해야 하였다.
다음과 같은 기준으로 저장소를 선택하였다.
- 토큰 사용 빈도 측면에서 경기 일정 생성 시 상대 팀 구단주에게 푸시 알림을 보내야 하므로, 토큰은 매번 경기 일정이 생성될 때마다 사용된다. 규모가 큰 구단에서는 잦은 일정 생성이 이루어 질 수 있지만 그렇지 않은 구단에서는 일정 생성이 자주 이루어지지 않을 것이라고 예상된다.
- 데이터의 중요성 측면에서 FCM 토큰은 중요한 정보이다. 승낙 과정이 완료되어야만 일정이 확정되므로, 이 과정에서 푸시 알림의 정확한 전달은 필수적이다. 때문에 토큰이 손실되거나 잘못 관리되면 중요한 통신이 실패할 수 있다.
Redis - Redis는 빠른 데이터 액세스가 가능하므로, 자주 액세스되는 토큰의 경우 효율적인 선택이 될 수 있다. 토큰 조회가 매우 빈번하고 시간이 선택에 중요한 고려 요소가 되는 경우 Redis를 사용하는 것이 유용할 것이다. 하지만, Redis는 기본적으로 메모리 내 데이터 저장을 사용하기 때문에 지속성에 대한 추가 구성이 필요할 수 있다는 점이 존재한다.
RDBMS - 토큰과 사용자 또는 구단 정보 간의 관계가 중요하거나, 트랜잭션 관리가 필요한 경우 RDBMS가 더 적합할 것이다. 비록 Redis보다 조회 속도가 느릴 수 있지만, 데이터의 안전성과 일관성을 보장할 수 있다는 장점이 있다.
일정 생성은 구단마다 다르며, 이에 따른 토큰 조회 또한 제각각일 것이다. 이때 빠른 데이터 엑세스 또한 주요 요소라고 생각이 들지 않았으며, 오히려 토큰과 사용자 및 구단 정보를 관계형 데이터베이스로 묶는 것이 더 효율적으로 관리할 수 있을 것이라고 생각했다.
일정 생성 요청 시 상대 구단의 id를 받아 해당 구단의 구단주를 탐색하고, 해당 구단주의 id값으로 토큰을 가져오는 로직을 하나의 트랜잭션으로 묶으면 좋을 것이라 판단하고, 해당 프로젝트에서는 RDBMS에 토큰을 저장하기로 하였다.
두 번째로 고려해야 하는 점은 토큰을 어떤 시점에 저장소에 저장하고, 업데이트 해주어야 하는지에 대한 것이다. 우선 저장하는 시점에 대한 상황을 생각해보았을 때 다음과 같다.
- 회원가입 시
- 로그인 시
- 구단 생성 시
- 앱 설치, 첫 실행 시
여기서 채택한 것은 사용자가 앱을 처음 실행할 때 토큰을 생성하고, 사용자가 로그인 또는 회원가입을 할 때 이 토큰을 서버에 업데이트하는 방식이다. 이렇게 하게 된다면 기기를 변경하였을 때 발생할 수 있는 문제점을 사전에 차단하는 것이 가능하기 때문이다. 때문에 서버 입장에서는 토큰 저장소를 조회하여 존재하고 있으면 업데이트, 없다면 새로 저장하는 api만 만들면 되었다.
1. User Service 토큰 저장소 생성
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Entity(name = "fcm_token_tb")
public class FcmToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "fcm_token_id")
private Long id;
@Column(name = "user_id")
private Long userId;
@Column(name = "fcm_token")
private String fcmToken;
}
토큰은 저장소는 userId와 fcmToken을 필드로 갖도록 설계.
2. FCMTokenService
@Service
@RequiredArgsConstructor
public class FcmTokenService {
private final FcmTokenRepository fcmTokenRepository;
@Transactional
public void saveOrUpdateToken(FcmTokenDto fcmTokenDto) {
fcmTokenRepository.findByUserId(fcmTokenDto.getUserId())
.ifPresentOrElse(
token -> fcmTokenRepository.updateFcmTokenByUserId(fcmTokenDto.getUserId(),
fcmTokenDto.getFcmToken()),
() -> {
FcmToken newToken = FcmToken.builder()
.userId(fcmTokenDto.getUserId())
.fcmToken(fcmTokenDto.getFcmToken())
.build();
fcmTokenRepository.save(newToken);
}
);
}
}
토큰 저장소를 조회하여 존재하고 있으면 업데이트, 없다면 새로 저장한다.
3. FcmTokenRepository
@Repository
public interface FcmTokenRepository extends JpaRepository<FcmToken, Long> {
Optional<FcmToken> findByUserId(Long userId);
@Modifying
@Query("UPDATE fcm_token_tb f SET f.fcmToken = :fcmToken WHERE f.userId = :userId")
void updateFcmTokenByUserId(@Param("userId") Long userId, @Param("fcmToken") String fcmToken);
}
JPQL을 사용하여 업데이트 로직을 완성하였다.
4. FcmTokenController
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user-service/fcm")
public class FcmTokenController {
private final FcmTokenService fcmTokenService;
@PostMapping("/token")
public DataResponse saveOrUpdateToken(@RequestBody FcmTokenDto fcmTokenDto) {
fcmTokenService.saveOrUpdateToken(fcmTokenDto);
return new DataResponse();
}
}
클라이언트단에서는 앱을 처음 실행할 때 , 사용자가 로그인 또는 회원가입을 할 때 해당 API를 호출하도록 한다.
reference
https://zuminternet.github.io/FCM-PUSH/