- Published on
- reading time
- 21분
MFE 환경에서 Trunk-Based Development를 위한 Bitbucket CI/CD 구축기
- Authors
- Name
- Heesu Choi
- dqdq4197@gmail.com
- Github
- dqdq4197
이전 직장에서 항공, 호텔, 패키지 등 여러 도메인이 얽힌 복잡한 모노레포 환경에서 개발팀의 속도와 안정성이라는 두 마리 토끼를 잡기 위해 분투했던 경험을 공유해보고자 한다.
당시 프로젝트는 마이크로 프론트엔드(MFE) 아키텍처로, 각 애플리케이션을 독립적인 팀에서 관리했지만 결국 하나의 통합된 프로덕트로 제공되어야 했다. 이러한 환경에서 어떻게 하면 팀의 개발 속도를 늦추지 않으면서도, 다른 팀에 영향을 주지 않고 안정적으로 코드를 통합하고 배포할 수 있었을까?
이 글에서는 당시 이 문제를 해결하기 위해 Trunk-Based Development (TBD) 전략을 채택했던 배경과, 이를 Bitbucket Pipelines를 통해 성공적으로 자동화했던 여정을 회고해보고자 한다.
왜 Trunk-Based Development(TBD)를 선택했는가?
내가 합류했던 초기의 프로젝트는 전통적인 Git-flow와 유사한 브랜치 전략을 사용했다.develop, release, hotfix 등 여러 브랜치를 오랫동안 관리해야 했다. 하지만 MFE 모노레포 환경에서 모든 팀이 각자의 긴 수명의 브랜치를 가져가자, 브랜치 간의 차이는 점점 커졌고 결국 'Merge Hell' 에 빠지기 일쑤였다. 한 팀의 배포가 다른 팀의 코드를 망가뜨리는 위험도 매우 크다고 느꼈다.
당시 우리 팀의 목표는 명확했다. 지속적인 통합(Continuous Integration)을 실현하고, 모든 팀의 변경 사항을 최대한 빠르고 자주 중앙 브랜치에 통합하여 리스크를 줄이는 것이었다. 이것이 바로 우리가 TBD 전략을 도입하게 된 결정적인 이유였다.
TBD는 모든 개발자가 '트렁크(Trunk)'라고 불리는 단일 메인 브랜치(당시 항공 팀의 경우 flight/main)를 중심으로 작업하는 방식이다. 여기서 중요한 점은, MFE 구조에 맞게 중앙 트렁크도 서비스마다 따로 있었다는 것이다. 예를 들어 항공팀은 flight/main을, 호텔팀은 hotel/main을 각자의 트렁크로 사용했다. 개발자들은 짧은 수명의 피처 브랜치에서 작업하고, 하루에도 여러 번 트렁크 브랜치로 코드를 병합했다.
이러한 TBD 전략은 당시 MFE 환경에 다음과 같은 명확한 이점을 가져다주었다.
- 코드가 항상 최신 상태로 트렁크에 통합되므로, 통합 과정에서 발생하는 문제를 조기에 발견하고 해결할 수 있었다.
- 브랜치의 수명이 짧아 트렁크와의 코드 차이가 적었기 때문에, 복잡한 병합 충돌이 거의 발생하지 않았다.
- 각 팀의 변경 사항이 자신의 도메인 트렁크로 빠르게 모였다. 이렇게 도메인별로 코드가 항상 최신 상태로 유지되니, 다른 팀의 결과물과 통합될 때의 상황을 예측하기 쉬웠고 팀 간의 기술적 논의나 협업도 훨씬 원활해졌다.
하지만 여기서 한 가지 중요한 질문이 생겼다. "미완성된 기능이나 버그가 있는 코드가 트렁크에 병합되면, 운영 환경에 그대로 배포되어 버리는 것 아닌가?"
이 질문에 대한 우리의 해답이 바로 '피처 플래그' 였다.
TBD의 핵심, 피처 플래그 (Feature Flags) TBD에서 트렁크 브랜치는 항상 배포 가능한 상태를 유지해야 한다. 우리는 피처 플래그를 통해 이 원칙을 지켰다. 피처 플래그는 코드의 특정 기능을 켜고 끌 수 있는 스위치와 같은 개념이다.
우리가 구축했던 워크플로우는 다음과 같았다.
- 개발자는 새로운 기능을 개발할 때, 해당 기능을 감싸는 피처 플래그를 함께 구현했다.
- 이 기능 코드는 트렁크에 병합될 때, 운영 환경에서는 플래그가 꺼진(off) 상태가 기본값이었다.
- 하지만 QA 및 Stage 환경에서는 플래그가 켜진(on) 상태로 설정되었다.
- 이를 통해 개발팀과 QA팀은 운영 환경에 아무런 영향을 주지 않으면서, 다른 기능들과 통합된 상태로 새로운 기능의 동작을 검증할 수 있었다.
- 모든 검증이 끝나고 기능을 사용자에게 공개하기로 결정하면, 해당 피처 플래그를 on으로 변경하여 재배포했다. (당시 우리는 플래그를 내부에서 관리하여 재배포가 필요했지만, 외부 설정 서비스를 사용했다면 배포 없이도 동적으로 기능을 켜고 끌 수 있었을 것이다.)
이처럼 피처 플래그는 코드의 병합과 사용자에게 기능을 배포하는 것을 분리하여, TBD 전략을 안전하게 만들어주는 핵심적인 장치였다.
Bitbucket Pipelines로 완성했던 TBD 워크플로우
이론을 바탕으로, 우리는 Bitbucket Pipelines를 커스텀하여 위에서 설명한 TBD 워크플로우를 완벽하게 자동화했다. 당시 구축했던 파이프라인 흐름은 다음과 같다.
Flow 1: Pull Request - 코드 품질을 지키는 첫 번째 관문
- 개발자는 각자의 트렁크에서 피처 브랜치를 생성하여 작업을 시작한다.
- 개발이 완료되면 자신의 트렁크로 Pull Request를 생성한다.
- 자동화: PR이 생성되는 즉시, Bitbucket Pipelines가 자동으로 다음 작업을 수행한다.
- lint: 코딩 스타일과 컨벤션을 검사
- test: 유닛 테스트, 통합 테스트 등을 실행
- tsc: TypeScript 타입 체크를 통해 타입 오류를 사전에 방지
리뷰어는 코드 리뷰에 앞서, 자동화된 검증 과정의 통과 여부를 먼저 확인하는 것을 팀의 컨벤션으로 삼았다. 이것이 트렁크 브랜치의 안정성을 보장하는 사실상의 첫 번째 안전장치 역할을 했다.
자세히 알아보기: 코드 품질 검증 파이프라인
Bitbucket 파이프라인에서는 pull-requests 속성을 사용하여, PR이 생성되거나 업데이트될 때만 실행되는 파이프라인을 별도로 정의할 수 있다.
이렇게 정의한 PR 파이프라인의 실행 시간을 단축시키기 위해, 우리는 parallel 옵션을 활용하여 테스트와 타입 체크처럼 서로 의존성이 없는 검증 단계를 병렬로 처리하도록 구성했다.
pipelines:
pull-requests:
'feature/*':
- parallel:
- step: *lint
- step: *test
- step: *tsc
'bugfix/*':
- parallel:
- step: *lint
- step: *test
- step: *tsc
Flow 2: Trunk Merge & 순차적인 자동 배포
- 코드 리뷰까지 완료된 PR은 해당 팀의 트렁크 브랜치로 병합한다.
- 자동화: 병합이 감지되면, Bitbucket Pipelines가 자동으로 다음 단계를 수행한다.
- QA 환경 자동 배포: 트렁크의 최신 코드가 QA 환경에 배포 진행 (피처 플래그는 ON 상태)
- Stage 환경 자동 배포: QA 배포가 성공적으로 완료되면, 이어서 Stage 환경에 배포 진행
- 슬랙 알림: 각 단계의 배포가 시작될 때마다, 어떤 변경사항이 어느 환경에 배포되는지 팀 채널에 실시간으로 공유하도록 하여 개발자가 아닌 프로젝트 구성원 모두가 상황을 인지할 수 있도록 함
자세히 알아보기: 순차적인 자동 배포 및 슬랙 알림
순차적인 자동 배포
Bitbucket 파이프라인에서는 flight/main과 같은 특정 브랜치에 코드가 병합될 때 배포 스크립트를 실행하도록 간단히 설정할 수 있다. 나는 이 기능을 활용하여 QA 배포가 성공적으로 완료되면, 이어서 Stage 배포가 순차적으로 실행되는 파이프라인을 구성했다. (자세한 설정 방법은 Bitbucket 공식 문서를 참고하면 된다.)
pipelines:
branches:
flight/main: # flight 팀의 트렁크 브랜치
- step: *deploy-qa-flight # QA 배포
- step: *deploy-stg-flight # Stage 배포
슬랙 알림
Bitbucket 파이프라인은 배포 결과(성공, 실패, 중지 등)에 대한 기본적인 슬랙 알림 연동 기능을 제공한다. 하지만 우리 팀의 워크플로우를 완성하기에는 두 가지 명확한 한계점이 있었다.
- 배포의 '시작' 시점을 알려주지 않고 '결과'만 알려준다는 점이다. 팀원들이 배포 진행 상황을 미리 인지하고 대응하기 위해서는 시작 알림이 필수적이었다.
- 알림 메시지를 커스터마이징할 수 없다는 점이다. 이번 배포에 어떤 변경 사항이 포함되었는지, 누가 트리거했는지 등 풍부한 컨텍스트를 담고 싶었지만 기본 기능만으로는 불가능했다.
이러한 이유로, 우리는 배포 시작을 알리고 원하는 정보를 자유롭게 담기 위해 파이프라인 스크립트 내에서 직접 슬랙 알림을 보내도록 구현해야 했다.
'어떻게 하면 개발자와 비개발자 모두에게 유용한 알림을 만들 수 있을까?' 이 질문은 서로 다른 두 관점을 모두 만족시켜야 하는, 꽤나 재미있는 고민이었다. 팀원들과 머리를 맞댄 끝에, 우리는 다음과 같은 내용을 알림의 핵심으로 삼기로 했다.
- 커밋 해시: 배포되는 코드의 정확한 버전을 나타내는 커밋 해시를 포함했다. 이를 통해 어떤 코드 베이스가 배포되었는지 명확히 알 수 있다.
- Jira 이슈 자동 링크: 커밋 메시지에 포함된 Jira 이슈 키(예: PRIVIA-123, DT-45)를 정규식(/(PRIVIA|DT)-\d+/g)으로 찾아내, 해당 이슈로 바로 갈 수 있는 링크를 자동으로 걸어주도록 구현했다. 덕분에 변경 사항과 관련된 업무 내용을 클릭 한 번으로 확인할 수 있다.
- "자세히 보기" 버튼: 전체 변경 내역을 담은 Bitbucket 관련 페이지로 바로 이동할 수 있는 버튼을 추가하여, 상세 내용을 쉽게 파악할 수 있도록 했다.
- 메시지 생략 처리: 변경 내역이 너무 길어 슬랙 메시지 제한을 넘을 경우, 내용이 잘리지 않도록 "...생략됨"과 같이 표시하고 "자세히 보기" 버튼을 통해 확인하도록 유도했다.
Flow 3: 안전한 운영 배포와 릴리즈
- QA와 Stage 환경에서 충분한 검증이 완료되면, 운영 환경에 배포할 준비가 된 것이다.
- 자동화 (수동 트리거): 운영 환경 배포는 안정성을 위해 개발자가 Bitbucket UI에서 직접 버튼을 클릭해야 배포가 시작된다. 이때 배포되는 코드는 이미 Stage 환경에서 완벽하게 검증된 코드와 동일하다.
기능을 사용자에게 공개하기로 결정했다면, 해당 기능의 피처 플래그를 ON으로 설정한 후, 다시 운영 환경 배포 파이프라인을 실행하여 릴리즈를 완료했다.
자세히 알아보기: 배포 단계 수동 트리거
파이프라인의 특정 step이나 stage를 자동으로 실행하는 대신 수동으로만 동작하게 만들고 싶을 때가 있다. 이때는 해당 설정에 trigger: manual 옵션을 추가하여 간단하게 구현할 수 있다.
pipelines:
branches:
flight/main:
- step: *deploy-qa-flight
- step: *deploy-stg-flight
+ - stage:
+ <<: *deploy-prod-flight-stage # 운영 배포
+ trigger: manual
Flow 4: 배포 후 작업 자동화
- 자동화: 운영 배포가 성공적으로 완료되면, 파이프라인은 마지막 임무를 수행한다.
- 트렁크 브랜치의 변경 사항을 프로젝트의 최상위 트렁크인
main브랜치로 통합하기 위한 Pull Request를 자동으로 생성한다.- PR 설명에는 이번 배포에 포함된 모든 커밋 내역이 자동으로 기입된다.
- PR 생성 사실 또한 슬랙으로 알림을 보내어, 리뷰어들이 빠르게 확인하고 병합할 수 있도록 도왔다.
자세히 알아보기: 자동 PR 생성 및 슬랙 알림
Bitbucket Cloud는 REST API를 통해 UI에서 제공하지 않는 다양한 작업을 자동화할 수 있다.
나는 이 점을 활용하여, 운영 환경 배포 스테이지(예: deploy-prod-flight-stage) 안에 배포 스크립트를 실행하고, 배포가 성공적으로 완료되면 이어서 Create a pull request API를 호출하는 로직을 포함시켜 PR 생성을 자동화했다.
PR 생성 API를 호출하기 전에 git log 명령어를 실행하여 이번 배포에 포함된 커밋 내역을 추출할 수 있다.
이 내용을 API 요청 본문에 담아 전달하면, 아래 이미지처럼 PR 설명(Description)에 커밋 로그를 자동으로 채워 넣어 리뷰어의 편의를 도울 수 있다.
PR 생성이 성공하면, 마지막으로 팀 채널에 슬랙 알림을 보내 모든 자동화 과정을 마무리했다.
이 슬랙 알림에는 리뷰어가 컨텍스트를 빠르게 파악하는 데 도움이 되는 다음과 같은 정보들을 포함했다.
- PR 바로가기 버튼: 자동 생성된 Pull Request로 즉시 이동할 수 있는 버튼이다.
- 배포 파이프라인 링크: 이 PR을 생성하는 운영 배포의 파이프라인 링크이다.
- 브랜치 정보: 어떤 브랜치의 코드가 master 브랜치로 병합되는지를 시각적으로 보여준다. (예: flight/main → master)
- Git 태그 정보: 배포의 기준이 된 소스 브랜치의 Git 태그를 표시하여 정확한 릴리즈 버전을 확인할 수 있게 했다.
마치며
돌이켜보면, 복잡한 MFE 모노레포 환경의 문제를 해결하기 위해 우리가 선택했던 Trunk-Based Development와 안정성을 확보해 준 피처 플래그, 그리고 이 모든 것을 자동화하여 개발자 경험을 극대화하고 배포 리스크를 최소화했던 Bitbucket Pipelines는 매우 성공적인 결정이었다.
이러한 CI/CD 파이프라인 구축을 통해 당시 우리 팀은 다음과 같은 성과를 얻을 수 있었다.
- 개발자는 복잡한 브랜치 관리에 신경 쓸 필요 없이 기능 개발에만 집중할 수 있었다.
- 자동화된 검증과 단계별 배포를 통해 운영 환경의 안정성을 크게 높였다.
- 모든 코드가 트렁크에 통합되고 각 기능은 피처 플래그로 격리되어 언제든 자신감 있게 배포할 수 있었고, 여러 기능을 동시에 QA하는 것도 가능해졌다.
혹시 여러 팀이 협업하는 복잡한 프로젝트를 진행하고 있다면, 내가 경험했던 TBD 전략과 자동화된 CI/CD 파이프라인 도입을 적극적으로 고려해 보길 바란다.