Web/MongoDB

MongoDB에 리뷰 저장하고 평점 평균 계산하기

가나닩 2024. 6. 27. 00:55

평점을 포함한 리뷰 작성이 가능한 웹페이지를 만들었을때 사용자가 입력한 리뷰데이터들이 DB에 모이면 평점들의 평균을 계산하여 페이지에 출력해줘야 합니다.

 

데이터베이스에 저장된 값의 평균을 구하는건 다양한 방법이 있는데, 구현 하는 상황에 맞게 선택해 사용해야 합니다.

결론부터 말하자면 리뷰 평균 계산에서는 3번의 방식을 사용하였습니다.

 

1. 서버에서 주기적으로 영화평점의 평균을 계산하여 DB에 미리 저장

사용자 피드백과 상관없이 일정주기 혹은 일정 시간에 특정 코드를 실행시키고 싶으면 크론(cron)을 사용하면 됩니다. 크론을 사용하면 여러가지 실행할 코드나 작업에 대한 스케쥴링이 가능합니다.

 

Vercel에서는 cron을 기본적으로 제공하고 있습니다. vercel배포시 작성하는 vercel.json에 cron관련 내용을 추가하여 api를 호출하는 방식으로 스케쥴링을 진행합니다.

 

평점들을 불러와 평균을 계산하고 DB에 저장하는 API를 만든뒤 cron을 이용해 주기적으로 API를 호출하면 됩니다.

 

참고 : How to Setup Cron Jobs on Vercel

 

How to Set Up Cron Jobs on Vercel

Learn how to set up and use low-frequency and high-frequency cron jobs on Vercel.

vercel.com

 

하지만 이 방식은 큰 단점을 가지고 있어 실제로 사용하지 않았습니다.

  • 영화수, 리뷰수가 많아지면 평점의 변화와 상관없이 모든 리뷰데이터의 평균을 계산하게 되서 비효율적임.
  • 사용자에게 실시간으로 정확한 리뷰의 평균점수를 제공할 수 없음.

 

 

2. 리뷰를 조회할때마다 해당 영화의 평점 평균을 계산하여 표시

리뷰 평균평점을 표시해야하는 페이지에 접속하면 필요한 영화의 평균점수만 계산하여 표시해 줄 수 있습니다.

필요한 부분만 계산을 진행하므로 앞서 설명한 주기적으로 평균을 계산하는 방식보다 연산의 수가 확연하게 줄어듭니다.

또한 사용자에게 실시간으로 정확한 평균점수를 제공해줄수 있다는 장점이 있습니다.

 

아래 코드는 평균점수를 계산하는 예시의 일부입니다.

// ...db 연결 과정 생략
const collection = db.collection('review');

//movie_id로 데이터를 찾고 데이터 내부의 rating값으로 평균을 계산하도록 설정
//여기서 movie_id는 사용자에게 request로 받음 
const pipeline = [
    { $match: { movie_id: movie_id } },
    {
        $group: {
            _id: "$movie_id",
            averageRating: { $avg: { $toDouble: "$rating" } },
        },
    },
];

//aggregate를 이용해 평균을 계산
const results = await collection.aggregate(pipeline).toArray();

if (results.length === 0) {
    res.status(404).json({ error: "리뷰 데이터를 찾을 수 없음" });
} else {
    // match로 필터링된 배열의 첫번째 항목을 response
    res.status(200).json({ averageRating: results[0].averageRating });
}

 

MongoDB의 aggregate를 이용해도 되지만 movie_id에 매칭되는 데이터를 전부 가져와 직접 평균을 계산해도 됩니다.

 

하지만 이 방식도 단점이 있습니다.

  • 요청이 들어왔을때 평균을 계산하고 응답을 주므로 페이지 로딩이 오래걸릴 수 있음
  • 평균점수가 변하지 않더라도 리뷰정보를 보기위해 사용자가 접속할때마다 평균점수를 계산해야하므로 여전히 비효율적임.

 

3. 리뷰를 작성할때마다 해당 영화의 평점 평균을 계산하여 DB에 저장

DB에 영화별로 평균 점수를 저장해두고 새로운 리뷰가 작성될때 업데이트 되도록 할 수 있습니다.

모든 유저의 요청에 대해 평균 점수를 계산하지 않아도 되고, 실질적으로 평균점수가 변할때(새로운 리뷰가 작성될때)만 평균을 다시 계산하므로 서버의 부하가 획기적으로 줄어듭니다.

또한 모든 요청에 대해 정확한 평균값을 제공해줄 수 있습니다.

 

* 주의할 점은 서버에서 신규 리뷰를 추가할때 평균점수의 계산까지 모두 마친뒤 정상완료 응답을 보내면 새로운 리뷰를 작성하는 사람은 리뷰가 정상적으로 작성되기를 기다리는 시간이 길어질 수 있습니다. 따라서 리뷰 추가 과정 자체에 문제가 없을경우 사용자에게 정상완료 응답을 보내고, 이후에 평균점수를 계산하도록 해야합니다.

 

아래는 평균점수를 DB에서 따로 관리하기 위한 예시의 일부입니다.

// 리뷰 추가 과정 생략 (아래 코드는 리뷰추가 완료후 실행됨)

// movie_id로 신규리뷰가 작성되는 영화의 평균점수 데이터가 존재하는지 탐색
let findReview = await db.collection("average_rating").findOne({ movie_id: req.body.movie_id });
console.log(findReview);

if (findReview) {
// 평균점수 데이터가 존재할경우 평균을 재계산하여 업데이트
    let nowRating = findReview.avgRating;
    let nowCount = findReview.count;
    let newRating = (nowRating * nowCount + Number(req.body.rating)) / (nowCount + 1);
    let updateRating = await db.collection("average_rating").updateOne(
        {
            movie_id: req.body.movie_id,
        },
        {
            $set: {
                avgRating: newRating,
                count: nowCount + 1,
            },
        }
    );
} else {
// 평균점수 데이터가 존재하지 않을 경우 새로 추가되는 리뷰의 점수를 평균점수로 추가
    let addRating = await db.collection("average_rating").insertOne({
        movie_id: req.body.movie_id,
        avgRating: Number(req.body.rating),
        count: 1,
    });
}

 

여기서는 aggregate를 사용하지 않고 평균을 계산하기 위해 count 항목이 추가되었습니다.

(aggregate를 활용하면 훨씬 쉽게 구현이 가능함)

현재 평균점수에 count를 곱한뒤 새로 추가되는 점수를 더하여 평균을 다시 계산하도록 하였습니다.

매번 리뷰데이터를 모두 불러와 평균을 계산하는 방식보다 DB의 부하가 적고 빠르지만 정확도가 떨어질 수 있습니다.

 

이 방식에도 몇가지 주의사항이 있습니다.

  • 리뷰가 삭제될경우 평균점수에 반영이 되지 않음. 삭제과정에도 평균점수를 재계산하도록 추가해야함.
  • 코드가 복잡해질 수 있음.
  • 사용자 경험 향상을 위해 api 응답 이후 실행되도록 해야함.
  • 응답 이후 실행되므로 서버측 알림이나 로그를 통해 에러를 관리해주어야 함.

 

4. 결론

앞서 소개한 3가지 방식의 특징과 장단점은 영화 리뷰의 평균을 계산하는 과정을 중점으로 작성되었습니다.

다른 값의 평균을 구하거나 aggregate의 특성을 활용해 필터링, 집계, 정렬 등의 기능을 사용할때는 각 상황의 특성을 고려하여 알맞는 방법을 선택해야 합니다.