단순 구현
워낙 공식문서가 잘되어있는 덕에, 공식문서를 통해 토큰을 클라이언트에서 받아오는 데 성공하고
서버에 FCM TOKEN을 관리할 수 있게 셋팅도 해두고, 프로젝트 기획에 맞게 FCM 발송을 할 수 있게 함수를 만들었다.
/**
* alert Type에 맞춰 푸시알림 전송
* @param alertType
* @returns
*/
async fcm(alertType: string, tartgetId: string, testMessage?: string) {
try {
//FCM TOKEN이 있어야만 전송이 가능하므로 전처리 작업
let token: string = null;
const exists = await this.fcmRepo.findOne({ where: { memberId: tartgetId } });
if (!exists || !exists.fcmToken) {
throw FCMTokenNotFoundException();
}
token = exists.fcmToken;
const { title, message } = this.setTitleAndMessageByType(alertType, testMessage);
const payload = {
token: token,
notification: {
title: title,
body: message,
},
};
const result = await admin
.messaging()
.send(payload)
.then(async (response) => {
//개발단계에서 테스트로 webhook으로 로그를 찍어보기 위함
await this.discordProvider.sendToDiscordCustom(`[${alertType} FCM Send]`, tartgetId, 'fcm');
return { sent_message: response };
})
.catch((error) => {
return { error: error.code };
});
return result;
}
catch (e: any) {
if (e.errorCode.status === 404) {
return 404;
}
else return e;
}
}
/**
* 타입 별 메세지 세팅
* @param alertType
* @returns
*/
setTitleAndMessageByType(alertType: string, testMessage?: string): { title: string, message: string } {
let title;
let message;
if (alertType === 'clock-in') {
title = '출근 알림';
message = '출근 체크 부탁드립니다.'
}
else if (alertType === 'clock-out') {
title = '퇴근 알림';
message = '퇴근 체크 부탁드립니다.'
}
else if (alertType === 'request') {
title = '승인 요청 알림';
message = '승인 요청이 들어왔습니다.'
}
else if (alertType === 'receive') {
title = '요청 처리 완료 알림';
message = '요청하신 승인이 처리되었습니다. 확인해주세요.'
}
else if (alertType === 'test') {
title = '테스트',
message = testMessage
}
else if (alertType === 'outting') {
title = '이석/외출 알림'
message = '현재 이석/외출중입니다. 복귀하셨다면 종료 버튼을 눌려주세요';
}
return { title, message };
}
이제, 기능에 맞게 해당 함수의 파라미터 셋팅을 해서 호출하면 된다.
await this.fcmService.fcm('test', 'test', 'test');
데이터를 효율적으로 셋팅하는 방법에 대한 고민
출퇴근 쪽의 푸시알림을 구현할 때에, 최대한 성능을 뽑아보고 싶었다. 개발 기간이 짧긴했지만, 최대한 오래 생각하고자 노력했다.
기획에 따르면,
출근 예정 시간 30분전 부터 출근이 확인될 때 까지 5분 단위로 알림을,
퇴근 예정 시간부터 퇴근이 확인될 때 까지 5분단위로 퇴근 푸시알림을 주기적으로 보내야 했다. 정리해보자면,
1. 오늘 날짜의 휴무가 아닌 임직원분들의 출퇴근 예정 시간 데이터를 매일 가져와야함
2. 주기적으로 알림을 보내는 조건에 맞는 임직원인지 확인해서
3. 푸시알림을 보내야함
5분 단위로 알림을 매일 보내야해서, NEST의 scheduling을 사용하기로 했다.
-- 예시 쿼리.. 근무 시작 시간 오름차순으로
-- 알림 보내야할 데이터 SELECT
SELECT
임직원 근태 테이블의 필드들
FROM
임직원 근태 테이블
WHERE
DAY = '오늘'
AND
TYPE NOT IN ('휴무', '공휴일', '경조사', '기타')
ORDER BY
WORK_START_TIME ASC
매 분 단위로, 위 쿼리를 사용해서 가져온 데이터들을 알림보내야 하는 조건에 맞는지 추가 로직을 태우던
쿼리에서 연산을 해서 나오던 매 분단위로 돌아간다고 생각하니 머리가 지끈했다. 데이터 건수가 많아질수록 문제가 분명 커질 것이다.
수행해야 할 것들을 또다시 정리해보았다.
1. Persistance Layer에 요청을 최대한 적게 보내기
2. 연산을 최대한 작게 수행하기 (조건에 반복에 조건에 반복......... 최대한 줄이기)
1번은 레디스를 사용하면 되겠다는 해답이 즉각 나왔지만, 연산을 최대한 적게해서 부하를 줄여주려다 보니 다시 1번으로 돌아올 수 밖에 없었다.
어떻게 효율적으로 key_value를 담아야 연산을 적게할 수 있을까... 에 대한 고민을 시작했고, redis의 명령어들에 대한 시간 복잡도나 기타 성능들을 파악하여
아래와 같이 target에 id를 담아, ${날짜}:출근or퇴근:${time} 과 같이 KEY를 셋팅하였다.
여기서 time은 알림을 보내야하는 시간이다. 로직 최상단부에 getHHMMFromNow()을 선언하여 HHmm형태로 redis의 key param을 셋팅해 줄 생각이었다.
{
target: [
'A', 'B',
'C', 'D',
'E', 'F',
'G', 'H',
'I', 'J',
'K', 'L',
'M', 'N'
],
time: '0730'
}
이렇게 셋팅하게 된 이유는 아래와 같이 판단했기 때문이다.
1. 출근데이터와 퇴근데이터는 일 단위로 한번만 갱신된다는 점
2. 그렇기 때문에 해당 직원이 출퇴근 했는지 레디스로 파악이 불가능하다는 점
3. 만약 데이터를 다시 업데이트할 경우, 추가로 연산을 수행해야 한다는 점
4. 레디스의 해시구조로 필드에 target이나 time을 사용할까 했지만, 해당 레디스 키값을 삭제하나, 출근 여부에 대한 키를 새로 추가하나 같다고 판단
마지막으로, 출근 퇴근을 앱에서 눌렸을 때, ${day}:출근완료or퇴근완료:${id}를 키로 등록해주고
출근 퇴근 알림을 보낼 때, 알림보내야 할 ID가 들어있으면 해당 멤버는 패스하게되었다.
//출근 알림을 8~12시 사이에 1분단위로만 보내기로 했음.
@Cron('*/1 8-12 * * *',
{
name: 'clock-in-FCM',
timeZone: 'Asia/Seoul'
}
)
async clockINFCMSend(hhmm: string): Promise<void> {
//임의 함수 now()이며 Date now()를 YYYYMMDD string으로 바꿔주는 함수
const today: string = now();
//보내야하는 명단
const userList: string | string[] = await this.cacheService.get(`${today}:출근:${hhmm}`);
userList.forEach(async (user) => {
const excludeUser: string = await this.cacheService.get(`${today}:출근완료:${user}`)
//보내야 하는 경우만
if (!excludeUser) {
await this.fcmService.fcm('출근', user);
}
})
}
마치며
임직원이 200명 내외이기 때문에, 현재 상황에서는 지금이 좋은 방법 중의 하나라고 생각하는데,
확장성을 고려하여 조금 더 생각해보고, 다른 케이스들도 조금 생각해놓고 미리 코드를 작성할 필요를 느낀다.
프로젝트가 마무리되서 실제 임직원분들이 사용하면서의 애로사항들을 포함하여 더 부하가 적은, 성능이 좋은 서비스를 구현해나갈 수 있도록 미리 준비를 해놓고, 완료되면 포스팅을 업데이트하도록 하겠다.
정리해서 써내려가니 엄청 분량이 적게 나온것에 통탄한다..
거의 3~4일을 이거 어떻게 구현해야 나중에도 별 탈없이 돌아갈까...
최선의 솔루션은 아직도 찾아가고 있는 중인 것 같다
2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!