모노리포 개발 가이드

본 문서는 클라이언트의 모노리포에서 작업을 함에 앞서 알아야 하는 규칙과 작업 흐름을 설명한다.

프로젝트 구성

모노리포란 하나의 리포지토리에서 여러 프로젝트를 함께 관리하는 것을 의미한다. 따라서 하나의 리포에는 많은 모듈이 포함되어 있으며 이들은 서로 로컬 라이브러리 모듈로 참조를 하고 있다.

이에 대해 더 자세히 알고 싶으면 Multirepo vs Monorepo 문서를 참조한다.

레이어 구성

  • 앱 레이어
    • 일반적인 application / sdk 를 구성하는 모듈 계층입니다. Buzzscreen SDK, BuzzAd SDK 등이 포함됩니다.
  • 피쳐 레이어
    • 유저 입장에서의 단위 기능을 제공하는 모듈 계층입니다. 앱 모듈에서 필요한 기능을 구현하기 위해 여러 피쳐 모듈을 조합할 수 있습니다. 비디오 기능 모듈, 만보기 기능 모듈 등이 포함됩니다.
  • 라이브러리 레이어
    • 위 모듈에서 공통으로 사용되는 로직을 단위 기능으로 제공하는 모듈 계층입니다. auth, logger 등이 포함됩니다.

로컬 라이브러리 모듈

로컬 라이브러리 모듈이란 로컬 프로젝트 내에서 참조하여 사용하는 모듈을 의미한다. 모노리포 구조의 클라이언트에서는 여러 로컬 라이브러리 모듈들을 참조하여 사용하는데 다음과 같이 사용한다.

implementation project(":path")

로컬 라이브러리 모듈의 경우 별도의 버전을 지칭하지 않고 현재의 코드를 직접 참조하여 개발을 수행한다. 그렇기 때문에 개발자는 로컬 라이브러리 모듈들에 대해서 버전 관리를 하지 않아도 된다는 착각을 할 수 있다. 하지만 SDK 개발을 하면서 이는 옳지 않은 말이며, 개발자는 각 로컬 라이브러리 모듈에 대해서 버전 관리 및 배포를 수행해줘야 한다. 그 이유는 앱과 SDK의 빌드 결과물이 다르기 때문인데 자세한 내용은 문서를 참조한다.

결론적으로 버즈빌의 SDK 개발을 위해서는 각 로컬 라이브러리 모듈에 대해서 버전 관리 및 배포를 수행해야 한다.

브랜치 모델

브랜치 모델이란 Git과 같은 소스 제어 도구에서 브랜치를 어떻게 관리하는지에 대하여 정의한 모델이다. 많이 알려진 모델로는 Git Flow, Trunk-Based Development 등이 있다. 클라이언트 모노리포에서는 항상 배포 가능한 올바른 상태의 소스를 유지하기 위해서 Trunk-Based Development 브랜칭 모델을 사용한다.

Trunk-Based Development

TBD Overview

Trunk-Based Development란 소스 제어 도구에서 브랜치를 어떻게 관리할지에 대한 전략이다. 개발자들은 마스터에서 협업하고, 긴 작업 단위의 브랜치 생성을 피한다. 그렇게 함으로서 협업 간 충돌을 최소화하고 빌드가 실패되는 상태를 막는다. 이 모델의 가장 주요한 특징은 트렁크라 불리는 마스터 브랜치를 항상 배포 가능한 올바른 상태로 유지하는 것이다. 이를 보장하기 위한 주요 규칙은 다음과 같다.

개발 규칙

  • 개발은 직접 푸시 혹은 브랜치 푸시로 진행한다.
    • 직접 푸시 : 마스터에서 작은 단위의 개발을 진행 후 바로 푸시한다.
    • 브랜치 푸시 : 마스터에서 파생된 피쳐 브랜치를 생성한 후 PR을 통해 마스터로 푸시한다.
  • 버즈빌에서는 개발 중 직접 푸시를 사용하지 않고, 브랜치 푸시를 사용한다.
  • 피쳐 브랜치는 가능한 작은 작업 단위를 가져야 한다.
  • 장기간 지속하여야 하는 피쳐 개발의 경우 피쳐 플래그인터페이스를 사용해 작업 단위를 나눈다.

배포 규칙

  • 배포는 직접 배포 혹은 브랜치 배포로 진행한다.
    • 직접 배포 : 마스터에서 바로 배포를 수행한다.
    • 브랜치 배포 : 마스터에서 필요 시점에 배포 브랜치를 생성 후 배포를 수행한다.
  • 버즈빌에서는 필요에 따라 직접 배포와 브랜치 배포를 혼용하고 있다. 자세한 내용은 아래 버즈빌 배포를 참조한다.
  • 배포 브랜치에서 작업한 결과는 마스터로 머지하지 않는다.
  • 배포 브랜치에서 추가 작업이 필요할 경우 마스터에서 작업을 수행 후 해당 커밋을 체리픽하여 가져온다.

더 자세히 알아보고 싶다면 Trunk-Based Development 문서를 참조한다.

버전 관리

유의적 버전

버즈빌 클라이언트에서는 통일된 버전 정책을 갖기 위해 유의적 버전을 사용한다.

유의적 버전이란 제품에 버전을 어떻게 부여하고 관리해야 하는지에 대한 규칙이다. 이를 활용해 각 모듈 혹은 제품마다 일정한 규칙의 버전 정책을 확립하고, 협업에 있어 혼란이 생길 수 있는 부분을 제거한다.

유의적 버전은 다음과 같은 주요 원칙을 갖는다.

  • 버전은 크게 Major, Minor, Patch로 이루어진 3가지 버전을 갖는다.
    • Major : 하위 호환이 불가능하도록 API가 수정되었을 때 올린다.
    • Minor : 하위 호환을 지원하면서 기능을 추가했을 때 올린다.
    • Patch : 하위 호환을 지원하면서 버그를 수정했을 때 올린다.
  • Major.Minor.Patch 버전 이후에 pre-release 버전을 확장해서 사용할 수 있다.
    • 예시 : 1.0.1-rc.1 or 1.2.3-alpha.2 …

더 자세히 알아보고 싶다면 유의적 버전 문서를 참조한다.

버즈빌 적용 사례

유의적 버전은 [Major, Minor, Patch]에 해당하는 핵심 버전 이후에 프리 릴리즈 코드 및 빌드 코드 등을 자유롭게 확장하여 사용할 수 있다.

버즈빌에서는 유의적 버전의 규칙을 따르는 모든 버전을 허용하지만, 주로 다음과 같은 규칙을 이용해서 제품의 버전을 관리하고 있다.

buzzvil version sample

  • [Major].[Minor].[Patch]-[Pre Release Code].[Pre Release Version]
    • Major : 메이져 버전
    • Minor : 마이너 버전
    • Patch : 패치 버전
    • 확장 버전
      • Pre Release Code : 프리 릴리즈 코드
        • 현재는 rc만 사용. alpha, beta 등 다른 키워드 역시 필요하다면 논의 후 사용 가능
      • Pre Release Version : 프리 릴리즈 코드 버전

이때 프리 릴리즈 코드를 활용한 정보 기록은 금지한다. 예를 들어 A/B 테스트를 위해 배포할 버전이라는 의미로 1.0.0-abtest.rc.1과 같은 버전을 주입하는 것을 금지한다. 이는 프리 릴리즈 코드의 목적에 맞지 않고 버전의 우선순위 비교 규칙상 사전식으로 버전 비교를 수행하기 때문에 버전 순서에 혼란을 줄 수 있다. 만약 이처럼 특정 버전의 시점에 의미를 기록하고 싶다면 Git의 tag 기능을 활용하는 것으로 한다.

버전 변경 시점

버전을 어떤 규칙으로 구성할 것인지도 중요하지만, 언제 버전을 올려야 하는지도 버전 관리의 중요한 한 부분이다.

버즈빌은 브랜치 모델로 Trunk-Based Development를 사용하고 있고, 그에 해당하는 버전 관리 규칙을 따르고 있다. 하지만 이 룰을 모든 모듈에 엄격히 적용할 경우 관리의 복잡성이 과도하게 늘어나기 때문에, 모듈의 상황에 따라 예외 규칙을 허용하여 버전 변경을 수행한다.

이 섹션에서는 Trunk-Based Development에서는 어느 시점에 버전을 올려야 하고, 버즈빌에서는 어떻게 규칙을 수정하여 버전을 올려야 하는지 정의한다.

Trunk-Based Development

git version validation

Trunk-Based Development에서는 모든 상태가 배포가 가능한 상태임을 보장해야 한다. 그러기 위해서는 모든 마스터의 커밋이 올바른 버전 상태를 가져야 함을 의미한다.

때문에 개발자는 마스터로 향하는 모든 푸시 및 PR에서 변화가 있는 모듈의 버전을 유의적 버전 규칙에 맞게 올려줘야 한다.

하지만 이처럼 모든 커밋에서 버전을 올려주는 것은 개발 과정에서 개발자가 신경 써야 하는 부분이 늘어나게 되는 문제가 있고, CI를 통해 자동화를 구축하여도 절차의 복잡도는 올라갈 수 밖에 없다.

버즈빌 정책

버즈빌에서는 Trunk-Based Development의 규칙을 간소화하기 위해서 커밋 시점마다 버전을 올리지 않고 배포되는 시점에 모듈의 버전을 올려준다. 이때 이야기하는 배포되는 시점제품에 배포 태그를 다는 시점을 의미한다.

단, 주의할 점은 모듈을 배포할 때 자기 자신의 버전을 올려줄 뿐만 아니라, 자신이 참조하는 로컬 라이브러리 모듈 중에서 변화가 있는 모듈 역시 버전을 올려줘야 한다는 점이다.

이러한 작업은 모듈이 많아질수록 사람이 수작업으로 하기 힘든 부분이다. 때문에 버즈빌에서는 그래들 플러그인을 개발하여 배포를 자동으로 수행한다.

핫픽스 대응

브랜치 모델과 유의적 버전 규칙을 잘 따른다면, 배포가 일어난 이후 모듈이 핫픽스가 이루어져도 기능 변화는 없어야 하고 패치 버전만 올라가야 한다. 하지만 실제 SDK 사업을 운영하다 보면 핫픽스를 통해서 기능 변화를 주어야만 하는 경우를 마주하게 된다. 그렇게 되면 기존의 개발 규칙을 어기게 되는 것이고 CI를 통해 올바른 버전 상태임을 검증할 수 없게 된다.

위와 같은 문제를 해결하기 위해 다음과 같은 규칙을 정의한다.

첫째, 핫픽스는 CI에서 버전 검증을 해주지 않는다.

다양한 변수가 있을 수 있기 때문에 작업자의 판단과 강도 높은 테스트를 통해서 안정성을 검증한다.

둘째, 핫픽스에서 로컬 라이브러리 모듈의 수정을 피한다.

핫픽스에서 모듈의 수정이 일어나면 기존의 모듈과 다른 새로운 버전의 코드가 생성되는 것이다. 이 때 배포 모듈의 경우 기본적으로 새로운 버전으로 배포가 되지만 로컬 라이브러리 모듈의 경우 별도로 새로운 버전으로 세팅하여 배포를 해야 한다.

따라서 모듈의 수정이 필요할 경우 가능한 마스터 브랜치에서 작업 및 배포 후 리모트 참조를 하는 방법으로 코드의 수정을 피한다.

셋째, 핫픽스에서 수정하는 모듈은 핫픽스 버전으로 배포한다.

만약 핫픽스에서 꼭 로컬 라이브러리 모듈의 수정이 일어나서 다시 배포해야 할 경우에는, 마스터에서 사용하던 버전과의 충돌을 방지하기 위하여 프리 릴리즈 코드를 hotfix로 해서 배포한다. (예시 : 1.0.1-hotfix.1)

배포 규칙

모노리포에서 어떤 규칙을 갖고 배포를 할지 정의한다.

브랜치 모델과 배포

클라이언트의 모노리포에서는 브랜치 모델로 Trunk-Based Development를 사용하고 있다. 때문에 배포하는 방법은 두 가지 경우로 나뉠 수 있다.

  • 배포 브랜치
    • 마이너 버전을 기준으로 배포 브랜치를 생성
    • 이후 유지보수 작업은 마스터에서 진행 후 배포 브랜치로 체리픽 후 패치 버전 증가
  • 직접 배포
    • 마스터에서 원하는 커밋에서 바로 배포

버즈빌 배포

현재 버즈빌에서는 특별한 경우가 아니면 배포 브랜치를 만들지 않고 마스터에서 태깅한 이후 바로 배포를 수행하고 있다. 이렇게 할 경우 다음 문제를 고려해야 한다.

SDK version issue when released from master

위와 같이 마스터에서 배포를 수행하는 도중에 기능 변화가 적용된 커밋이 있다면, SDK의 마이너 버전이 증가한 것이기 때문에 패치 버전의 증가만으로는 배포를 수행할 수 없다. 하지만 이를 해결하기 위해 위와 같이 모든 경우에 배포 브랜치를 생성하여 체리픽으로 관리를 하게 되면, 배포 시스템이 너무 복잡해지기 때문에 버즈빌에서는 다음과 같은 규칙으로 배포를 진행한다.

Buzzvil release policy

  • 기본적으로 배포는 마스터에서 태깅 후 직접 배포한다.
  • 기능 변화가 중간에 있었다 하더라도 작업자의 판단에 따라 마이너 버전을 올리지 않고 배포한다.
  • 작업자가 판단할 때 포함되면 안 되는 변화가 있다면 이전 배포 태그에서 브랜치를 생성하여 배포한다.

FAQ

  • Q) 버전 관리를 안드로이드의 versionName 을 기준으로 수행하는데 versionCode 로 하면 안 되는가?
    • A) CI에서 버전의 증가를 정확히 검증하기 힘들기 때문에 힘들다. versionCode는 정수 기반으로 1씩 증가하는 룰을 갖고 있는데 핫픽스 브랜치에서 새로 배포를 해야 할 경우 versionCode가 별도로 증가 할 수 있다. 이 경우 마스터의 헤드에서 versionCode가 3이었을 경우 다음 커밋의 versionCode는 4일 수도 아닐 수도 있다. 따라서 버전에 대한 검증을 정확히 하기 위해서 정수기반의 versionCode가 아닌 시맨틱 버전이 적용된 versionName을 기준으로 버전 관리를 수행한다.

Reference