Gem 개발 가이드라인
GitLab v19.4요약
GitLab은 모놀리식 코드베이스에서 코드 재사용성과 모듈성을 높이는 수단으로 Gem을 사용합니다. 기능이 충분히 독립적이고, GitLab 이 다른 애플리케이션에서도 직접 사용하려 하거나 더 넓은 커뮤니티에 도움이 된다고 판단될 때 코드베이스에서 라이브러리를 추출합니다.
GitLab은 모놀리식 코드베이스에서 코드 재사용성과 모듈성을 높이는 수단으로 Gem을 사용합니다.
기능이 충분히 독립적이고, GitLab 이 다른 애플리케이션에서도 직접 사용하려 하거나 더 넓은 커뮤니티에 도움이 된다고 판단될 때 코드베이스에서 라이브러리를 추출합니다.
코드를 gem으로 추출하면 그 gem 이 GitLab 애플리케이션 코드에 숨은 의존성을 갖지 않는다는 점도 보장할 수 있습니다.
독립적이라고 볼 수 있고 GitLab 비즈니스 로직과 분리되어 별도로 개발할 수 있는 기능을 구현할 때는 항상 Gem을 사용합니다.
Rails 코드베이스에서 새 gem을 추출할 여지가 가장 많은 곳은 lib/ 폴더입니다.
lib/ 폴더에는 범용적인 코드, GitLab 전용 코드, 나머지 코드베이스와 강하게 결합된 코드가 섞여 있습니다.
코드베이스의 일부를 Gem으로 추출할지 판단하려면 다음 질문을 스스로에게 던져 봅니다.
- 이 코드가 별도의 작은 프로젝트로 만들 수 있을 만큼 범용적인지 검토합니다.
- 모놀리식 코드베이스 밖에서 내부적으로 사용될 것으로 예상되는지 검토합니다.
- 별도 컴포넌트로 공개할 만큼 더 넓은 커뮤니티에 유용한지 검토합니다.
위 질문 중 하나라도 답이 '예'라면 새 Gem을 만드는 방안을 적극적으로 검토합니다.
우선 같은 리포지터리에 새 Gem을 만들고, 더 넓은 커뮤니티가 사용할 것으로 예상되는 시점에 별도 리포지터리로 옮길지 평가할 수도 있습니다.
추출한 Gem의 이름을 악의적인 사용자가 선점하지 못하도록 Gem 이름 예약 안내를 따릅니다.
Gem 사용의 장점#
Gem을 사용하면 코드 유지보수에 여러 이점이 있습니다.
- 코드 재사용성 - Gem은 한 가지 목적에 집중하는 독립된 라이브러리입니다. Gem을 사용하면 공통 기능을 문서화와 테스트가 잘 된 단순한 패키지로 분리해 여러 애플리케이션에서 재사용할 수 있습니다.
- 모듈성 - Gem은 특정 기능을 자체 완결적인 라이브러리 안에 캡슐화해 격리를 만듭니다. 덕분에 코드를 더 잘 구성하고, 특정 모듈의 소유자를 명확히 정의하며, 개별 gem을 더 쉽게 유지보수하거나 업데이트할 수 있습니다.
- 작은 규모 - Gem은 독립된 기능 묶음을 구현하므로 설계상 규모가 작습니다. 작은 프로젝트는 이해하고 확장하고 유지보수하기가 훨씬 쉽습니다.
- 테스트 - Gem은 규모가 작아 전체 테스트를 훨씬 빠르게 실행할 수 있고 gem을 철저히 테스트할 수 있습니다. 또한 gem은 패키지로 묶여 있고 자주 바뀌지 않으므로 테스트 실행 빈도를 낮출 수 있어 CI 테스트 시간도 줄어듭니다.
Gem 명명#
Gem 이름은 다음 세 가지 경우로 나뉩니다.
unique_gem: GitLab에 특화된 내용이 없다면 gem 이름에gitlab을 넣지 않습니다existing_gem-gitlab: 공개된 gem을 포크해 수정하거나 확장한 경우에는 RubyGems 규약에 따라-gitlab접미사를 붙입니다gitlab-unique_gem: GitLab 프로젝트 컨텍스트에서만 유용한 gem에는gitlab-접두사를 붙입니다.
기존 gem의 예시는 다음과 같습니다.
y-rb: yrs 용 Ruby 바인딩입니다. Yrs "wires" 는 Yjs 프레임워크의 Rust 포팅입니다.activerecord-gitlab: 공개activerecordgem에 GitLab 전용 패치를 추가합니다.gitlab-rspec과gitlab-utils: 특정 컨텍스트를 돕거나 코드를 재사용하기 위한 GitLab 전용 클래스 모음입니다.
같은 리포지터리 내에#
기존 코드베이스에서 Gem을 추출할 때는 GitLab 모노레포의 gems/에 둡니다.
그러면 gem의 장점(모듈화된 코드, 개발 중 더 빠른 테스트 실행)을 얻으면서 복잡성(리포지터리 간 변경 조율, 새 권한, 여러 프로젝트 등)은 피할 수 있습니다.
같은 리포지터리에 저장된 Gem은 Gemfile에서 path: 구문으로 참조합니다.
추출한 Gem의 이름을 악의적인 사용자가 선점하지 못하도록 Gem 이름 예약 안내를 따릅니다.
새 Gem 생성 및 사용#
새 gem을 추가한 예시는 !121676에서 확인할 수 있습니다.
-
Gem 명명 규약에 따라 적절한 gem 이름을 정합니다.
-
bundle gem gems/<name-of-gem> --no-exe --no-coc --no-ext --no-mit으로gems/<name-of-gem>에 새 gem을 만듭니다. -
rm -rf gems/<name-of-gem>/.git으로gems/<name-of-gem>의.git폴더를 제거합니다. -
rm -r gems/<name-of-gem>/sig/로 자동 생성된 RBSsig/디렉터리를 제거합니다. -
gems/<name-of-gem>/README.md를 편집해 gem에 대한 간단한 설명을 작성합니다. -
gems/<name-of-gem>/<name-of-gem>.gemspec을 편집해 다음 예시처럼 gem 정보를 채웁니다.# frozen_string_literal: true require_relative "lib/name/of/gem/version" Gem::Specification.new do |spec| spec.name = "<name-of-gem>" spec.version = Name::Of::Gem::Version::VERSION spec.authors = ["group::tenant-scale"] spec.email = ["engineering@gitlab.com"] spec.summary = "Gem summary" spec.description = "A more descriptive text about what the gem is doing." spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/<name-of-gem>" spec.license = "MIT" spec.required_ruby_version = ">= 3.0" spec.metadata["rubygems_mfa_required"] = "true" spec.files = Dir['lib/**/*.rb'] spec.require_paths = ["lib"] end -
gems/<name-of-gem>/.rubocop.yml을 다음과 같이 갱신합니다.inherit_from: - ../config/rubocop.yml -
새로 추가한 gem의 CI를 구성합니다.
-
gems/<name-of-gem>/.gitlab-ci.yml을 추가합니다.include: - local: gems/gem.gitlab-ci.yml inputs: gem_name: "<name-of-gem>" -
.gitlab/ci/gitlab-gems.gitlab-ci.yml에 다음을 추가합니다.include: - local: .gitlab/ci/templates/gem.gitlab-ci.yml inputs: gem_name: "<name-of-gem>"
-
-
Gemfile에서 다음과 같이 gem을 참조합니다.gem '<name-of-gem>', path: 'gems/<name-of-gem>'
Gem의 의존성 지정#
gem에는 자체 Gemfile 이 있지만, 실제 애플리케이션에서는
gem 디렉터리에 있는 개별 Gemfile 대신 모놀리식 GitLab의 최상위
Gemfile 이 사용됩니다.
따라서 gem의 Gemfile에는 최상위 Gemfile과 충돌할 수 있는 의존성
버전을 쓰지 않아야 하며, 가능하면 같은 의존성을 사용하도록
해야 합니다.
Rack이 그 예입니다. 모놀리식 애플리케이션이
Rack 2를 사용하고 있고
Rack 3으로 업그레이드하는 중이라면,
GitLab 이 개발하는 모든 gem도 Rack 2에 대해 테스트해야 하며, CI에서 별도 Gemfile을
사용한다면 Rack 3에 대해서도 테스트할 수 있습니다.
예시를 참고합니다.
이는 Rack 에만 해당하는 것이 아니라 모든 의존성에 해당합니다.
Gem 분리 예시#
gitlab-utils는 strong_memoize 나 Gitlab::Utils.to_boolean처럼 GitLab 개발자가 사용하는
공통 기본 기능을 구현한 클래스 모음을 담은 Gem 입니다.
gitlab-database-schema-migrations는 데이터베이스 마이그레이션을 리포지터리에 저장하는 방식을
개선하는 Rails 프레임워크 확장을 담을 수 있는 Gem 후보입니다. 이 확장은 Rails 위에 구축되며
GitLab 애플리케이션에 한정되지 않으므로, 다른 프로젝트에서도 일반적으로 사용하거나
업스트림에 반영할 수 있습니다.
gitlab-database-load-balancing도 앞의 예와 비슷하게, Rails 데이터베이스 처리에 대한 GitLab 전용
로드 밸런싱을 구현할 Gem 후보입니다. 이 코드는 상당히 복잡하고 특수하므로, 격리되고 테스트가
잘 된 Gem에서 복잡성을 관리하면 거대한 모놀리식 코드베이스에서 그 복잡성을
덜어낼 수 있습니다.
gitlab-flipper는 코드베이스에서 기능 플래그를 지원하기 위한 GitLab 전용 확장을 모두 구현할
또 다른 Gem 후보입니다. 시간이 지나며 모놀리식 코드베이스에는 기능 플래그 사용 여부 검사,
일관성 검사, 추가된 기능 플래그의 소유자를 추적하는 여러 헬퍼가 쌓였습니다. 이는
GitLab 비즈니스 로직에 속하지 않으며, Flipper 구현을 더 잘 추적하고
GitLab 기능 플래그로 도그푸딩하도록 훨씬 쉽게 바꾸는 데 쓸 수 있습니다.
activerecord-gitlab은 GitLab 전용 Active Record 패치를 추가하는 gem 입니다.
복잡성을 격리하기 위해 이런 코드는 별도로 관리하는 편이 바람직합니다.
기타 잠재적 사용 사례#
gitlab-ci-config는 .gitlab-ci.yml을 파싱하는 GitLab의 CI 코드를 모두 담을 Gem 후보입니다.
이 코드는 적절한 추상화가 없어 현재 GitLab 애플리케이션과 어느 정도 얽혀 있습니다.
그러나 별도 Gem으로 옮기면 GitLab 애플리케이션과의 통합을 처리하는 다양한 어댑터를
만들 수 있습니다. 예를 들어 인터페이스에서 includes:를 해석하는 어댑터를 정의할 수 있습니다.
gitlab-ci-config Gem 이 생기면 GitLab 안에서는 물론 GitLab Rails 밖과
GitLab CLI에서도 사용할 수 있습니다.
외부 리포지터리 내에#
일반적으로 이 방식은 심각한 단점이 있으므로 진행하기 전에 신중히 검토합니다.
외부 리포지터리에 저장된 Gem은 반드시 Gemfile에서 version 구문으로 참조해야 합니다.
또한 반드시 RubyGems에 게시해야 합니다.
예시#
GitLab은 다음과 같은 여러 외부 gem을 사용합니다.
잠재적 단점#
- Gem은 GitLab 이 유지보수하는 것이라도 메인 Rails 애플리케이션과 동일한 코드 리뷰 절차를 반드시 거치지는 않습니다. 이는 애플리케이션 보안 측면에서 특히 중요합니다.
- 일관된 코드 리뷰 기준을 지원하는 Danger 같은 도구를 포함해 CI/CD를 처음부터 구성해야 합니다.
- 코드를 별도 프로젝트로 추출하면 기능을 바꿀 때 최소 두 개의 머지 리퀘스트가 필요합니다. 하나는 gem에서 기능을 변경하는 MR 이고, 다른 하나는 Rails 애플리케이션에서 버전을 올리는 MR 입니다.
gitlab-rails와의 통합에 두 번째 MR 이 필요하므로 통합 문제를 늦게 발견할 수 있습니다.gitlab-rails보다 리뷰어와 Maintainer 풀이 작아 코드 리뷰를 받는 데 더 오래 걸리고 "버스 팩터" 의 영향이 커집니다.- 새 gem 버전을 릴리스하는 워크플로가 일관되지 않습니다. 현재는 라이브러리 Maintainer 재량으로 방식을 정합니다.
gitlab-rails보다 코드의 가시성과 노출이 낮아 지식 사일로가 생깁니다.- GitLab에는 리뷰어를 Maintainer로 승격하는 절차가 잘 정의되어 있습니다. 추출된 라이브러리에는 그런 절차가 없어 코드 리뷰 기준이 낮아질 위험과 변경을 그대로 출시할 위험이 커집니다.
- GitLab 이 자체적으로 gem을 사용하는 요구 사항이 더 넓은 커뮤니티의 요구와 어긋날 수 있습니다. 일반적으로 자체 gem의 최신 버전을 쓰고 있지 않다면 경고 신호일 수 있습니다.
잠재적 장점#
- 리포지터리가 작아 CI/CD가 더 빨리 실행되므로 피드백 주기가 짧아집니다.
- 프로젝트를 더 넓은 커뮤니티에 공개해 외부 기여를 받을 수 있습니다.
- 리포지터리 소유자가 변경을 리뷰하기에 가장 적합한 경우가 많아,
gitlab-rails에서 적절한 리뷰어를 찾을 필요가 줄어듭니다.
Ruby Gem 생성 및 게시#
새 Gem 프로젝트는 항상 gitlab-org/ruby/gems 네임스페이스에 만듭니다.
- gem에 적합한 이름을 정합니다. GitLab 이 소유하는 gem 이라면 이름 앞에
gitlab-접두사를 붙입니다. 예를 들어gitlab-sidekiq-fetcher입니다. - 필요에 따라 로컬에서 gem을 만들거나 포크합니다.
- gem 이름이 예약되도록 빈
0.0.1버전의 gem을 rubygems.org에 게시합니다.
-
다음 명령을 실행해 새 gem의 소유자로
gitlab_rubygems사용자를 추가합니다.gem owner <gem-name> --add gitlab_rubygems- 비공개 RubyGems 위원회 프로젝트에서 소유권을 확인받도록 Rémy Coutable에게 알립니다.
-
선택 사항. 다음 사용자 중 일부 또는 전부를 공동 소유자로 추가합니다.
- 선택 사항. 관련된 다른 개발자를 공동 소유자로 추가합니다.
https://rubygems.org/gems/<gem-name>에 접속해 gem 이 정상적으로 게시되었고gitlab_rubygems도 소유자인지 확인합니다.gitlab-org/ruby/gems그룹(또는 그 하위 그룹)에 프로젝트를 만듭니다.-
신규 프로젝트 안내를 따릅니다.
-
CI/CD 구성 설정 안내를 따릅니다.
-
새 gem 버전을 릴리스하고 게시하려면
.gitlab-ci.yml에 다음을 추가해gem-releaseCI 컴포넌트를 사용합니다.include: - component: $CI_SERVER_FQDN/gitlab-org/components/gem-release/gem-release@~latest이 job은 gem 패키지를 게시하기 위해
gitlab-org/ruby/gems그룹에서 상속한gitlab_rubygemsRubyGems API 토큰을 사용해 gem을 빌드하고 게시하며, 태그와 릴리스를 만들고 변경 로그 데이터 생성 API 엔드포인트로 릴리스 노트를 채웁니다.변경 로그 항목 파일을 언제 어떻게 생성하는지는 변경 로그 항목 전용 문서를 참고합니다. GitLab 프로젝트와 일관성을 유지하도록, Gem 프로젝트도
.gitlab/changelog_config.yml에gitlab-stylesgem의 파일과 같은 내용으로 변경 로그 YAML 설정 파일을 정의할 수 있습니다. -
릴리스 절차를 쉽게 하려면
gitlab-stylesgem의 템플릿과 같은 내용으로.gitlab/merge_request_templates/Release.mdMR 템플릿을 만들 수도 있습니다(gitlab-styles는 실제 gem 이름으로 반드시 바꿉니다). -
프로젝트 공개 안내를 따릅니다.
-
참고: 경우에 따라 gem을 자체 네임스페이스로 옮기는 편이 나을 수 있습니다. 예를 들어 프로젝트가 자연스럽게 둘 이상으로 늘어나거나(플러그인을 별도 라이브러리로 두는 경우 등), GitLab 팀 구성원뿐 아니라 GitLab 외부 사용자도 이 프로젝트의 Maintainer가 될 것으로 예상되는 경우가 여기에 해당합니다. 후자의 상황(GitLab 외부 Maintainer)은 현재 GitLab에서 일하는 사람이 GitLab 재직 기간이 끝난 뒤에도 그 gem을 계속 유지보수하려는 경우에도 해당할 수 있습니다.
vendor/gems/ 디렉터리#
vendor/의 목적은 외부 리포지터리가 따로 있지만 단순함을 위해 모노레포에
저장하려는 외부 의존성을 GitLab 모노레포로
가져오는 것입니다.
vendor/gems/는 스크립트로든 수동으로든 외부 리포지터리에서 가져오는 경우에만 사용해야 합니다.vendor/gems/는 자체 개발 gem을 저장하는 데 사용해서는 안 됩니다.vendor/gems/는 GitLab 모노레포에서 빌드되도록 하는 수정은 받아들일 수 있습니다gems/는 GitLab 모노레포에 포함된 모든 자체 개발 gem을 저장하는 데 사용해야 합니다.- GitLab 모노레포의
gems/에 없는 외부 저장 의존성에는 모두 RubyGems를 사용해야 합니다.
vendor/gems의 기존 gem 처리#
-
외부 리포지터리가 없고 현재
vendor/gems/에 저장된 자체 개발 Gem은 다음과 같이 처리합니다.-
다른 리포지터리에서 사용하는 Gem:
- 자체 리포지터리로 옮깁니다.
- RubyGems를 통한 게시를 시작하거나 계속합니다.
- 이 Gem은
Gemfile에서 버전으로 참조하며 RubyGems에서 가져옵니다.
-
모노레포에서만 사용하는 Gem:
- RubyGems에 새 버전을 게시하지 않습니다.
- 이미 게시된 버전에 의존하는 애플리케이션이 있을 수 있으므로 RubyGems에서 내리지는 않습니다.
- 이 gem 들은
gems/로 옮깁니다. - 이 Gem은
Gemfile에서path:로 참조합니다.
-
-
외부에서 가져와 모노레포에 벤더링한
vendor/gems/는 다음과 같이 처리합니다.- 업스트림에 반영할 수 없거나 아직 반영되지 않은 수정이 필요하다면 리포지터리에서 유지보수합니다.
- 벤더링한 gem은 서드파티가 게시할 수 있습니다.
- 이 Gem은 GitLab 이 RubyGems에 게시하지 않습니다.
- RubyGems에 의존할 수 없으므로 이 Gem은
Gemfile에서path:로 참조합니다.
rubygems.org 관련 고려 사항#
Gem 이름 예약#
새 gem 이 포함된 공개 코드를 게시하기 전에, RubyGems에서 이름을 선점당하지 않도록 gem 이름을 미리 예약할 수 있습니다.
gem 이름을 예약하려면 Ruby Gem 생성 및 게시 절차를 다음과 같이 바꿔 진행합니다.
- 버전은
0.0.0을 사용합니다. raise "Reserved for GitLab"만 담은lib/NAME.rb파일 하나를 포함합니다.build와publish를 수행한 뒤 https://rubygems.org/gems/에서 성공했는지 확인합니다.
계정 생성#
GitLab 업무를 위해 RubyGems.org 계정을 만든다면 다음을 지킵니다.
- 회사 이메일 계정(
@gitlab.com)을 사용합니다. - YubiKey 나 기기 패스키 같은 Web Authentication 기기로 2단계 인증을 설정합니다.
Maintainer 및 계정 변경#
계정 이메일이나 비밀번호 변경, gem 소유자 변경, gem 삭제 등 모든 변경은 이슈나 Slack(해당 팀의 Slack 채널, #rubygems, #ruby, #development)을 통해 직접 책임지는 팀에 미리 알려야 합니다.