기존 FCM 서비스 로직 흐름
- 알림 생성 요청: 스케줄러나 특정 이벤트에 의해 FCM 푸시 알림을 전송할 필요가 있을 때, FcmService에서 푸시 알림 전송 로직이 호출됩니다.
- FCM 메시지 생성: FcmService에서 FCM 푸시 알림을 전송하기 위해 메시지를 생성합니다. 이 메시지는 FirebaseMessaging 객체를 통해 전송됩니다.
- 멀티캐스트 전송: FcmService는 여러 개의 FCM 토큰을 한 번에 전송할 수 있는 multiSendMessage 메서드를 사용하여 배치로 푸시 알림을 전송합니다. 이 과정에서 Firebase Admin SDK가 사용되며, FCM 서버와 직접 통신하여 푸시 알림을 전송합니다.
- 결과 처리: 전송된 푸시 알림에 대한 결과를 받아 성공 여부를 확인합니다.
기존 코드의 문제점
- 서버 부하: FCM 푸시 알림을 직접 전송할 때, 특히 많은 사용자에게 동시에 푸시 알림을 전송할 경우, Spring 애플리케이션 서버에 부하가 걸릴 수 있습니다. 이는 추후에 MAU가 높아졌을 때 서버의 성능 저하로 이어질 수 있으며, 서버가 다른 중요한 요청을 처리하는 데 영향을 줄 수 있습니다.
- 확장성 문제: FCM 푸시 알림 전송 로직이 Spring 애플리케이션에 내장되어 있기 때문에, 트래픽이 급격히 증가하는 경우 이를 효율적으로 처리하기 어려울 수 있습니다.
- 실패 처리 문제: 배치 전송 중 일부 메시지가 실패할 경우, 전체 배치가 실패한 것처럼 처리될 가능성이 있습니다. → 배치가 실패하면 하나의 배치로 묶인 메시지가 날아가는 큰 문제점이 존재합니다.
따라서, FCM 알림 서비스를 Spring 애플리케이션에서 분리하여 SQS + Lambda를 활용해 처리하기로 결정했습니다. 전체적인 로직의 흐름은 아래와 같습니다.
SQS + Lambda 로직 흐름
- 알림 생성 요청: 스케줄러나 특정 이벤트에 의해 FCM 푸시 알림을 전송할 필요가 있을 때, SqsMessageService를 통해 SQS 큐에 메시지를 전송합니다.
- SQS 큐로 메시지 전송: FCM 푸시 알림 요청이 SQS 큐로 전송됩니다. 각 FCM 메시지는 SQS 큐에 메시지로 저장됩니다. SQS 큐는 높은 확장성을 가지고 있으며, 여러 메시지를 효율적으로 처리할 수 있습니다.
- Lambda 트리거: SQS 큐에 메시지가 들어오면, Lambda 함수가 트리거되어 큐에서 메시지를 가져와 처리합니다.
- Lambda에서 FCM 메시지 전송: Lambda 함수는 큐에서 받은 메시지를 바탕으로 FCM 푸시 알림을 생성하고, 이를 Firebase Admin SDK를 통해 전송합니다. Lambda는 자동으로 확장되어 동시에 많은 메시지를 처리할 수 있습니다.
- 결과 처리: Lambda 함수 내에서 메시지 전송 결과를 처리하고, 실패한 메시지에 대한 로그를 남기거나 재시도 로직을 구현할 수 있습니다.
SQS + Lambda 사용의 장점
- 확장성: SQS는 매우 확장 가능하고 안정적인 메시징 서비스를 제공하며, Lambda는 필요에 따라 자동으로 확장되어 많은 양의 푸시 알림 요청을 동시에 처리할 수 있습니다. 이로 인해 트래픽이 급증하는 상황에서도 안정적으로 푸시 알림을 전송할 수 있습니다.
- 서버 부하 감소: FCM 푸시 알림 전송 작업이 Spring 애플리케이션 서버가 아닌 Lambda에서 처리되므로, 애플리케이션 서버의 부하가 크게 줄어듭니다. 이는 서버가 다른 중요한 작업을 더 효과적으로 처리할 수 있도록 합니다.
- 비용 효율성: Lambda는 사용한 만큼만 비용을 지불하는 구조로, 특정 시점에만 푸시 알림을 많이 전송하는 경우 비용을 절감할 수 있습니다.
- 에러 처리: SQS와 Lambda를 사용하면 메시지의 실패 시에도 자동으로 재시도할 수 있는 메커니즘을 설정할 수 있으며, 배치 전송 중 일부 메시지가 실패해도 나머지 메시지에는 영향을 미치지 않게 처리할 수 있습니다.
위와 같은 장점들로 인해, 기존 코드를 SQS + Lambda 구조로 리팩토링하기로 결정했습니다. 이를 위해, 우선, SQS와 Lambda를 연결해서 Event를 처리하는 서버를 구현해보겠습니다.
- SQS 인스턴스 생성
- lambda 함수 생성
- lambda에 SQS 권한 부여
- 생성된 lambda에 트리거로 SQS추가
위와 같이 설정을 하면 lambda와 SQS가 잘 연결되었는지 테스트 코드를 통해 확인할 수 있습니다.
이벤트 JSON형식은 lambda 함수에서 코드를 생성한 형식에 맞춰서 테스트를 진행해야 합니다.
Spring 애플리케이션에서 FCM 푸시 요청을 처리하기 위해서는 Lambda 함수에서 해당 요청을 받아 처리하는 코드가 필요합니다. 이러한 작업을 처리하기 위해 주로 Python이나 Node.js와 같은 언어가 많이 사용됩니다.
두 언어 모두 AWS Lambda에서 지원되며, 간결하고 효율적인 비동기 처리가 가능하기 때문에 이러한 장점을 고려하여, 저는 Python을 사용하여 Lambda 함수를 작성했습니다.
Python은 간결하고 직관적인 문법을 제공하며, AWS SDK (boto3)와 같은 라이브러리 지원이 잘 되어 있어 AWS 서비스와의 통합이 용이합니다.
작성한 lambda_function.py 코드를 순서에 맞게 설명하면
- Lambda 함수 호출 (lambda_handler 실행):
- SQS에서 메시지가 도착하면 AWS Lambda가 자동으로 lambda_handler 함수를 호출합니다.
- 이때 SQS 메시지의 내용이 event 매개변수로 전달됩니다.
- Firebase 초기화 (initialize_firebase):
- Lambda 함수가 실행될 때 Firebase가 이미 초기화되지 않았다면 initialize_firebase 함수가 호출되어 Firebase가 초기화됩니다.
- 이 과정에서 AWS Secrets Manager에서 Firebase 자격 증명을 가져와 Firebase SDK를 설정합니다.
- SQS 메시지 처리:
- lambda_handler 함수에서 event['Records']를 순회하면서 SQS 메시지들을 하나씩 처리합니다.
- 각 메시지의 body를 JSON으로 파싱하고, title, body, token 정보를 추출합니다.
- FCM 알림 전송 (send_fcm_update_notification):
- 추출된 정보(제목, 내용, 디바이스 토큰)를 바탕으로 FCM 푸시 알림 메시지를 생성합니다.
- send_fcm_update_notification 함수를 통해 Firebase Cloud Messaging을 사용하여 해당 디바이스로 알림을 전송합니다.
- 결과 반환:
- 모든 메시지가 성공적으로 처리되면 lambda_handler는 상태 코드 200과 함께 성공 메시지를 반환합니다.
- 처리 중 오류가 발생하면 오류 메시지를 출력하고 상태 코드 500을 반환합니다.
import json
import boto3
import firebase_admin
from firebase_admin import credentials, messaging
def get_firebase_credentials():
secret_name = "firebase-dev/serviceAccount" # Secrets Manager에 저장한 비밀의 이름
region_name = "ap-northeast-2" # Secrets Manager가 위치한 리전
# Secrets Manager 클라이언트 생성
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name=region_name
)
try:
# Secrets Manager에서 비밀 가져오기
get_secret_value_response = client.get_secret_value(SecretId=secret_name)
secret = get_secret_value_response['SecretString']
return json.loads(secret)
except Exception as e:
print(f"Error retrieving secret: {e}")
raise e
def initialize_firebase():
# 기본 Firebase 앱이 이미 초기화되었는지 확인
if not firebase_admin._apps:
# Firebase 자격 증명 가져오기
credentials_data = get_firebase_credentials()
cred = credentials.Certificate(credentials_data)
firebase_admin.initialize_app(cred)
def send_fcm_multicast_notification(title, body, tokens):
try:
# FCM 메시지 생성 및 전송 로직
message = messaging.MulticastMessage(
notification=messaging.Notification(
title=title,
body=body
),
tokens=tokens # 여러 개의 디바이스 토큰을 사용
)
response = messaging.send_multicast(message)
print('Successfully sent message:', response)
except messaging.ApiCallError as e:
print(f"API call error sending message: {e}")
except Exception as e:
print(f"Error sending message: {e}")
def lambda_handler(event, context):
try:
# Firebase 초기화
initialize_firebase()
# SQS 메시지 처리
tokens = []
title = None
message_body = None
for record in event['Records']:
# SQS 메시지 본문 가져오기
body = json.loads(record['body'])
title = body.get('title')
message_body = body.get('body')
token = body.get('token')
tokens.append(token)
if tokens:
print(f"Sending notification to tokens: {tokens}")
send_fcm_multicast_notification(title, message_body, tokens)
return {
'statusCode': 200,
'body': json.dumps('Messages sent successfully')
}
except Exception as e:
print(f"Error in lambda_handler: {e}")
return {
'statusCode': 500,
'body': json.dumps(f"Error: {str(e)}")
}
리팩토링 된 코드를 확인하려면 다음 Github를 확인해주세요!
- 전체 코드 링크
GitHub - depromeet/WalWal-server: 세상 모든 반려동물을 한 자리에서! 왈왈🐶 백엔드
- PR 링크
FCM 알림 서비스 SQS + Lambda 리팩토링 by dbscks97 · Pull Request #238 · depromeet/WalWal-server
리팩토링 하면서 삭제된 코드와 이유
삭제 된 내용
- FcmService: 해당 클래스는 Spring 애플리케이션에서 FCM 푸시 알림을 전송하기 위해 사용되었습니다. 이 서비스는 multiSendMessage와 같은 메서드를 통해 여러 개의 FCM 푸시 알림을 배치로 전송하는 로직을 포함하고 있었습니다.
- 이제 FCM 푸시 알림은 SQS(Lambda)를 통해 전송되도록 변경되었습니다. 따라서 Spring 애플리케이션에서 직접 FCM 메시지를 전송하는 기능이 더 이상 필요하지 않습니다. FcmService와 관련된 모든 로직은 Lambda 함수로 대체되었으므로, 이 클래스는 불필요하게 되어 졌습니다.
삭제 된 내용
- FcmConfig : 해당 클래스는 Spring 애플리케이션에서 FCM을 사용하기 위한 설정 파일로, Firebase Admin SDK를 초기화하여 FCM 푸시 알림을 전송할 수 있는 FirebaseMessaging 빈을 설정하는 역할을 했습니다.
- Spring 애플리케이션이 더 이상 FCM 푸시 알림을 직접 관리하지 않기 때문에 이 설정 파일은 필요 없게 되었습니다. FCM 푸시 알림 전송이 Lambda에서 처리되므로, Spring 애플리케이션 내에서 Firebase Admin SDK를 초기화하거나 사용하는 작업이 없어졌습니다.
'Spring' 카테고리의 다른 글
정적 팩토리 메서드 패턴의 중요성 (0) | 2024.07.17 |
---|---|
Error creating bean with name 'jpaAuditingHandler' 에러 해결 (0) | 2024.02.01 |
멀티모듈 서비스 레이어 분리 (0) | 2024.02.01 |
ApplicationTests > contextLoads() FAILED 에러 해결 (1) | 2024.02.01 |
메세지 큐를 활용한 트랜잭션 관리 - 기술적 챌린지 (1) | 2023.08.22 |