Monorepo 도입 배경
SaaS를 만들어 고객에게 제공하는 과정에서 한 개로 시작한 컴포넌트가 필요에 따라 여러 개로 나누어져 관리되는 경우를 흔히 볼 수 있습니다. 이렇게 나누어진 컴포넌트 사이에는 연관관계가 생기고, 때로는 하나의 컴포넌트가 다른 컴포넌트에 의존성이 생기기도 합니다. AB180의 Airbridge API 개발팀에서도 다양한 컴포넌트를 다루고 있고, 이 과정에서 겪었던 몇 가지 문제점을 Monorepo를 통해 해결한 경험을 소개하고자 합니다.
Monorepo란?
Monorepo는 하나의 저장소에서 여러 프로젝트를 관리하는 것을 의미합니다. 각각의 프로젝트들은 기존에 사용하던 환경처럼 각각 독립된 개발 환경을 가질 수 있습니다. 다시 말해, 같은 저장소에 속해있는 프로젝트들이 동일한 개발환경을 구성하고 있어야 하는 것은 아닙니다.
도입 배경
앞서 말씀드린 것처럼 Airbridge API 개발팀에서 다양한 컴포넌트를 관리하고 있습니다. 각 컴포넌트는 독립된 저장소를 가지고 있습니다. 이 중 api, dashboard, utils 컴포넌트를 예시로 들어보겠습니다.
utils 컴포넌트는 다른 여러 컴포넌트에서 사용되는 모델, 로직 등의 코드를 공통으로 사용하고자 만들어둔 패키지 형태의 컴포넌트 입니다. 해당 컴포넌트는 api, dashboard 컴포넌트에서 사용되고 있습니다.
utils 컴포넌트에 있는 코드를 업데이트하고 api, dashboard에 반영하기 위해서는 다음과 같은 과정을 밟아야 했습니다. 먼저 utils 컴포넌트에서 발생한 변경점을 새 브랜치에 commit하고 utils 저장소에 push하고 pr을 만든 뒤 master에 merge하고, github에서 새로운 버전을 release합니다. api와 dashboard 컴포넌트에서 새로운 버전의 utils를 반영하고, 발생한 변경점을 새 브랜치에 commit하고 각 저장소에 push하고 pr을 만든 뒤 master에 merge합니다. 만약 추가적으로 utils에 변경이 필요하다면 보기만 해도 복잡한 이 과정을 다시 한 번 밟아야합니다.
작업 목표
기존의 개발 패턴을 해치지 않으면서 위에서 보신 것처럼 복잡한 업데이트 과정을 단순화하여 생산성을 높이고, Airbridge API 팀에서 사용하는 여러 컴포넌트를 한 번에 관리할 수 있게 가시성을 높이고자 했습니다. 그리고 utils처럼 다른 컴포넌트에 영향을 줄 수 있는 경우, 연관된 컴포넌트까지 일관성을 유지할 수 있게 테스트와 배포까지 잘 수행되는 것을 목표로 삼았습니다.
저장소 합치기
Airbridge API 팀에서 사용하는 컴포넌트를 api 저장소로 합치는 작업을 진행했습니다. 루트 디렉터리를 기준으로 프로젝트를 구분하는 형태로 Monorepo 를 구성했습니다. 루트 디렉터리에 api, dashboard, utils 폴더를 만들고 각 컴포넌트의 소스를 옮겼습니다. 패키지 형태로 사용하던 utils 컴포넌트의 경우에는 api, dashboard에서 로컬 패키지 형태로 사용하도록 수정했습니다.
CI/CD 연결
각 컴포넌트는 Github Webhook을 이용하여 Codebuild와 연동되어 있고, PR 생성 시 CI, Merge시 CD가 이루어지는 자동화된 CI/CD가 구성되어 있고, PR에서 CI의 결과를 직접 확인할 수 있습니다.
Monorepo가 적용된 환경에서도 각 컴포넌트의 CI/CD는 기존처럼 별개로 동작하는 것을 목표로 했기에, 각 저장소에 연결되어 있던 Github Webhook들을 Monorepo인 api 저장소로 이동시켰습니다.
api 저장소에 연결된 컴포넌트 CI/CD용 Webhook
git diff 정보를 이용하여 해당 컴포넌트의 변경점이 있거나 연관된 컴포넌트의 변경점이 발생한 경우에만 CI/CD를 수행할 수 있도록 각 컴포넌트의 Codebuild 스크립트를 변경합니다. 이렇게 하면 api 저장소에 PR이 생성될 경우 CI/CD가 실행되지만, 변경점이 컴포넌트의 없는 경우에는 실제 CI/CD 과정을 밟진 않습니다.
api 컴포넌트의 경우 api, utils 의 변경점이 있을 경우 CI/CD 를 수행하게 함
Monorepo 를 적용한 상태에서 utils 컴포넌트를 업데이트 하는 과정을 살펴보겠습니다. 로컬 패키지를 사용하고 있기에 utils 컴포넌트의 코드를 변경하고 새 브랜치를 만들고 api 저장소에 utils, api 변경점을 한 번에 commit, push, pr, merge 하는 과정을 밟으면 업데이트 과정이 완료됩니다. Multirepo를 사용할 때 보다 훨씬 간단해진 것을 체감할 수 있습니다.
작업 완료 이후 발견한 문제
각 컴포넌트의 CI/CD 수행 여부를 CI/CD 과정에서 직접 판단하도록 설계하면서 발생한 몇 가지 문제를 먼저 살펴보겠습니다.
1. 복잡해진 CI/CD 스크립트
CI/CD의 필요 여부를 판단하고 이에 따라 분기하는 로직이 추가되어 Codebuild 스크립트가 복잡해졌고 본연의 역할인 CI/CD를 넘어선 더 많은 역할을 책임지게 되었습니다. 그리고 컴포넌트의 연관관계를 변경될 때도 Codebuild 스크립트를 변경해야 합니다.
2. CI/CD 성공 처리 문제
생략된 CI/CD의 경우에도 성공으로 처리되어 종료됩니다. 정상적으로 수행되어 성공으로 처리된 것인지, 생략하여 성공으로 처리된 것인지를 판단할 수 없었습니다.
3. 같은 코드 재배포 불가
모종의 이유로 특정 컴포넌트의 재배포가 필요한 경우가 있습니다. 하지만 같은 코드를 배포할 경우 이미 동일한 코드가 배포된 상태이므로 변경점이 없다고 판단하여 CD를 수행할 수 없습니다.
4. 관리포인트의 증가
api 저장소에 새로운 컴포넌트가 추가로 합치거나 기존에 있는 컴포넌트를 제거할 때 CI/CD용 Github Webhook을 추가해줘야 한다는 것도 신경써야 했습니다.
해결하기 위해서
앞서 말씀드린 문제점을 근본적으로 해결하기 위해서는 CI/CD Codebuild가 실행되기 전에 각 컴포넌트의 CI/CD 수행 여부를 판단할 수 있는 무언가가 필요했습니다.
Code Deployer의 탄생
Code Deployer라고 이름을 지은 새로운 컴포넌트를 만들어서 이를 통하여 CI/CD가 수행될 수 있도록 과정을 개선했습니다. Code Deployer는 PR이 생성되거나 PR의 상태가 변화될 때 Github Webhook을 통해 Trigger 됩니다. PR의 브랜치와 master 브랜치 사이의 코드 변경사항을 추적하여 각 컴포넌트의 CI/CD가 필요한지 판단하고, 필요시 CI/CD를 실행시켜주는 역할을 합니다. Code Deployer의 도입으로 앞서 언급했던 문제를 해결할 수 있었습니다.
1. 복잡해진 CI/CD 스크립트 해결
각 컴포넌트의 Codebuild 스크립트에서 CI/CD 수행 여부를 판단하기 위해 추가했던 로직을 제거했습니다. 또한 컴포넌트의 연관관계를 지정할 수 있는 project_diff.txt 파일을 각 컴포넌트에 추가하여 관리할수 있게 함으로써 연관관계 설정을 위해 스크립트를 변경해야 하던 것을 해결했습니다.
2. CI/CD 성공 처리 문제 해결
Code Deployer가 각 컴포넌트의 CI/CD 필요 여부를 불필요한 CI/CD는 수행하지 않게 됨으로써 실제로 성공한 CI/CD 결과만 기록될수 있게 되었습니다.
3. 같은 코드 재배포 불가 해결
CI/CD 과정에서 배포가 필요한지 판단하는 로직을 제거함으로써 자연스럽게 이 문제도 같이 해결되었습니다. 만약 재배포가 필요하다면 CD용 Codebuild를 Trigger하면 같은 코드라고 하더라도 정상적으로 배포가 이루어집니다.
4. 관리 포인트의 증가 해결
Codebuild와 직접 연결되어있던 Github Webhook을 모두 제거하고, Code Deployer와 연결되는 Github Webhook을 추가했습니다. Code Deployer가 저장소 내의 모든 컴포넌트에 대한 CI/CD 실행을 담당하게 되면서 컴포넌트의 추가/제거 시 Github Webhook에 대해 신경을 써줘야 하던 부분도 해결됐습니다.
조금 더 잘하기 위해서
PR에서 CI 상태 확인
Code Deployer가 Codebuild를 실행시키도록 변경되더라도 기존처럼 각 컴포넌트의 CI 결과를 PR에서 확인할 수 있도록 하기 위해 Github Checks API를 활용하여 구현했습니다.
생략된 CI도 확인 가능
변경점이 없는 컴포넌트의 경우 CI/CD를 진행할 필요가 없으므로 Skip 하도록 했고, 이에 대한 가시성을 확보했습니다.
Slack에서 CI/CD 상태 확인
기존의 Codebuild 알림에서는 PR이나 CI 진행 상태가 한정적이었기에 이를 의미있게 활용하기 어려웠습니다.
누가 무엇을 배포한 것인지 PR 정보를 알 수 있게 추가하고, 작업자가 CI 완료 알림을 받을 수 있게 함으로써 알림의 가시성도 챙겼습니다.
또한 작업이 프로덕션에 반영된 경우 따로 알림을 보내주도록 했습니다.
이러한 과정을 거치면서 개선하고자 했던 목표를 달성했고, 생산성 향상을 위한 추가 기능 구현까지 완성하며 Monorepo 도입 작업을 완료할 수 있었습니다.
Monorepo 도입 이후 장단점
좋았던 점
Airbridge API 팀에서 관리하는 여러 컴포넌트를 하나의 저장소에서 한 눈에 확인할 수 있게 되었습니다. 여러 컴포넌트에 해당하는 티켓을 작업할 때 관련된 변경점을 하나의 PR에서 확인할 수 있게 되었습니다. utils와 같은 패키지 형태의 컴포넌트도 하나의 저장소에서 관리할 수 있게 되어 생산성을 향상시킬 수 있게 되었습니다.
아쉬웠던 점
컴포넌트별로 코드 접근 권한을 제어할 수 없다는 것과, 로컬 패키지로 사용하는 컴포넌트들을 각각 다른 버전으로 사용할 수 없다는 것은 아쉬운 점인 듯합니다.
마치며
다양한 컴포넌트를 다루는 개발팀에서 Monorepo 도입을 고려할 때 이 글이 도움이 되기를 기대해봅니다.
Airbridge API 팀은 Monorepo 도입과 같이 팀원들의 생산성 향상과 보다 나은 개발환경을 끊임없이 고민하고 있습니다.
ᴡʀɪᴛᴇʀ
Juho Choi @juho
Data Serving API Team Lead @AB180