Typescript

[Typescript/MongoDB] Part 2. MongoDB: populate, virtual

cwchoiit 2023. 10. 3. 19:02
728x90
반응형
SMALL
728x90
반응형
SMALL

모든 데이터 조회 시 중요하게 생각할 것은 요청의 수를 줄일 수 있다면 최대한 줄이는 게 좋다.

다음 그림을 보자.

이런 구조를 가지고 있는 Blog Collection이 있다고 할 때, 블로그 전체를 조회하는 무식한 경우는 당연히 없겠지만 그런 경우가 있다고 가정해보고 전체 블로그를 가져오게 될 때, 만약 해당 블로그의 유저 정보와 커멘트들도 담아와야 한다면 블로그마다 유저와 커멘트들을 조회하는 비용이 발생한다.

 

아래 코드를 보자.

import { IBlog } from '../models/blog/blog';
import axios from 'axios';

const URI = 'http://localhost:3000';

const test = async () => {
  console.time('loading time: ');
  let {
    data: { blogs },
  } = await axios.get(`${URI}/blog`);

  blogs = await Promise.all(
    blogs.map(async (blog: IBlog) => {
      const {
        data: { user },
      } = await axios.get(`${URI}/users/${blog.user}`);
      const {
        data: { comments },
      } = await axios.get(`${URI}/blog/${blog._id}/comments`);

      blog.user = userRes.data.user;
      blog.comments = commentsRes.data.comments;
      return blog;
    })
  );
  console.log('blogs:', blogs[0]);
  console.timeEnd('loading time: ');
};

test();

코드의 내용은 대략 블로그 전체를 가져온 후 각 블로그마다 블로그의 유저 아이디를 가져와서 해당 아이디를 통해 유저와 커멘트를 가져온다. 이는 모든 블로그를 가져오는 요청을 한 번, 블로그를 가져오는 요청에서 처리하는 데이터베이스 조회, 조회된 데이터의 유저 및 커멘트를 가져오기 위한 요청 한 번, 각 요청에서 처리하는 데이터베이스 조회 등 상당히 많은 비용이 발생한다.

 

이것을 해결할 수 있는 방법 중 하나가 MongoDB의 'populate'이다.

populate은 특정 컬렉션의 도큐멘트에서 가져올 다른 객체가 있다면 (블로그의 유저와 커멘트) 해당 객체도 같이 조회하여 조회된 데이터를 추가해주는 방법이다.

 

코드는 다음과 같이 아주 간단히 작성할 수 있다.

// Get blogs
blogRouter.get('/', async (req, res) => {
  try {
    const blogs = await Blog.find().populate([
      { path: 'user' },
      { path: 'comments' },
    ]);
    return res.status(200).send({ blogs });
  } catch (e: any) {
    console.log(e);
    res.status(500).send({ error: e.message });
  }
});

위 코드처럼 원하는 콜렉션 정보를 populate으로 넣어주면 MongoDB는 해당 정보도 함께 가져와준다. 

그리고 이렇게 "블로그를 조회할 때 유저 정보와 커멘트들도 포함해서 가져와줘!" 라고 MongoDB에게 요청하면 위에 코드도 아래처럼 더 간결해진다.

import { IBlog } from '../models/blog/blog';
import axios from 'axios';

const URI = 'http://localhost:3000';

const test = async () => {
  console.time('loading time: ');
  let {
    data: { blogs },
  } = await axios.get(`${URI}/blog`);

  console.log('blogs:', blogs[0]);
  console.timeEnd('loading time: ');
};

test();

응답 속도도 데이터가 많아지면 많아질수록 꽤 의미있는 정도로 줄어들고 코드 또한 간단해지는 장점이 있다.

허나 여기서 한 가지 더 알고 넘어가야 할 게 있다. 'virtual'이다.

 

내 Blog Schema는 comments는 포함하지 않는다. 실제로 코드를 보면 다음과 같다.

// 블로그 스키마
const blogSchema = new Schema<IBlog>(
  {
    title: { type: String, required: true },
    content: { type: String, required: true },
    isLive: { type: Boolean, required: true, default: false },
    user: { type: Types.ObjectId, required: true, ref: 'user' },
  },
  { timestamps: true }
);

// 커멘트 스키마
const commentSchema = new Schema<IComment>(
  {
    content: { type: String, required: true },
    user: { type: Types.ObjectId, required: true, ref: 'user' },
    blog: { type: Types.ObjectId, required: true, ref: 'blog' },
  },
  { timestamps: true }
);

 

이게 나의 블로그 스키마인데 커멘트는 없다. 블로그에는 커멘트라는 데이터는 없고 커멘트에만 블로그라는 데이터를 참조한다. 그렇다면 당연히 blog 전체를 가져오는 find()에서 populate를 사용할 때 comments를 담을 수 없게 된다. 그럼 커멘트를 담고 싶을 때 방법은 두가지인데 첫번째는 스키마에 커멘트를 유저처럼 추가하거나, virtual을 사용하는 것이다. 난 후자를 선택했다.

 

blogSchema.virtual('comments', {
  ref: 'comment',
  localField: '_id',
  foreignField: 'blog',
});

blogSchema.set('toObject', { virtuals: true });
blogSchema.set('toJSON', { virtuals: true });

위처럼 virtual()을 사용하면 실제 데이터베이스에 해당 필드가 생성되는 건 아니고 가상의 데이터를 블로그 객체안에 추가해주는 것이다. 이렇게 하고 나면 블로그 전체를 가져오는 쿼리를 populate과 같이 수행할 때 커멘트 역시 가져올 수 있게 된다.

 

728x90
반응형
LIST