모든 데이터 조회 시 중요하게 생각할 것은 요청의 수를 줄일 수 있다면 최대한 줄이는 게 좋다.
다음 그림을 보자.
이런 구조를 가지고 있는 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과 같이 수행할 때 커멘트 역시 가져올 수 있게 된다.
'Typescript' 카테고리의 다른 글
[Typescript/MongoDB] Part 4. MongoDB: Indexing (0) | 2023.10.09 |
---|---|
[Typescript/MongoDB] Part 3. MongoDB: Nesting (0) | 2023.10.05 |
[Typescript/MongoDB] Part 1. Typescript with MongoDB (3) | 2023.09.30 |