MongoDB에서 ObjectId로 Document 연결 해야만 하는 이유

MongoDB에서 ObjectId로 Document 연결 해야만 하는 이유

MongoDB에서 Document를 연결할 때, 참조(Ref)를 위해 ObjectId를 사용하는 것이 좋은 이유에 대해 알아보겠습니다.

MongoDB에서 Document 연결

MongoDB에서 Document 간의 관계를 표현하는 방법은 크게 2가지가 있습니다.

  1. 내장된 문서 (Embedded Document)
  2. 참조된 문서 (Referenced Document)

참조를 사용하여 연결된 Document를 저장 할 때, _id 필드를 ObjectId로 저장할 것인지, 문자열로 저장할 것인지 선택할 수 있습니다. 하지만, ObjectId로 저장하는 것이 더 많은 이점을 가지고 있습니다.

ObjectId vs String

  1. 성능
    MongoDB는 기본적으로 _id 필드를 인덱싱합니다. ObjectId12바이트의 바이너리 데이터로 구성되어 있어, 인덱싱이 더 빠릅니다. 반면, 문자열은 크기가 더 크고, 비교 연산이 상대적으로 느립니다.

  2. 공간 효율성
    ObjectId는 12바이트로 고정되어 있습니다. 반면, 문자열은 길이가 가변적이기 때문에, 저장 공간을 더 많이 차지할 수 있습니다.

  3. Aggregate pipeline과의 호환성
    Mongoosepopulate 메소드는 참조된 document를 불러올 때, 참조 ID가 ObjectId문자열이든 상관없이 동작합니다. 그러나 MongoDBaggregate pipeline을 사용할 때, 문자열 ID를 사용하는 경우 추가적인 변환 작업이 필요합니다. 이는 성능 저하와 코드 복잡성을 초래할 수 있습니다.

Membership 모델

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import mongoose from "mongoose";

const membershipSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
role: {
type: String,
required: true,
},
});

const Membership = mongoose.model("Membership", membershipSchema);

Membership모델에서 User모델을 참조하고 있습니다.

1
2
3
4
5
6
7
8
9
10
Membership.create({
user: "60d4b1b3f3b9b3b4b4b4b4b4",
role: "admin",
})
.then((result) => {
console.log(result);
})
.catch((err) => {
console.error(err);
});

user 필드에 string를 사용하여 Membership 모델을 생성합니다.

Populate에서 String ID 사용의 경우

Populate 메소드를 사용할 때, 참조 ID가 ObjectId문자열이든 상관없이 동작합니다. 아래 코드는 Mongoosepopulate 메소드를 사용하여 ObjectId문자열 ID를 참조하는 예시입니다.

1
2
3
4
5
6
7
8
9
Membership.find()
.populate("user")
.exec((err, results) => {
if (err) {
console.error(err);
return;
}
console.log(results);
});

Membership 모델에서 user 필드를 참조하고 있습니다. populate 메소드를 사용하여 user 필드를 참조한 document를 불러옵니다. 이때, user 필드가 ObjectId문자열이든 상관없이 동작합니다.

Aggregate Pipeline에서의 문제

Aggregate Pipeline을 사용할 때, 문자열 ID를 사용하게되면 ObjectId로 변환해야 하는 경우가 자주 발생합니다. 이는 불필요한 변환 로직을 추가해야 하며, 코드의 복잡성과 유지보수 비용을 증가시킵니다. 아래 코드는 ObjectId로 변환하는 예시입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Membership.aggregate([
{
$addFields: {
// user 필드를 ObjectId로 변환하여 userObjId 필드로 저장
userObjId: {
$cond: {
if: { $eq: [{ $type: "$user" }, "objectId"] },
then: "$user",
else: { $toObjectId: "$user" },
},
},
},
},
{
$lookup: {
from: "users",
localField: "userObjId",
foreignField: "_id",
as: "user",
},
},
{
$project: {
userObjId: 0, // 결과에서 userObjId 필드 제외
},
},
]).exec((err, results) => {
if (err) {
console.error(err);
return;
}
console.log(results);
});

위 코드에서 user 필드가 ObjectId가 아닌 경우, userObjId라는 새로운 필드를 생성하여 ObjectId로 변환한 후, lookup 연산을 수행합니다. 이 과정은 코드의 가독성을 떨어뜨리며, 성능에도 영향을 미칠 수 있습니다. ObjectId로 저장하면 이러한 변환 과정이 필요 없으며, 코드가 간결해집니다.

Membership 저장 시 ObjectId 사용

1
2
3
4
5
6
7
8
9
10
Membership.create({
user: mongoose.Types.ObjectId("60d4b1b3f3b9b3b4b4b4b4b4"),
role: "admin",
})
.then((result) => {
console.log(result);
})
.catch((err) => {
console.error(err);
});

위 코드와 같이 Membership 모델에서 user 필드에 ObjectId를 사용하여 Membership 모델을 생성, 수정을 하여야 합니다.

결론

MongoDB에서 document를 연결할 때 ObjectId를 사용하는 것이 성능, 공간 효율성, 일관성 측면에서 더 나은 선택입니다. Node.jsMongoose를 사용하여 ObjectId를 참조로 사용하는 방법도 간단하고 직관적입니다. 특히, Aggregate Pipeline을 사용할 때 ObjectId를 사용하면 추가적인 변환 로직 없이 효율적으로 데이터를 처리할 수 있습니다. ObjectId를 사용하여 효율적이고 일관된 데이터 모델을 구축해 보세요.

공유하기