Discord.js로 2000ms 이상 소요되는 api에 대해 알람을 보내도록 설정해 놓았는데, 4000ms가 넘는 api가 나왔다.
async likeStore(userUUID: string, storeUUID: string): Promise<void> {
const store = await StoreModel.findOne({
uuid: storeUUID,
deletedAt: null,
});
if (!store) {
// 삭제되었거나 없는 가게일 경우
throw new InternalServerError(ErrorCode.STORE_NOT_FOUND, [
{ data: 'Store not found' },
]);
}
// 가게에 좋아요
await new StoreLikeModel({ user: userUUID, store: storeUUID }).save()
// 해당 가게에 이번주에 대한 점수 부여하는 함수
await this.scoreToStore(userUUID, storeUUID, store.location, 'add');
}
async scoreToStore(userUUID: string, storeUUID: string, location: string,
type: 'add' | 'sub' ): Promise<void> {
const [sunStart, satEnd] = dayjsKR.getWeek();
// 이번주(일~토)안에 등록되었는지 확인
const storeScore = await StoreScoreModel.findOne({
store: storeUUID,
date: { $gt: sunStart, $lt: satEnd },
});
if (storeScore) {
if (type === 'add') {
storeScore.score += 1;
} else {
storeScore.score -= 1;
}
await storeScore.save();
} else {
// 없다면 score 1으로 생성
const locationSplit = location.split(' ');
let shortLocation;
if (locationSplit.length < 2) {
// 주소가 한단어 이하인 경우 그냥 주소룰 할당
shortLocation = location;
} else {
// 두 글자 이상인 경우 앞 문자 두개 저장
shortLocation = `${locationSplit[0]} ${locationSplit[1]}`;
}
await new StoreScoreModel({
store: storeUUID,
shortLocation: shortLocation,
date: Date.now().toString(),
score: type === 'add' ? 1 : 0,
}).save();
}
}
이게 해당 API이다. 해당 가게에 좋아요를 누르는 API였는데 매주, 지역마다 좋아요를 가장 많이 받은 상위5개의 가게를 선정해야 했기 때문에 좋아요를 누르는 API에 해당 가게에 점수를 부여하는 로직도 같이 있어서 그런 것 같았다.
await Promise.all([
new StoreLikeModel({
user: userUUID,
store: storeUUID,
}).save(),
this.scoreToStore(userUUID, storeUUID, store.location, 'add')
]);
먼저 두개의 작업이 서로 다른 table에 데이터를 넣는 작업이여서, Promise.all로 동시에 실행 해주었다.
Javascript의 Promise.all은 하나의 작업이라도 실패하면 전체 작업을 중단하지만, Javascript의 Promise는 취소되지 않기 때문에 실패하기전에 하나의 작업이 이미 완료되었다면, 해당 작업은 정상적으로 완료되어버린다.이렇게 되면 좋아요는 기록되지 않고, 해당 가게의 점수만 올라가게 되는 것이다.
const [storeLike, storeScore] = await Promise.all([
new StoreLikeModel({
user: userUUID,
store: storeUUID,
}).save(),
this.scoreToStore(userUUID, storeUUID, store.location, 'add')
]);
if(storeLike.status === 'rejected') { rollback; }
if(storeScore.status === 'rejected') { rollback; }
따라서 해당 작업들을 Promise.all 대신 Promise.allSettled로 병렬처리하고 각각의 반환값을 체크하여 하나의 작업이라도 실패한 경우에는 나머지 작업을 rollback해주는 코드를 넣어주었다.
const locationSplit = location.split(' ');
let shortLocation;
if (locationSplit.length < 2) {
// 주소가 한단어 이하인 경우 그냥 주소룰 할당
shortLocation = location;
} else {
// 두 글자 이상인 경우 앞 문자 두개 저장
shortLocation = `${locationSplit[0]} ${locationSplit[1]}`;
}
await new StoreScoreModel({
store: storeUUID,
shortLocation: shortLocation,
date: Date.now().toString(),
score: type === 'add' ? 1 : 0,
}).save();
두번째로는 해당 가게 지역의 shortLocation을 매번 좋아요 누를때 일일히 구하지 않고, 미리 가게를 생성할때 추가하여 매번 생성하는 일이 없도록 하였다. 우리 서비스는 생성 / 수정 보다 조회가 훨씬 많이 일어나기 때문에 조회나 좋아요 같은 자주 일어나는 작업에는 최대한 작업을 줄이고 생성 / 수정에 최대한 많은 로직을 넣게 수정하였다.
await new StoreScoreModel({
store: storeUUID,
shortLocation: store.shortLocation,
date: Date.now().toString(),
score: type === 'add' ? 1 : 0,
}).save();
데이터베이스의 한 column을 추가함으로써 shortLocation을 계산하는 부분을 없앴고, 위의 코드를 이렇게 줄일 수 있었다.
위 처럼 코드를 개선한 후 총 10번의 api테스트를 진행하였을때 모두 1000ms로 소요시간이 줄어든 점을 확인할 수 있었다.