며칠 전에 백엔드를 개발하면서 해괴한 현상을 발견했다. 분명히 mongoose 스키마 상에서는 똑같은 ObjectId
타입의 필드인데, 어떤 놈은 string
으로 저장되고, 다른 놈은 ObjectId
로 DB에 저장되고 있었다. 코드 상으로는 도저히 원인도 모르겠고, 구글에 쳐봐도 나오지 않아 꼬박 하루를 원인을 찾느라 보냈다. 아직도 정확한 원인은 알아내지 못했지만, 간단한 문제상황 및 workaround 정도는 여기에 적어도 될 것이다.
문제가 되는 상황을 요약하면 다음과 같다.
mongoose 스키마에서 nested field로 지정된 필드가 ObjectId 필드인 경우, mongoose가 hex string을 ObjectId로 자동 변환해주지 않는다.
mongoose는 기본적으로 스키마 상에서 ObjectId
로 지정이 되어있으면, MongoDB에서 ID로 인식하는 hex string이 들어온다면 이를 자동으로 ObjectId
타입으로 변환하여 DB에 저장해준다(참고). 그런데 ObjectId
타입의 필드가 nested field일 경우, ObjectId
타입의 데이터가 들어오면 문제가 없지만 hex string이 들어온다면 이 자동 변환이 작동하지 않고 있었다.
{
foo: {
bar: "572bb8222b288919b68abf5b"
},
foobar: ObjectId("572bb8222b288919b68abf5b")
}
분명히"572bb8222b288919b68abf5b"
hex string을 넣어줬건만, foobar
에는 ObjectId
로 잘 변환되어 들어가고 있던 반면 foo.bar
에는 그대로 raw string으로 저장되고 있던 것이다. 이렇게 다르게 저장되면 클라이언트에서 읽어올 때 다른 타입으로 읽게 되기 때문에 추적하기 어려운 이슈가 발생하기 쉽다.
이 현상이 프로젝트 내의 설정때문인지 알아보기 위해 로컬에서 간단하게 MongoDB 및 mongoose 서버를 셋업해보았다.
import mongoose from 'mongoose'
const hexStr = '572bb8222b288919b68abf5b'
const { connect, disconnect, Schema, model } = mongoose
async function main() {
await connect('mongodb://127.0.0.1:27017/test')
const testSchema = new Schema({
foo: {
type: {
bar: {
type: Schema.Types.ObjectId,
required: true
}
},
required: true
},
foobar: {
type: Schema.Types.ObjectId,
required: true
},
})
const Test = model('Test', testSchema)
// 똑같은 hexStr 문자열을 넣어줌
await Test.create({
foo: {
bar: hexStr
},
foobar: hexStr
})
await disconnect()
}
main().catch((err) => console.log(err))
위에서 언급한 대로 nested field인 foo.bar
와 일반 필드인 foobar
모두 ObjectId
타입으로 지정되어 있다. 위 코드를 실행하면 DB에 foo.bar
와 foobar
를 필드로 하는 document가 생성되게 되고 종료될 것이다.
Mongoose 옵션에 nested field와 관련한 설정을 해주지 않았기 때문에 발생하는 현상이라고 반쯤은 미리 예상하고 있었기 때문에, 당연히 같은 현상이 재발할 줄 알았다. 그런데 이렇게 재현한 결과 nested field에도 똑같이 ObjectId
타입으로 데이터가 잘 들어가고 있었다.
{
"_id": "ObjectId("624ee7997c9b7ed5a9366234")",
"foo": {
"bar": "ObjectId("572bb8222b288919b68abf5b")",
"_id": "ObjectId("624ee7997c9b7ed5a9366235")"
},
"foobar": "ObjectId("572bb8222b288919b68abf5b")",
"__v": 0
}
이게 옵션 차이가 아니라면 뭐가 다른거지? 하고 생각을 해보니, 설치한 mongoose 버전의 차이일 수 있다는 생각이 들었다. 위의 테스트 코드의 mongoose 버전은 6.2.10
버전이었고, 프로젝트 내의 mongoose 버전은 5.9.3
버전이었기 때문이다. 이에 mongoose 버전을 낮추고 다시 코드를 실행해보았다.
{
"_id" : ObjectId("624ee91a03b8168ef8edbc34"),
"foo" : {
"bar" : "572bb8222b288919b68abf5b"
},
"foobar" : ObjectId("572bb8222b288919b68abf5b"),
"__v" : 0
}
아... 결국 mongoose 버전에 따라 다르게 동작하고 있던 것이다. 이게 mongoose@5.9.3
만의 이슈인지는 알 수 없지만 설정과 관련된 이슈는 아닌 것 같아 프로젝트의 mongoose 버전을 올리기 전까지는 어쩔 수 없이 안고 가야 하는 문제가 되어버렸다.
모종의 사유로 mongoose를 업데이트하기 어렵거나 공수가 클 경우, 아래와 같이 따로 hex string을 wrapping하는 함수를 정의하여 사용할 수 있을 것이다.
import mongoose from 'mongoose'
function convertToObjectId(id: string | mongoose.Types.ObjectId) {
if (typeof id === 'string') {
return mongoose.Types.ObjectId(id)
}
return id
}
// 새 document 생성 시 ObjectId를 wrapping
await Test.create({
foo: {
bar: convertToObjectId(hexStr)
},
foobar: convertToObjectId(hexStr)
})