ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TIL] 스파르타) Chapter 3 개인 과제 진행 - 2 일차, git rebase
    TIL-sparta 2024. 5. 14. 21:15

     

     > 스파르타 Chapter 3 아이템 시뮬레이터 서버 개발 과제를 진행하면서 발생한 문제 및 git rebase에 대해 새롭게 알게된 사실 등을 정리해보았다.

     

    학습 키워드: Node.js, express, javascript, regex, MongoDB, mongoose, populate, git, rebase

     

    1. Mongoose populate, ref, $in

    1) What is it?:

     MongoDB의 ODM(Object-Document Mapper)인 mongoose에서 지원하는 기능으로, document의 field 중 ref 값에 적힌 collection 이름을 통해 해당 document를 불러와 채우는 기능이다. 이 기능을 모른 채로 스크립트를 우선 작성했는데, 적용 전 후로 어떻게 달라지는지 실제 프로젝트에 사용한 스크립트를 통해 알아보자.

     

     

    2) Trial 1:

    // characters.schema.js
    
    ...
    
    CharactersSchema.statics.getEquippedArray = async (character_id) => {
      const character = await Character.findOne({ character_id: character_id }).exec();
      if (!character) return null;
      const items = await Item.find({ _id: { $in: [character.equipped] } });
      // console.log(items);
      return items;
    };

     첫 번째 스크립트는 Characters schema에 추가한 statics 함수로, character_id를 인자로 받아서 character를 찾고, 찾은 캐릭터의 장비 목록인 equipments array의 모든 항목을 $in operator를 사용해 Item.find()로 해당 Item들을 모두 찾아오도록 작성했다.

     

     

    3) Trial 2:

    // characters.route.js
    
    ...
    
    const character_id = req.params.character_id;
    const character = await Character.findOne({
      character_id: character_id,
    }).exec();
    if (!character)
      return res.status(404).json({
        errorMessage: `Character with character_id: ${character_id} does not exist.`,
      });
    
    const totalHealth = character.health;
    const totalPower = character.power;
    const equipment = await Equipment.findOne({
      _id: character.equipments,
    });
    
    if (equipment.equipped) {
      const item = await Item.findOne({ _id: equipment.equipped });
      totalHealth += item.health;
      totalPower += item.power;
    }
    
    return res.status(200).json({
      name: character.name,
      health: totalHealth,
      power: totalPower,
    });

    두 번째 스크립트는 character router에서 CRUD의 R, get method에 해당하는 부분이다. 이 스크립트 작성 당시에는 Characters 모델 schema가 Equipments schema를, Equipments 가 Items 를 ref 하도록 설정했었다. 그래서 character를 우선 받아오고, 받은 캐릭터에서 equipments의 _id 값을 통해 equipment를 다시 받아오고, 거기서 또 item의 _id (작성 당시 한 개의 장비만 장착할 수 있었음)를 찾아 findOne() 으로 Item을 받아오는 식으로 작성했다.

     

     

    4) Problem & Solution:

     2)와 3) 모두 작동은 하지만 문제가 있는데, 쿼리를 여러번 반복하게 되면 DB와의 통신을 여러번 반복해야해서 비용 측면에서 매우 비효율적이고, 시간 또한 오래 걸릴 수 있다. 그래서 대안을 찾아보던 중 populate라는 것이 있다는 것을 알게됐다.

    // characters.schema.js
    
    const CharactersSchema = new mongoose.Schema(
      {
        
        ...
        
        equipped: [
          {
            type: mongoose.Schema.Types.ObjectId,
            ref: "Items",
          },
        ],
      },
      { autoIndex: false },
    );
    // characters.route.js
    
    ...
    
    const character = await Character.findOne({
      character_id: character_id,
    }).populate("equipped"); // #1
    
    if (!character) {
      msg = `Character with character_id: ${character_id} does not exist.`;
      return res.status(404).json({ errorMessage: msg });
    }
    
    let totalHealth = character.health;
    let totalPower = character.power;
    const equipped = character.equipped; // populated?
    equipped.forEach((item) => { // #2
      totalHealth += item.health;
      totalPower += item.power;
    });
    
    return res.status(200).json({
      message: msg,
      data: {
        name: character.name,
        health: totalHealth,
        power: totalPower,
      },
    });

     이 기능을 알기 전 까지는 ref라는게 별로 쓸모 없는 항목 같다고 생각했는데, 단지 내가 정확히 알지 못하고 썼기 때문이었고, populate와 같이 쓰게 되면서 정확한 용도를 알게되었다. 우선 첫 번째 스크립트는 Characters 모델 schema의 일부인데, equipped가 array로 정의되어있고, 각 항목들이 ObjectId 타입을 가지며, ref 값이 'Items" 로 설정되어 있다. Items는 itemsSchema의 모델 명이자 컬렉션 명이다.

     

     이어서 두 번째 스크립트의 #1 을 보면, findOne() 끝에 .populate("equipped") 가 추가되었는데, populate 안에 들어간 parameter "equipped" 가 Items ref 값을 가진 field의 이름이다. 이렇게 지정해주면 character document를 받아오면서 해당 field에 있는 _id 값을 내부 쿼리를 통해 Items collection에서 대조해 매치되는 document를 같이 불러와 추가해준다. 단일 항목에서 사용할 수도 있고, 여기서처럼 Array 안에 들어간 여러 개의 항목에 대해서도 똑같이 작동한다. ref로 불러온 document의 ref에 속한 document 또한 불러오도록 설정할 수 있다. 자세한 사항은 공식 문서를 참고하자.

     


    5) Issue regarding assignment specification:

     과제의 도전 요구 사항을 진행하던 중 알게된 사실인데, 과제 spec에서는 캐릭터가 아이템을 장착하거나 탈착할 때 캐릭터의 스탯을 직접적으로 수정하라고 되어있다. 앞서 보다시피, 만들어둔 스크립트는 클라이언트의 get 요청으로 캐릭터의 stat을 조회하게 될 때 아이템의 값을 합산하도록 작성했는데, 왜냐하면 Item의 능력치 값이 put method로 유동적으로 변할 수 있어서 장비 착용 시 마다 캐릭터 스탯을 수정해버리면 item의 스탯 수정 이후의 대처가 힘들어지기 때문이다.

     

     그래서 수정을 해야 할지 말지를 강사분께 문의했는데, 결론부터 말하면 요구 사항에 적힌대로 캐릭터의 스탯을 다이렉트로 변동시키는 방향으로 스크립트를 수정하기로 했다. 이유는 우선 아이템 스탯 수정에 대한 부분은 맞는 이야기지만, 실제 시뮬레이션 서버라고 가정했을 때 애초에 서버에서 스탯 값을 계산하는 방식은 서버의 부하를 늘리기 때문에 보통은 값들을 그대로 클라이언트에 보내서 처리하게 한다고 말씀해주셨다. 따라서 과제 spec의 방법에 약간의 문제가 있는 것은 맞지만, 어차피 끝까지 가면 둘 다 수정해야 한다는 사실은 똑같기 때문에 일단은 요구 조건에 맞추는 것으로 방향을 잡았다.

     

    2. git rebase

    1) What is it?:

     분기된 branch의 commit이 뒤쳐져 있는 상태에서 base가 되는 branch의 변경점을 해당 branch에 반영하고 싶을 때 사용한다.

     

     

    2) How does it work?:

     대상이 되는 base branch의 최신 변경점 commit 기록의 뒤에 현재 branch의 작업 내용을 이어붙여 기록된 분기 지점을 바꿔주는 방식이다.

    git checkout [workingBranch]
    git rebase [baseBranch]

     방법은 rebase를 진행할 branch로 이동하고 git rebase 뒤에 base가 되는 branch의 이름을 적은 뒤 입력하면 된다. 예를 들어, develop에서 분기한 branch의 이름이 feat/some-feature라면, feat/some-feature branch로 이동하여 git rebase develop 이라고 입력하면 된다.

     

     만약 이렇게 rebase가 완료된 branch를 다시 develop에 merge할 때, rebase와 merge 사이에 develop의 commit에 변화가 없었을 경우에 한하여, develop이 merge에 대한 commit을 새로 작성하지 않고 feat/some-feature의 가장 최신 commit으로 HEAD의 위치를 fast-forward한다.

     


    3) Merge conflict:

    git add [filesWithConflictsResolved]
    git rebase --continue

     간혹 rebase 진행 과정에서 base banch의 commit과 작업 중인 branch의 commit에서 수정 항목이 겹쳐 merge conflict가 발생하기도 한다. 이럴 때는 우선 각각의 파일에서 충돌 해결 후 해당 파일들을 stage 하여 충돌 해결을 마킹해주고, git rebase --continue 를 입력하여 rebase 작업을 재개해준다.

     

     여기서 주의할 점은, staging 이후 절대로 commit을 해서는 안된다는 것이다. 이전 스파르타 팀 프로젝트들을 진행하면서 rebase를 여러 번 사용했는데, merge conflict가 생길 때 마다 add 및 commit을 했었다. 그런데 이후 부터 push 및 merge를 할 때 마다 새로 작업한 내용이 이전 작업 내용으로 덮어씌워지는 이상한 현상이 있었다. 당시에는 원인이 rebase라고만 막연하게 의심하면서도 정확한 이유를 알지 못했는데, 조사해보니 rebase 작업이 완전히 완료되지 않은 채로 commit을 통해 종료하게 되면 이와 같이 commit의 순서가 꼬이는 현상이 발생할 수 있다고 한다.

     

     

    4) push rejected:

     또한, rebase 이후에 push를 하면 rejected 메세지가 출력되는 경우가 있는데, 이는 rebase 이전에 push된 branch가 local branch의 rebase 이후 commit 로그에 변동이 생겨서 remote보다 뒤쳐져 있는 것으로 인식되기 때문(이유가 정확하지는 않으나 뒤쳐진 것으로 인식되기는 한다)이다. 해당 branch가 개인용으로 혼자 사용하는 branch인 경우 git push origin --delete [branchName] 으로 해당 branch 제거 후 다시 push하는 식으로 해결하면 된다.

     

     여러 사람이 같이 사용하는 branch인 경우 rebase를 사용하지 않는 것이 최선이지만, 만약 필요한 경우 rebase를 진행하기 전에 모든 contributor들이 commit & push 한 뒤, 다시 한 명의 contributor가 pull 및 rebase 이후 remote를 지운 다음 push하고, 마지막으로 다른 협업자가 자신의 local branch를 삭제 및 재생성하고 pull 하는 방식으로 진행하여 누군가의 작업 내역이 손실되거나 커밋이 꼬이는 일이 없도록 하자.

     


    5) Why use it?:

     Git pull이나 merge를 이용하여 합치면 편리할텐데 왜 번거롭게 rebase를 사용하는가 하면, 그 이유는 pull이나 merge와 다르게 rebase는 commit 기록을 남기지 않으므로, 작업중이던 branch의 commit history에 불필요한 내용을 섞지 않고 원래의 상태 그대로 깔끔하게 관리할 수 있기 때문이다. 까다롭다고 피하지 말고 상황이 맞으면 반드시 사용하도록 하자.

     

     

    --

     

    REFERENCES:

     

     

    Node.js 게임서버 입문 주차 개인 과제 | Notion

    함께 학습을 진행할 팀을 확인해보세요!

    teamsparta.notion.site

     > Chapter 3 개인 과제 spec

     

    GitHub - donkim1212/ch3-item-simulator: Chapter3 개인 과제

    Chapter3 개인 과제. Contribute to donkim1212/ch3-item-simulator development by creating an account on GitHub.

    github.com

     > 개인 과제 Github Repository

     

    Git push rejected after feature branch rebase

    OK, I thought this was a simple git scenario, what am I missing? I have a master branch and a feature branch. I do some work on master, some on feature, and then some more on master. I end up with

    stackoverflow.com

     > stackoverflow.com, "Git push rejected after..."

     

    Why git rebase overwrite my local changes? How to avoid overwrite?

    I'm trying to rebase my branch with the master. But it is always overwrite my local changes after rebase. I'm using the command git rebase and it will show one file conflict, then I manually reso...

    stackoverflow.com

     > stackoverflow.com, "Why git rebase overwrite..."

     

    Git - Rebasing

    Ahh, but the bliss of rebasing isn’t without its drawbacks, which can be summed up in a single line: If you follow that guideline, you’ll be fine. If you don’t, people will hate you, and you’ll be scorned by friends and family. When you rebase stuf

    git-scm.com

     > git-scm.com, "Git - Rebasing"

     

    Mongoose v8.3.4: Query Population

    Populate MongoDB has the join-like $lookup aggregation operator in versions >= 3.2. Mongoose has a more powerful alternative called populate(), which lets you reference documents in other collections. Population is the process of automatically replacing th

    mongoosejs.com

     > mongoosejs.com, Query Population

    728x90
Designed by Tistory.