2023. 11. 8. 20:50ㆍProject 우아한남형제들/기술적 고민
https://organic-hunter-0ab.notion.site/FCM-60b5f7ebbb1f486a8b139ebe4ea5a09c?pvs=4
이 글은 위 팀 노션에서 서로 공유하기 위한 글이지만, 다른 사람들에게도 공유되면 좋겠어서 옮긴 글입니다 :)
배경
Firebase Cloud Messaging(이하 FCM) 관련해서
user-service는 알람 이벤트를 어떻게 핸들링할지 의뢰가 들어왔습니다.
이 부분에 대해서 협의가 충분히 고려해야 되는 이유는
사용자에게 실제로 가는 알람까지 여러 마이크로서비스의 협업이 동반되기 때문입니다.
간략히 FCM 의 과정을 설명하자면
사용자가 애플리케이션에 로그인할 때 생성되는 FCM 토큰을 활용하여
사용자가 주문 상태 변경과 같은 중요한 이벤트를 실시간으로 받아볼 수 있도록 합니다.
그래서 정확히 각 서비스가 어떻게 동작해야 할지에 대한 협의가 필요합니다
FCM 이란?
쉽게 얘기하면 실제로 휴대폰에 알림 메시지를 보내주는 플랫폼입니다.
알람 서비스에서 알람을 보내달라는 요청을 FCM이 받고
FCM이 실제로 메시지 보내게 되는 주축입니다.
MSA 상에서 어떻게 처리될지에 대한 과정을 한번 정리해 봤습니다.
FCM 처리 과정
프로세스 설명
- 로그인 및 FCM 토큰 저장 (Client → user-service )
- user-service에서 로그인 후,
- user-service는 FCM 토큰을 Redis에 저장합니다.
- 일단 일단락
- 주문 상태 변경 감지 / 메뉴 상태 변경 (ex 재고 out) 시 알림 요청 (order-service → alarm-service )
- order-service에서 주문 상태가 변경이 되면
- alarm-service로 알림을 보내달라는 요청을 합니다.
- 알림 서비스가 FCM 토큰을 조회 (alarm-service → user-service )
- alarm-service는 user-service를 사용자의 FCM 토큰을 요청, 받아옵니다.
- 알림 발송 (alarm-service → firebase )
- 조회된 FCM 토큰을 사용하여 Firebase Cloud Messaging 서비스에 알림을 전송합니다.
- FCM 서버는 해당 토큰의 장치로 알림을 전송합니다.
코드 예시 (모든 통신은 일단 Open Feign)
아직 어느 통신 부분을 메시지 큐를 사용할지 정해지지 않아 일단 Open Feign 기준의 코드들입니다.
1. 로그인 및 FCM 토큰 저장 (Client → user-service )
user-service에서 로그인 처리 후 FCM 토큰 저장 로직
// UserApplication.java
@RestController
public class UserController {
@Autowired
private final UserService userService;
@Autowired
private final RedisRepository redisRepository; // FCM 토큰을 저장하기 위한 Redis 레포지토리
@PostMapping("/login")
public ResponseEntity<?> loginUser(@RequestBody LoginRequest loginRequest) {
// 로그인 로직 처리
User user = userService.login(loginRequest.getUsername(), loginRequest.getPassword());
// FCM 토큰을 Redis에 저장
redisRepository.saveToken(user.getId(), loginRequest.getFcmToken());
return ResponseEntity.ok().body(user);
}
}
2. 주문 상태 변경 감지 / 메뉴 상태 변경 (ex 재고 out) 시 알림 요청 (order-service → alarm-service )
order-service 내 주문 상태 변경 감지 후 alarm-service에 알림 요청
// OrderApplication.java
@RestController
public class OrderController {
@Autowired
private final OrderService orderService;
@Autowired
private final AlarmServiceClient alarmServiceClient; // Feign 클라이언트
@PostMapping("/orders/{orderId}/status")
public ResponseEntity<?> updateOrderStatus(@PathVariable Long orderId, @RequestBody OrderStatusUpdateRequest request) {
Order order = orderService.updateOrderStatus(orderId, request.getStatus());
// 주문 상태 변경 시 알림 서비스에 알림 요청
alarmServiceClient.sendNotification(order.getUserId(), "Your order status has been updated.");
return ResponseEntity.ok().body(order);
}
}
@FeignClient("alarm-service")
public interface AlarmServiceClient {
@PostMapping("/notify")
void sendNotification(@RequestParam("userId") Long userId, @RequestParam("message") String message);
}
3. 알림 서비스가 FCM 토큰을 조회 (alarm-service → user-service )
alarm-service 내에서 user-service를 통해 FCM 토큰 조회
// AlarmApplication.java
@RestController
public class AlarmController {
@Autowired
private final UserServiceClient userServiceClient; // Feign 클라이언트
@PostMapping("/notify")
public ResponseEntity<?> notifyUser(@RequestParam("userId") Long userId, @RequestParam("message") String message) {
// user-service에서 FCM 토큰 조회
String fcmToken = userServiceClient.getFcmToken(userId);
// FCM을 통해 알림 발송 로직 (여기서는 로직이 생략되어 있음)
sendNotificationToFirebase(fcmToken, message);
return ResponseEntity.ok().build();
}
private void sendNotificationToFirebase(String fcmToken, String message) {
// Firebase Cloud Messaging에 알림을 보내는 코드
}
}
@FeignClient("user-service")
public interface UserServiceClient {
@GetMapping("/users/{userId}/fcmToken")
String getFcmToken(@PathVariable("userId") Long userId);
}
4. user-Service에서 FCM 토큰 제공 ( user-service→alarm-service )
user-service에서 FCM 토큰을 조회하여 제공하는 부분
// UserApplication.java
@RestController
public class UserController {
@Autowired
private final RedisRepository redisRepository; // Redis 레포지토리
@GetMapping("/users/{userId}/fcmToken")
public String getFcmToken(@PathVariable("userId") Long userId) {
return redisRepository.findTokenByUserId(userId);
}
}
5. 알림 서비스가 FCM을 통해 알림 발송
앞서 AlarmController 내의 sendNotificationToFirebase 메서드에서
실제로 FCM에 알림을 발송하는 로직이 구현됩니다.
여기서는 FCM과의 통신을 담당하는 별도의 서비스나 라이브러리를 사용하게 됩니다.
// AlarmApplication.java
@RestController
public class AlarmController {
@Autowired
private final UserServiceClient userServiceClient; // Feign 클라이언트
@PostMapping("/notify")
public ResponseEntity<?> notifyUser(@RequestParam("userId") Long userId, @RequestParam("message") String message) {
// user-service에서 FCM 토큰 조회
String fcmToken = userServiceClient.getFcmToken(userId);
// ******FCM을 통해 알림 발송 로직 (여기서는 로직이 생략되어 있음)******
sendNotificationToFirebase(fcmToken, message);
return ResponseEntity.ok().build();
}
private void sendNotificationToFirebase(String fcmToken, String message) {
// Firebase Cloud Messaging에 알림을 보내는 코드
}
}
@FeignClient("user-service")
public interface UserServiceClient {
@GetMapping("/users/{userId}/fcmToken")
String getFcmToken(@PathVariable("userId") Long userId);
}
기술적 결론
- user-service에서 Redis에 FCM 토큰을 담아 보관합니다.
- Redis에서는 userId를 키 값으로 사용할 예정입니다.
- 추후 다른 서비스에서 해당 FCM 토큰을 원하면
http://user-service/users/{userId}/fcmToken로 호출할 수 있도록 api를 생성 예정 - Kafka의 사용 여부는 아직 결정 x