협업 시 공통 소스 코드를 유지하는 것은 중요합니다. 팀원 모두가 사용하는 공통 소스 코드를 한명의 개발자가 본인의 개별적 목적을 위해 수정하는 것은 바람직하지 않죠. 실제로 이러한 상황은 저의 과거 직장에서 벌어지던 일이였습니다. 이를 해결하고자 했던 과정을 공유하고자 합니다.

문제점

당시 소스 리뷰는 원활하게 이뤄지지 않았으며, 개발자의 작업 내용은 Pull Request와 같은 검토단계를 거치지 않고 develop 브랜치로 직접 푸시되고 있었습니다. 어찌보면 빠르게 결과물을 만들어야 하는 외주회사의 특징이라고 할 수도 있지만, 주도적으로 리뷰를 할 수 있는 중간관리자 및 시니어 개발자의 부재 또한 한 몫을 했습니다.

일반적으로 공통 소스 코드는 모듈화하여 관리합니다. 현 직장에서는 git submodule 기능으로 각 프로젝트의 소스 코드와는 분리하여 관리하고 있습니다. 전 직장의 경우 사정이 달랐습니다. 프로젝트가 하나씩 들어올때마다 바로 전 프로젝트 소스를 그대로 복사해서 가져온 후 적당히 지울 부분들 지우고 작업하는 방식이였습니다. 또한 경력자 분들은 공통 소스 코드에 대해 ‘알아서 수정하라’고 말하기까지 했죠. 나중에는 유지가 안되어 목적을 알 수가 없는 코드가 난무하는 상황이었습니다.

원칙을 세우자

이에 저는 적어도 공통 소스 코드에 대한 원칙은 세우고 개발하자고 주장했습니다. 그 원칙은 다음과 같습니다.

  • 회사 단위 공통 소스 코드

    • 기능적 또는 성능적 결함이 있지 않는 이상 수정하지 않는다.
    • 새로운 기능을 추가하지 않는다.
    • 불가피하게 수정한 경우 회의때 언급하여 팀원들에게 컨펌을 받는다.
  • 프로젝트 단위 공통 소스 코드

    • 위의 공통 소스만으로 요구사항을 해결하기 어려운 경우 커스터마이징을 해서 사용한다.
    • 각 프로젝트 내에서 상황에 맞게 수정한다.

당시까지 작업된 소스를 보며 위 두 분류로 구분하여 디렉토리를 나눴습니다.

Git Hook 활용 (husky)

그럼에도 해결되지 않는 부분이 있었는데, 바로 실수로 커밋하는 경우입니다. 리뷰 단계가 없다보니 디버깅을 위해 짜놓은 코드를 안지우고 커밋해도 모르고 지나갈 수 있습니다. 이에 저는 npm 라이프러리 husky 4 버전을 통해 Git Hook을 제어하고자 했습니다.

소스 코드 수정사항이 원격 저장소로 반영되는 과정은 다음과 같습니다.

작업공간git add스테이지 영역git commit로컬 저장소git push원격 저장소

저는 git commit 단계와 git push 단계에서 수정되지 말아야 하는 소스가 포함된 경우 그 이벤트를 막는 로직을 설계했고, 결과물은 아래와 같습니다.

package.json:

  "husky": {
    "hooks": {
      "pre-commit": "node gitHook.js pre-commit",
      "pre-push": "node gitHook.js pre-push"
    }
  },

gitHook.js:

const { exec } = require("child_process");

// 커밋을 방지할 디렉토리
const PROTECTED_DIR = ["src/common", "environment"];

// 경로 형식을 '/' 로 통일
const dirFormat = (dir) => {
  return dir.replace(/^[\/+|\\+]/, "").replace(/\/+|\\+/g, "/");
};

// pre-commit을 인자로 받았을때 실행
if (process.argv.slice(-1)[0] === "pre-commit") {
  exec("git diff --name-only --cached", (error, stdout, stderr) => {
    if (error) throw error;
    const protectedFiles = [];
    stdout.split(/\s+/).forEach((fileName) => {
      if (PROTECTED_DIR.some((dir) => dirFormat(fileName).startsWith(dir)))
        protectedFiles.push(fileName);
    });

    if (protectedFiles.length) {
      console.error(
        "\x1b[31m%s\x1b[0m",
        "보호된 디렉토리에 수정이 감지되었습니다:\n\t" +
          protectedFiles.join("\n\t")
      );
      process.exit(1);
    }
  });
}

// pre-push를 인자로 받았을때 실행
if (process.argv.slice(-1)[0] === "pre-push") {
  exec("git rev-parse --abbrev-ref HEAD", (error, stdout, stderr) => {
    if (error) throw error;

    exec(
      `git diff --name-only HEAD origin/${stdout}`,
      (error, stdout, stderr) => {
        if (error) throw error;
        const protectedFiles = [];

        stdout.split(/\s+/).forEach((fileName) => {
          if (PROTECTED_DIR.some((dir) => dirFormat(fileName).startsWith(dir)))
            protectedFiles.push(fileName);
        });

        if (protectedFiles.length) {
          console.error(
            "\x1b[31m%s\x1b[0m",
            "보호된 디렉토리에 수정이 감지되었습니다:\n\t" +
              protectedFiles.join("\n\t")
          );
          process.exit(1);
        }
      }
    );
  });
}

결과

저는 이것이 완벽한 해결방안이었다고 생각하진 않습니다. 공통 소스 코드의 개선 사항이 프로젝트간에 직접적으로 공유되지 않는다는 점 등 여러 문제점이 여전히 존재하는 것은 사실이니까요. 개인적으로는 프로젝트의 초기 상태를 저장소에 따로 관리하는 방법 등의 여러 방안을 구상해봤지만 당시 실행에 옮기기에는 부담되는 면이 있었습니다. 그럼에도 임시방편으로 세워둔 이러한 원칙들은 가독성을 높이는데 꽤 효과가 있었다고 생각합니다.

업데이트:

댓글남기기