반응형

NoSQL인 MongoDB는 Join의 개념이 없다!

하지만 분명 값을 참조해올 필요는 존재할 것이다.

 

본 포스팅에서는,

'User', 'Movie' 컬렉션과 'Review' 컬렉션이 존재할 때,

 

'Review' 컬렉션에 'movie'라는 field를 만들어 'Movie'의 값들을 전부 참조해오는 것과

'Review' 컬렉션에 'writer'라는 field를 만들어 'User'의 필드 중 'name'만 참조해오는 것을 구현해볼 것이다.

 


Collection 만들기

// Movie.ts
import mongoose from "mongoose";
import { MovieInfo } from "../interfaces/movie/MovieInfo";

const MovieSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true,
    },
    director: {
        type: String,
        required: true,
    },
    startDate: {
        type: Date,
    },
    thumbnail: {
        type: String,
    },
    story: {
        type: String,
    },
});

export default mongoose.model<MovieInfo & mongoose.Document>("Movie", MovieSchema);

Movie 컬렉션은 별다른 참조 필드 없이 작성했다.

 

import mongoose from "mongoose";
import { ReviewInfo } from "../interfaces/review/ReviewInfo";

const ReviewSchema = new mongoose.Schema({
    writer: {
        type: mongoose.Types.ObjectId,
        required: true,
        ref: "User",
    },
    movie: {
        type: mongoose.Types.ObjectId,
        required: true,
        ref: "Movie",
    },
    title: {
        type: String,
        required: true,
    },
    content: {
        type: String,
        required: true,
    }
});

export default mongoose.model<ReviewInfo & mongoose.Document>("Review", ReviewSchema);

Review 컬렉션은

writer 필드에서 User의 id를,

movie 필드에서 Movie의 id를 참조했다.

 

User는 따로 적진 않겠지만, 'name'이라는 필드를 가지고 있다.

 

컬렉션 생성에 필요한 interface 들은 아래와 같다.

// ReviewInfo.ts
import mongoose from "mongoose";

export interface ReviewInfo {
    writer: mongoose.Types.ObjectId;
    movie: mongoose.Types.ObjectId;
    title: string;
    content: string;
}

// MovieInfo.ts
export interface MovieInfo {
    title: string;
    director: string;
    startDate: Date;
    thumbnail: string;
    story: string;
}

 

 

Service에서 참조 로직 작성하기

Controller 등 다른 것들은 참조 여부와 큰 관련이 없다.

Service 로직에서 참조를 어떻게 구현하는지 살펴보자.

 

첫번째로 'updateReview'를 만들 것이다.

도입부에서 언급한대로, writer(User의 id 참조)를 통해 User의 name을 참조해오고,

movie(Movie의 id 참조)를 통해 Movie의 전체 필드를 가져올 것이다.

const updateReview = async (reviewId: string, reviewUpdateDto: ReviewUpdateDto): Promise<ReviewResponseDto | null> => {
    
    try {
        const review = await Review.findByIdAndUpdate(reviewId, reviewUpdateDto)
        .populate('writer', 'name').populate('movie');
        if (!review) {
            return null;
        }
        const data: any = {
            writer: review.writer,
            movie: review.movie,
            title: review.title,
            content: review.content
        }
        return data;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

.populate() 메서드를 통해 참조를 구현할 수 있다.

위에서 볼 수 있듯이,

특정 필드(name)만 참조해오려면 -> .populate('필드명', '참조해올 필드명')

전체를 참조해오려면 -> .populate('필드명')

과 같이 작성한다.

 

또한 받아온 정보를 사용하기 전에 가공 과정이 필요하다.

이때, TypeScript를 사용하고 있다면,

data에 any 타입을 지정해주는 것에 유의해야 한다.

 

참고로 반환 타입인 ReviewResponseDto는 아래 코드와 같다.

import { MovieInfo } from "../movie/MovieInfo";

export interface ReviewResponseDto {
    writer: string;
    title: string;
    content: string;
    movie: MovieInfo;
}

movie에 Moive 전체 필드를 가져올 것이므로, 타입을 MovieInfo로 지정해주었다.

 

 

두번째로는 'getReviews'를 만들어 보자.

'movie'가 특정 id값과 일치하는 모든 Review documents를 불러오는 로직이다.

 

이 또한, writer(User의 id 참조)를 통해 User의 name을 참조해오고,

movie(Movie의 id 참조)를 통해 Movie의 전체 필드를 가져올 것이다.

const getReviews = async (movieId: string): Promise<ReviewResponseDto[] | null> => {
    try {
         const reviews = await Review.find({
             movie: movieId
         }).populate('writer', 'name').populate('movie');
         if (reviews.length === 0) {
            return null;
        }

         const data = await Promise.all(reviews.map((review: any) => {           
             const result = {
                writer: review.writer.name,
                movie: review.movie,
                title: review.title,
                content: review.content
             };

             return result;
         }));

        return data;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

참조해온 방식은 위의 ' updateReview'와 동일하다.

 

여기서는 mongoose 내장 함수인 find를 활용해

Movie 컬렉션에서 movie가 받아온 movieId와 같은 모든 document의 정보를 가져오고

각각을 populate로 참조해서 적용한다.

 

한 가지 유의할 다른 점은, 참조해온 데이터 가공 방식이다.

Promise.all()과 map을 활용하는데,

map이 여러개의 Promise를 반환하기 때문에 await Promise.all()을 활용해

await 병렬 구조로 실행한다.

 

Promise.all()을 쓰지 않아도 동일한 결과를 나타내지만(적어도 저 코드에서는 그렇더라는..),

성능을 위해 Promise.all()을 사용하는 것 같다.

 

 

아래는 위에서 작성한 로직(getReviews)의 Response 예시이다.

{
    "status": 200,
    "success": true,
    "message": "리뷰 조회 성공",
    "data": [
        {
            "writer": "test1",
            "movie": {
                "_id": "627b4f23f22c32b2fcddc951",
                "title": "닥터 스트레인지",
                "director": "이우진",
                "startDate": "2022-05-01T15:00:00.000Z",
                "thumbnail": "https://example-sopt.s3.ap-northeast-2.amazonaws.com/%E1%84%80%E1%85%A7%E1%86%BC%E1%84%87%E1%85%A9%E1%86%A8%E1%84%80%E1%85%AE%E1%86%BC.jpeg",
                "story": "재밌는 영화입니다."
            },
            "title": "첫번째 리뷰",
            "content": "재밌는 영화였습니다."
        },
        {
            "writer": "test1",
            "movie": {
                "_id": "627b4f23f22c32b2fcddc951",
                "title": "닥터 스트레인지",
                "director": "이우진",
                "startDate": "2022-05-01T15:00:00.000Z",
                "thumbnail": "https://example-sopt.s3.ap-northeast-2.amazonaws.com/%E1%84%80%E1%85%A7%E1%86%BC%E1%84%87%E1%85%A9%E1%86%A8%E1%84%80%E1%85%AE%E1%86%BC.jpeg",
                "story": "재밌는 영화입니다."
            },
            "title": "두번째 리뷰",
            "content": "재미 하나도 없는 영화였습니다."
        }
    ]
}

'movie'가 '627b4f23f22c32b2fcddc951' 인 모든 Review documents를 불러오고,

'movie'는 Movie document의 모든 필드를,

'writer'는 User document의 'name' 필드를 가져온다.

 

실제 Review에는 아래와 같이 저장되어 있다.

(tmi : __v 는 버전을 나타내는 versionKey 필드인데, mongoose를 통해 데이터를 집어넣으면 생긴다고 한다. 궁금해서 방금 쳐봤다.)

 

 

이렇게 MongoDB에서 데이터를 참조하는 것을 구현해보았다.

 

 

 

 

 

 

 

반응형

+ Recent posts