monorepo

6 개의 포스트

포크에서 벗어나기: Meta가 50개 이상의 유스케이스에서 WebRTC를 현대화한 방법 (새 탭에서 열림)

메타는 대규모 오픈소스 프로젝트인 WebRTC를 커스터마이징하여 사용하며 겪었던 '포크 트랩(Forking Trap)'을 해결하기 위해, 최신 업스트림 버전과 내부 최적화 버전을 동시에 실행할 수 있는 듀얼 스택 아키텍처를 구축했습니다. 이를 통해 50개 이상의 유즈케이스에서 안전하게 A/B 테스트를 수행하며 성공적인 마이그레이션을 마쳤고, 결과적으로 성능 향상과 바이너리 크기 최적화 및 보안 강화를 달성했습니다. 현재 메타는 이 구조를 바탕으로 모노레포 환경에서도 업스트림의 최신 업데이트를 지속적으로 반영하며 기술적 부채 없이 서비스를 운영하고 있습니다. **포크 트랩과 모노레포 환경의 도전 과제** * 오픈소스 프로젝트를 내부적으로 포크하여 오래 사용하면 업스트림과의 격차가 벌어져 최신 기능을 반영하기 어려워지는 '포크 트랩'이 발생합니다. * 빌리언 단위의 사용자를 보유한 서비스에서 대규모 라이브러리를 한 번에 교체하는 것은 리스크가 크기 때문에, 구버전과 신버전을 동시에 실행하며 검증할 수 있는 A/B 테스트 역량이 필수적이었습니다. * 하지만 메타의 모노레포 환경과 정적 링크(Static Linking) 방식에서는 동일한 라이브러리의 두 버전을 동시에 포함할 때 '단일 정의 원칙(ODR)' 위반으로 인한 수천 개의 심볼 충돌 문제가 발생했습니다. **심(Shim) 레이어와 듀얼 스택 아키텍처** * 애플리케이션과 WebRTC 구현체 사이에 프록시 역할을 하는 '심(Shim) 레이어'를 구축하여 통합된 API를 제공했습니다. * 애플리케이션은 버전과 무관한 심 API를 호출하고, 심 레이어는 런타임 설정(Flavor)에 따라 레거시 또는 최신 구현체로 호출을 전달합니다. * 모든 라이브러리를 복제하는 대신 최하위 레이어에서 심을 구현함으로써, 바이너리 크기 증가폭을 예상치(38MB) 대비 약 87% 감소한 5MB 수준으로 억제했습니다. **심볼 충돌 해결과 하위 호환성 유지** * 자동화된 네임스페이스 재명명(Renamespacing) 스크립트를 통해 `webrtc::` 네임스페이스를 각 버전에 맞게 `webrtc_legacy::`, `webrtc_latest::` 등으로 분리했습니다. * 네임스페이스 외부에 존재하는 글로벌 C 함수와 변수들은 버전별 식별자를 추가하여 충돌을 방지했습니다. * 기존 코드의 수정을 최소화하기 위해 C++의 `using` 선언을 활용하여, 외부 호출부에서는 여전히 기존 네임스페이스를 사용하는 것처럼 보이게 하면서 내부적으로는 올바른 버전에 연결되도록 설계했습니다. **런타임 버전 디스패치 및 관리** * 템플릿 기반의 헬퍼 라이브러리를 사용하여 중복 로직을 줄이고 버전별 특화 동작을 정의했습니다. * 앱 시작 시점에 결정되는 글로벌 플래그(Enum)를 통해 어떤 WebRTC 버전을 사용할지 동적으로 결정합니다. * 패치 관리의 복잡성을 해결하기 위해 모노레포 내에서 업스트림 버전을 주기적으로 가져오고 내부 패치를 반복적으로 적용하는 워크플로우를 정립했습니다. 대규모 오픈소스 프로젝트를 운영할 때 직접적인 포크보다는 이와 같은 모듈식 아키텍처와 자동화된 네임스페이스 관리를 도입하는 것이 기술적 고립을 막는 효과적인 전략이 될 수 있습니다. 이는 특히 안전한 배포와 지속적인 업스트림 동기화가 중요한 대규모 시스템에서 실무적인 해법을 제시합니다.

GitLab 파이프라인 로직이 엔지니어링 문제를 해결하는 5가지 방법 (새 탭에서 열림)

GitLab의 파이프라인 실행 모델은 모노레포, 마이크로서비스, 다중 환경 배포와 같은 현대적인 엔지니어링 복잡성을 해결하기 위해 설계되었습니다. 부모-자식 파이프라인, DAG(Directed Acyclic Graph), 멀티 프로젝트 트리거 등의 기능을 조합하면 단순히 빌드 속도를 높이는 것을 넘어 조직의 표준을 강제하면서도 병목 현상을 줄이는 확장 가능한 CI/CD 시스템을 구축할 수 있습니다. 결과적으로 이러한 구성 가능한 패턴들을 이해하고 활용하는 것이 효율적인 소프트웨어 배포의 핵심입니다. **모노레포 최적화를 위한 부모-자식 파이프라인과 DAG 실행** - 특정 서비스의 변경사항이 발생했을 때만 관련 파이프라인이 실행되도록 '부모-자식 파이프라인'을 구성하여 불필요한 전체 재빌드를 방지합니다. - `trigger: include`와 `strategy: depend`를 사용하여 부모 파이프라인이 자식 파이프라인의 결과에 의존하게 함으로써, 상위 수준에서 전체 서비스의 상태를 한눈에 파악할 수 있습니다. - `needs` 키워드를 활용한 DAG(비순차적 실행) 모델을 적용하면, 동일 단계(stage)의 다른 작업이 끝나기를 기다리지 않고 의존성이 해결되는 즉시 다음 작업을 시작하여 파이프라인 실행 시간을 획기적으로 단축합니다. - 각 서비스가 독립적인 설정 파일을 가질 수 있어 조직적 분리가 용이하며, 한 서비스의 설정 오류가 전체 모노레포 시스템을 중단시키지 않도록 격리합니다. **마이크로서비스 간 연동을 위한 멀티 프로젝트 파이프라인** - 서로 다른 리포지토리에 존재하는 프론트엔드와 백엔드 간의 의존성 문제를 해결하기 위해 '멀티 프로젝트 트리거'를 사용하여 파이프라인을 연결합니다. - 프론트엔드 파이프라인에서 API 계약(Contract) 아티팩트를 생성하고, 이를 백엔드 파이프라인 트리거 시 전달하여 서비스 간 정합성을 자동으로 검증합니다. - `$CI_JOB_TOKEN`을 활용한 Jobs API 호출을 통해 다른 프로젝트의 아티팩트를 안전하게 가져올 수 있으며, 이를 통해 통합 테스트의 자동화 수준을 높입니다. - 업스트림 파이프라인 뷰에서 연결된 다운스트림 파이프라인의 상태를 실시간으로 확인할 수 있어, 서비스 간 변경 사항이 미치는 영향에 대한 가시성을 제공합니다. GitLab이 제공하는 이러한 파이프라인 로직은 단순한 빌드 도구를 넘어 복잡한 아키텍처를 관리하는 강력한 오케스트레이션 엔진 역할을 합니다. 대규모 모노레포를 운영하거나 서비스 간 의존성이 복잡한 마이크로서비스 환경이라면, DAG를 통한 속도 최적화와 멀티 프로젝트 트리거를 통한 통합 검증 체계를 우선적으로 도입할 것을 권장합니다.

개발 속도 향상을 위한 모노레포 크기 줄이기 (새 탭에서 열림)

Dropbox는 87GB에 달하던 서버 모노레포 크기를 20GB로 약 77% 줄여 개발자 속도와 CI 효율성을 획기적으로 개선했습니다. 이 과정에서 Git의 기본 델타 압축 알고리즘이 특정 디렉토리 구조에서 비효율적으로 작동한다는 점을 발견했으며, GitHub 팀과 협력하여 최적화된 리팩(Repack) 설정을 적용해 저장소 용량 한계 문제를 해결했습니다. 결과적으로 1시간 이상 걸리던 클론 시간을 15분 미만으로 단축하며 운영상의 리스크를 제거했습니다. ### 대규모 모노레포 성장이 유발하는 운영 병목 - 저장소 크기가 87GB를 넘어서면서 초기 개발 환경 구축을 위한 클론 시간이 1시간을 초과했고, 이는 매번 신규 클론을 수행하는 CI(지속적 통합) 파이프라인의 성능 저하로 이어졌습니다. - 코드 데이터는 매일 20~60MB씩 증가하며 GitHub Enterprise Cloud의 하드 리밋인 100GB에 근접해 가고 있었으며, 이는 단순한 코드 양의 증가라기보다 저장 방식의 구조적 결함에 의한 현상이었습니다. - 내부 동기화 시스템의 타임아웃 발생 빈도가 높아지는 등 저장소 크기 자체가 엔지니어링 루프 전체를 느리게 만드는 핵심 원인이 되었습니다. ### Git 델타 압축 알고리즘과 디렉토리 구조의 충돌 - Git은 파일 간의 차이점(Delta)만 저장하여 용량을 줄이는데, 비교 대상 파일을 선정할 때 파일 경로의 '마지막 16자'만을 참조하는 휴리스틱 방식을 사용합니다. - Dropbox의 다국어(i18n) 파일 구조는 `i18n/[언어코드]/LC_MESSAGES/[파일명].po` 형태였는데, 언어 코드가 경로 중간에 있어 Git은 서로 다른 언어의 동일 파일명을 가진 파일들을 비교 대상으로 묶었습니다. - 내용이 전혀 다른 언어 간의 파일을 비교하다 보니 압축 효율이 극도로 낮아졌고, 아주 작은 번역 수정에도 불필요하게 큰 팩(Pack) 파일이 생성되는 결과로 이어졌습니다. ### GitHub 서버 측 리팩 최적화를 통한 문제 해결 - 실험적 플래그인 `--path-walk`를 사용하면 파일 경로 전체를 탐색해 압축 효율을 극대화할 수 있음을 로컬 테스트로 확인했으나, 이는 GitHub 서버의 비트맵 및 델타 아일랜드 최적화 기능과 호환되지 않았습니다. - 로컬에서 최적화하여 푸시하더라도 GitHub 서버가 전송 시 자체 설정으로 다시 팩을 구성하기 때문에, GitHub 지원팀과 협력하여 서버 측 리팩 설정을 조정하는 방식을 택했습니다. - Git이 더 넓고 깊게 유사성을 검색할 수 있도록 `window`와 `depth` 매개변수를 각각 250으로 상향 조정한 공격적인 리팩을 수행하여, 데이터 손실 없이 저장소 크기를 20GB 수준으로 압축하는 데 성공했습니다. ### 대규모 저장소 관리를 위한 제언 - 모노레포의 크기가 비정상적으로 급증한다면 단순한 바이너리 파일 유입뿐만 아니라, Git의 델타 압축 메커니즘과 현재의 디렉토리 구조가 상충하고 있지는 않은지 점검해야 합니다. - 저장소 최적화는 클라이언트 단의 노력만으로는 한계가 있으며, 호스팅 서비스(GitHub 등)의 서버 측 리팩 설정과 인프라 호환성을 반드시 고려하여 전략을 수립해야 합니다.

AI 및 엔지니어링 생산성에 (새 탭에서 열림)

Dropbox는 AI 도구를 단순한 실험을 넘어 비즈니스 가치 창출을 위한 핵심 전략으로 채택하고 있으며, 이를 통해 엔지니어링 생산성의 비약적인 향상을 꾀하고 있습니다. 최근 개최된 경영진 라운드테이블을 통해 AI 도입이 코드 리뷰와 디버깅 등 개발 전반의 효율을 높이는 동시에, 품질 유지와 비즈니스 성과 연결이라는 새로운 도전 과제를 제시하고 있음을 확인했습니다. 결과적으로 성공적인 AI 전환을 위해서는 기술적 도입뿐만 아니라 리더십의 조율과 조직적 프레임워크의 변화가 반드시 병행되어야 한다는 결론을 도출했습니다. ### AI를 통한 Dropbox의 생산성 가속화 전략 * **전사적 우선순위 설정:** AI 도입을 단순한 풀뿌리 수준의 실험이 아닌 회사 차원의 핵심 과제로 격상하여 리더십의 지지를 확보하고, 새로운 도구 도입을 위한 계약 및 승인 절차를 대폭 간소화했습니다. * **자체 AI 플랫폼 구축:** 대규모 다국어 모노레포(Monorepo)라는 특수한 환경에 맞추기 위해 기성 AI 도구에만 의존하지 않고, 풀 리퀘스트(PR) 빌드 실패 시 AI가 자동으로 수정안을 제안하는 자체 도구를 개발하여 운영 중입니다. * **데이터 기반의 성과 추적:** 엔지니어당 월간 PR 처리량(Throughput)을 핵심 지표로 설정하여 AI 도구 활용도가 높은 그룹의 생산성이 월등히 높음을 확인했으며, 내부 설문을 통해 개발자들의 긍정적인 감성 지표 변화를 모니터링하고 있습니다. * **개발자 자율성 부여:** 팀별로 최적의 도구를 선택할 수 있는 유연성을 제공하여 도입 과정에서의 마찰을 줄이고, 소프트웨어 개발 생애 주기(SDLC) 전반에서 AI가 자연스럽게 스며들 수 있도록 지원합니다. ### AI 시대의 엔지니어링 리더십과 조직 운영 * **균형 잡힌 생산성 관리:** AI로 인한 속도 향상이 코드 품질 저하나 장기적인 유지보수 비용 상승으로 이어지지 않도록 생산성과 품질 사이의 엄격한 균형 감각이 요구됩니다. * **리더십 정렬과 규범화:** 기술 리더십은 효과적인 AI 사용 규범을 설정하고 집행하는 중추적인 역할을 수행해야 하며, AI 배포 속도에 대해 경영진과 명확한 공감대를 형성해야 합니다. * **인적 역량의 공식적 평가:** AI 활용 능력을 엔지니어의 경력 개발 프레임워크(Career Framework)에 공식적으로 포함시켜 조직의 전략적 방향성을 명확히 하고, 비개발 직군의 생산성 향상으로도 그 범위를 확장하고 있습니다. ### 향후 과제 및 실무적 제언 * **유휴 용량의 전략적 재투자:** AI가 확보해 준 엔지니어링 여력을 기술 부채 해결, 시스템 마이그레이션, 서비스 신뢰성 강화 등 고부가가치 영역에 우선적으로 투입해야 합니다. * **비즈니스 성과와의 직접 연결:** 단순히 "코딩 속도가 빨라졌다"는 지표를 넘어, 향후에는 생산성 향상이 실제 비즈니스 결과물과 제품 출시 속도(Velocity)에 어떻게 기여하는지 직접적으로 매핑하는 운영 모델을 구축하는 것이 핵심입니다.

당근페이 백엔드 아키텍처가 걸어온 여정. Money라는 하나의 작은 프로젝트부터 수십 개의 서비스를 하나의… (새 탭에서 열림)

당근페이 백엔드 아키텍처는 서비스의 급격한 성장과 조직의 확장에 발맞춰 계층형, 헥사고날, 그리고 클린 아키텍처 기반의 모노레포 형태로 끊임없이 진화해 왔습니다. 초기에는 빠른 기능 출시를 위해 단순한 구조를 채택했으나, 비즈니스 복잡도가 증가함에 따라 의존성 관리와 코드 응집도를 높이기 위해 구조적 제약을 강화하는 방향으로 발전했습니다. 결과적으로 아키텍처는 기술적 부채를 해결하는 수단을 넘어, 대규모 팀이 협업하며 지속 가능한 성장을 이뤄낼 수 있는 기반이 되었습니다. ### 초기 성장을 견인한 계층형 아키텍처 (Layered Architecture) * **빠른 실행력 중심:** 2021년 당근페이 출시 초기, 송금 서비스의 신속한 시장 진입을 위해 `Controller-Service-Repository`로 이어지는 직관적인 3계층 구조를 사용했습니다. * **성장통의 발생:** 서비스가 커지면서 송금, 프로모션, FDS 등 다양한 기능이 하나의 계층에 뒤섞였고, 서비스 간 순환 참조와 강한 결합이 발생해 코드 변경의 영향 범위를 예측하기 어려워졌습니다. * **기술 부채의 축적:** 모든 비즈니스 로직에 프레임워크 기술(Spring)이 깊숙이 침투하면서 테스트 작성이 까다로워지고, 순수 도메인 로직만 분리해 관리하기 어려운 구조적 한계에 직면했습니다. ### 구조적 제약을 통한 응집도 향상 (Hexagonal Architecture) * **외부 구현과의 분리:** 도메인 규칙을 중심에 두고 UI, DB, 외부 API 등 인프라 영역을 포트와 어댑터를 통해 분리하여 프레임워크에 의존하지 않는 POJO 중심의 설계를 지향했습니다. * **모듈 역할의 세분화:** 프로젝트를 핵심 규칙을 담은 `domain`, 사용자 시나리오 단위의 `usecase`, 실제 입출력을 담당하는 `adapter` 모듈로 재구성하여 의존성 방향을 한곳으로 모았습니다. * **재사용성과 테스트 용이성:** 유스케이스 단위로 로직이 응집되면서 REST API뿐만 아니라 이벤트 컨슈머, 배치 잡 등 다양한 진입점에서 동일한 비즈니스 로직을 안전하게 재사용할 수 있게 되었습니다. ### 규모 확장에 대응하는 클린 아키텍처와 모노레포 * **모노레포 도입의 배경:** 머니, 포인트, 빌링 등 도메인이 늘어남에 따라 여러 저장소를 관리하는 비용이 증가했고, 이를 효율적으로 통합 관리하기 위해 하나의 저장소에서 여러 서비스를 운영하는 모노레포 구조를 채택했습니다. * **계약 기반의 모듈 분리:** 각 도메인을 `contract(인터페이스)`와 `impl(구현체)` 모듈로 쪼개어 의존성 규칙을 강제했습니다. 다른 모듈은 `contract`만 참조하게 하여 불필요한 내부 구현 노출을 차단했습니다. * **빌드 성능 및 생산성 최적화:** Gradle의 `api`와 `implementation` 구성을 활용해 컴파일 시점의 의존성을 제어함으로써, 대규모 프로젝트임에도 불구하고 빌드 시간을 단축하고 변경 영향도를 최소화했습니다. 아키텍처에는 정답이 없으며, 조직의 규모와 비즈니스의 현재 단계에 가장 적합한 형태를 선택하는 것이 중요합니다. 당근페이의 사례처럼 초기에 과도한 설계를 지양하되, 서비스 성장 속도에 맞춰 구조적 제약을 단계적으로 도입함으로써 기술 부채를 통제하고 개발 생산성을 유지하는 전략을 권장합니다.

에어비앤비 (새 탭에서 열림)

에어비앤비는 4.5년에 걸쳐 수천만 라인의 Java, Kotlin, Scala 코드로 구성된 대규모 JVM 모노레포를 Gradle에서 Bazel로 성공적으로 이전했습니다. 이번 마이그레이션을 통해 빌드 속도는 3~5배, IDE 동기화 및 배포 속도는 2~3배 향상되었으며, 개발자 만족도(CSAT)가 38%에서 68%로 크게 올랐습니다. Bazel의 밀폐성(Hermeticity)과 원격 실행 기능을 활용하여 대규모 코드베이스에서도 안정적이고 확장 가능한 빌드 시스템을 구축한 것이 핵심 성과입니다. **Gradle에서 Bazel로 전환한 이유** * **빌드 속도의 혁신:** Bazel의 원격 빌드 실행(RBE)을 통해 수천 개의 작업을 병렬로 처리하며, 'Build without the Bytes' 기능을 도입하여 필요한 아티팩트만 다운로드함으로써 대역폭 소모를 줄였습니다. * **빌드 안정성 및 밀폐성:** Gradle과 달리 샌드박스 환경을 제공하여 빌드 작업이 지정된 입력 외의 파일 시스템(예: /tmp 디렉토리)에 접근하는 것을 차단하고, 환경 차이로 인한 빌드 실패를 방지했습니다. * **통일된 인프라 구축:** 에어비앤비 내의 웹, iOS, Python, Go 등 다양한 언어의 레포지토리를 Bazel로 단일화하여 원격 캐싱, 로깅, 변경된 타겟 계산 로직을 공유할 수 있게 되었습니다. **단계적 마이그레이션과 개념 증명(PoC)** * **Viaduct 플랫폼 선정:** 에어비앤비에서 가장 크고 복잡한 서비스 중 하나인 GraphQL 모놀리스 'Viaduct'를 첫 타겟으로 선정하여, 가장 까다로운 케이스에서 성능 이점을 증명했습니다. * **공존 전략:** 초기에는 개발자가 Gradle과 Bazel 중 선택해서 사용할 수 있도록 두 시스템을 병렬로 운영하여 서비스 중단 위험을 최소화했습니다. * **개발자 설득:** 단순한 성능 향상을 넘어, 초기 단계에서 발생한 버그와 통합 문제를 해결하여 개발자들이 자발적으로 Bazel을 선택하도록 유도했습니다. **자동 빌드 파일 생성 및 유지보수** * **커스텀 생성기 개발:** Bazel 빌드 파일(BUILD)을 수동으로 관리하는 불편을 줄이기 위해 소스 코드의 패키지와 임포트 구문을 분석하여 의존성 그래프를 그리는 자동 생성기를 구축했습니다. * **Gazelle의 영감:** Go 언어의 Gazelle 도구에서 아이디어를 얻었으나, JVM 언어의 특성과 성능 요구사항을 맞추기 위해 캐싱 기능을 포함한 자체 도구로 발전시켰습니다. * **CI 통합:** 모든 커밋 전에 자동 생성기를 실행하여 Gradle과 Bazel의 빌드 그래프가 항상 일치하도록 유지했습니다. **IDE 사용자 경험 개선** * **IntelliJ 동기화 최적화:** 대규모 모노레포에서 Gradle 동기화가 최대 40분까지 소요되던 문제를 Bazel의 병렬 분석과 'Query Sync(실험적 기능)' 도입을 통해 3~10분 수준으로 단축했습니다. * **IntelliJ Aspect 활용:** Bazel의 Aspect 기능을 사용하여 프로젝트 구조 정보를 추출함으로써 IDE가 소스 코드와 의존성을 더 효율적으로 이해하도록 돕습니다. **성공적인 전환을 위한 교훈** 대규모 마이그레이션에서 가장 중요한 것은 **성능에 대한 집착**과 **개발자 경험(DevEx)에 대한 투자**입니다. 빌드 속도가 빨라지면 개발자들은 자연스럽게 새로운 도구를 수용하게 되며, 특히 IntelliJ와 같은 IDE와의 매끄러운 통합이 프로젝트의 성패를 좌우합니다. 또한 빌드 파일 생성과 같은 반복적인 작업을 자동화하여 개발자가 시스템 환경 설정이 아닌 코드 작성에만 집중할 수 있는 환경을 조성하는 것이 필수적입니다.