-
[TIL] 스파르타) Chapter 3 개인 과제 시작, MongoDB와 auto incrementTIL-sparta 2024. 5. 13. 22:00
> Chapter 3 아이템 시뮬레이터 Node 서버 개발 개인과제 발제에 따라 지급된 Node.js 강의들을 수강하고 프로젝트 개발을 시작헀다. 과제 진행 간에 발생한 문제를 정리해보았다.
학습 키워드: Node.js, express, mongoDB, mongoose, schema, auto increment
MongoDB 와 auto increment
1) 과제 요구사항:
과제 요구사항에 캐릭터의 생성 작업이 이루어지면 생성된 캐릭터의 character_id (이하 cid)를 response에 돌려줘야한다는 조건이 있다. 여기서 cid는 1부터 시작해서 1씩 증가하는 Number 값이다.
2-1) 발생한 문제 (index):
router.post("/characters", async (req, res, next) => { ... const equipment = new Equipment({}); await equipment.save(); // character_id: await Character.getNextNumber(), // or simply use Trigger from MongoDB Atlas const character = new Character({ name: name, equipments: equipment._id, }); // await equipment.save(); // #1 await character.save(); return res.status(200).json({ character_id: character.character_id }); ... }
위는 캐릭터를 담당하는 router의 캐릭터 Create 작업의 일부다. 앞(생략된 부분)에서 name 값의 validation이 끝나면 equipment와 character 모델을 각각 생성하고 저장해주는데, 처음에는 저장 시 equipment만 저장되고 character가 저장되지 않았다. 약간의 삽질 후 DB의 collection을 좀 조사해보니, 처음에 추가했던 document가 index를 자동으로 생성하는 바람에 equipment에서 더 이상 사용하지 않는 unique field 값에 문제가 생겨서 에러가 출력되던 것이었다.
2-2) 해결 방안:
자동 생성된 index를 모두 지우고 model schema에 { autoIndex: false } 를 추가해 자동 index 기능을 비활성화 했다.
AutoIndex는 DB에 들어오는 데이터의 타입이나 required 등의 속성을 체크해주는 index를 자동으로 생성해주는 기능이어서 개발 단계에서는 이를 비활성화 하기 보다 schema가 수정될 때 collection을 한 번 drop하고 index를 새로 생성하도록 하는 것이 좋다고 한다. 실제 서비스되고 있는 DB에서 사용되고있는 모델의 스키마가 수정되는 경우에는 MongoDB Atlas에서 Indexes를 수동으로 추가/제거/수정할 수도 있다.
Indexing & 비용: Indexing 기능은 검색을 빠르게 하기 위한 추가 설정인 만큼 리소스를 더 사용하기 때문에 추가 비용이 발생할 수 있다고 한다. 잘 못하면 요금 폭탄을 맞을 수 있다고 하니 실제 프로덕션에서는 index를 사용해야 할지, 한다면 어떤 항목에 적용해야 하는지를 면밀하게 검토하는 것이 좋겠다.
3-1) 발생한 문제 (counter):
MongoDB에는 field의 value를 1씩 증가시키는 기능이 자체적으로 구현되어있지 않다. 그래서 예전에 개발했던 Node 서버 프로젝트를 참고해 모델의 statics에 함수를 추가하여 findOne과 sort의 조합으로 가장 높은 cid를 가진 document에 1을 추가해 새로운 cid를 계산하려했다.
CharactersSchema.statics.getNextNumber = () => { const character = CharacterSchema.findOne({ character_id }).sort("-order").exec(); if (character) return character.character_id + 1; return 1; };
처음 작성했던 함수인데, findOne()을 찾을 수가 없는 문제가 있었다. 알고보니 참고했던 예전 프로젝트의 함수가 작동을 안하는 상태로 미사용 방치된 함수였다. findOne이 모델에 묶여있다는 사실은 깨달았는데 어떻게 수정해야할지 고민하다가 보류하고, 일단 강사분이 알려주신 아래의 4)번 해결 방안을 임시로 설정해두었다.
3-2) 해결 방안 1 (trigger & counter):
exports = async function(changeEvent) { var docId = changeEvent.fullDocument._id; const countercollection = context.services.get("ch3-item-simulator").db(changeEvent.ns.db).collection("counters"); const charactercollection = context.services.get("ch3-item-simulator").db(changeEvent.ns.db).collection(changeEvent.ns.coll); var counter = await countercollection.findOneAndUpdate({_id: changeEvent.ns },{ $inc: { seq_value: 1 }}, { returnNewDocument: true, upsert : true}); var updateRes = await charactercollection.updateOne({_id : docId},{ $set : {character_id : counter.seq_value}}); console.log(`Updated ${JSON.stringify(changeEvent.ns)} with counter ${counter.seq_value} result : ${JSON.stringify(updateRes)}`); };
MongoDB Atlas에 있는 Trigger 기능을 이용한 해결 방법(링크 참고)인데, DB의 특정 cluster > database > collection 으로 들어오는 insert 작업에 대해 trigger에 정의된 추가 작업을 해주는 방식이다. 여기서 추가 작업이란, counter라는 이름의 새로운 collection을 생성해 컬렉션 내 document를 하나 두고 값을 하나씩 증가시켜서 그 값을 다시 처음의 collection에 넣어주는 작업이다.
위 스크립트를 복사하는 경우 수정해야 할 부분은 const로 정의된 항목의 get() 안에 있는 문자열을 대상 cluster의 이름으로 바꿔줘야한다. 또한 가장 아래에 있는 var updateRes의 updateOne() 에서 두 번째 argument 속 $set: {} 에 정의된 키(character_id)의 명칭을 auto increment 작업을 수행해줄 field의 이름으로 바꿔줘야 한다.
해당 스크립트가 MongoDB 안에서 관리되기 때문에 추후 모델의 schema.js 파일을 재사용하게 되면 다시 한 번 trigger를 추가해주는 작업을 해줘야 해서 번거롭다는 점이 문제라면 문제다.
3-3) 해결 방안 2 (statics 함수):
// CharactersSchema ... CharactersSchema.statics.getNextNumber = async () => { const character = await Character.find().sort({ character_id: -1 }).limit(1).exec(); if (character[0]) return character[0].character_id + 1; return 1; }; const Character = mongoose.model("Characters", CharactersSchema); export default Character;
4) 의 방식대로 해결한 이 후에 다시 3) 의 문제로 돌아가서 살짝의 수정을 거쳤는데, 새로운 방식에서는 export를 수행하기 전에 Characters model을 미리 정의해주도록 했다. 이렇게 하면 findOne()을 호출할 수 있어서 처음에 의도한 작업을 수행할 수 있게 된다. 하지만 기존 처럼 findOne을 사용하면 가장 큰 값의 특정이 어려워서 find()를 사용하고 character_id로 역순 정렬 이후 limit(1)으로 하나만 받는 식으로 변경했다.
// characters.schema.js CharactersSchema.pre("save", async function (next) { if (this.isNew) { this.character_id = await Character.getNextNumber(); next(); } else { next(); } });
Schema의 statics에 정의된 함수는 Character.getNextNumber() 같은 형식으로 mongoose의 model을 통해 호출할 수 있다. CharacterSchema.pre에서 "save" 동작 (create나 update 등)이 일어날 때 해당 작업을 캐치하도록 위와 같이 정의해주고, this.isNew, 즉 새로운 document를 추가하는 작업의 경우에서 getNextNumber를 사용하여 새로운 character_id 값을 할당하도록 설정하면 된다.
임시로 두 가지 방법을 모두 설정해 두었는데, 무엇이 더 나은 방법인지 알아보고 하나로 통일하도록 해야겠다.
추가: mongoose-auto-increment 라는 패키지를 이용하는 방법도 있다.
--
REFERENCES:
> 게임 서버 개인 과제 spec
> 개인 프로젝트 Github Repository
> MongoDB, Auto-Increment (Trigger)
728x90'TIL-sparta' 카테고리의 다른 글
[TIL] 스파르타) Ch.3 개인 과제 - 3일 차 (Joi, git commit --amend) (2) 2024.05.15 [TIL] 스파르타) Chapter 3 개인 과제 진행 - 2 일차, git rebase (2) 2024.05.14 프로그래머스) 햄버거 만들기 풀이 (Java) (0) 2024.05.12 [TIL] 스파르타) Node.js 강의 수강 (0) 2024.05.11 [TIL] 스파르타) CS 강의 수강 (TCP/IP, HTTP, HTTPS) (0) 2024.05.10