Gems 개발 가이드라인

GitLab은 모놀리식 코드베이스에서 코드의 재사용성과 모듈성을 향상시키는 도구로 Gems를 사용합니다.

코드 기능이 매우 고립되어 있고 다른 어플리케이션에서 사용하고 싶거나 널리 사용할 수 있다고 판단될 때 코드베이스에서 라이브러리를 추출합니다.

또한, 저희는 Gem으로 코드를 추출함으로써 해당 Gem이 우리의 애플리케이션 코드에 숨겨진 의존성을 포함하지 않도록 보장합니다.

Gems는 비즈니스 로직에서 분리되어 개별적으로 개발될 수 있는 기능을 구현할 때 항상 사용해야 합니다.

새로운 Gem을 추출할 기회는 Rails 코드베이스의 lib/ 폴더에 있습니다.

우리의 lib/ 폴더에는 일반적이고 범용적인 코드, GitLab에 특화된 코드 및 코드베이스의 나머지 부분과 밀접하게 통합된 코드가 혼합되어 있습니다.

코드베이스의 일부를 Gem으로 추출할지 결정하기 위해 다음 질문들을 스스로에게 물어보세요:

  1. 이 코드는 개별적으로 처리하고 작은 프로젝트로 수행할 수 있는 범용적인가요?
  2. 이 코드를 모놀리스 외부에서 내부적으로 사용할 것으로 예상하나요?
  3. 우리가 이를 별도의 구성요소로 출시할 만큼 더 넓은 커뮤니티에 유용한가요?

위 질문 중 에 해당하는 경우, 새로운 Gem을 생성하는 것을 강력히 고려해야 합니다.

이미 존재하는 코드베이스에서 새로운 Gem을 생성하는 경우, 해당 Gem을 Gemfile에서 path: 구문과 함께 참조해야 합니다.

경고: 악의적인 사용자가 추출된 Gems의 이름을 무단으로 사용하는 것을 방지하려면 gem 이름을 예약하는 지침을 따르세요.

Gems 사용의 장점

Gems 사용은 코드 유지보수에 여러 가지 이점을 제공할 수 있습니다:

  • 코드 재사용성 - Gems는 단일 목적을 제공하는 격리된 라이브러리입니다. Gems를 사용하면 공통 함수를 단순한 패키지로 격리하여 서로 다른 어플리케이션에서 재사용되고 문서화되어 테스트되며 사용될 수 있습니다.

  • 모듈성 - Gems는 특정 기능을 캡슐화하여 격리함으로써 모듈화를 돕습니다. 이는 코드를 더 잘 정리하고 특정 모듈의 소유자를 명확히 정의하고, 특정 Gems를 유지 또는 업데이트하기 쉽게 만듭니다.

  • 소규모 - 격리된 함수 세트를 구현했기 때문에 Gems는 디자인 상으로 작습니다. 소규모 프로젝트는 이해하기, 확장하기 및 유지 관리하기 훨씬 쉽습니다.

  • 테스트 - 소규모이기 때문에 테스트 실행이 빨라집니다. 또한 Gems가 패키지되어 자주 변경되지 않기 때문에 테스트를 덜 자주 실행할 수 있으며 CI 테스트 시간을 개선할 수 있습니다.

Gem 명명

Gems는 세 가지 다른 경우로 나뉠 수 있습니다:

  • unique_gem: Gem이 GitLab과 관련된 내용을 포함하지 않는 경우에는 gem 이름에 gitlab을 포함하지 않습니다.
  • existing_gem-gitlab: 공개적으로 사용 가능한 gem을 fork하고 수정/확장하는 경우, Rubygems의 규칙에 따라 -gitlab 접미어를 추가하세요.
  • gitlab-unique_gem: GitLab 프로젝트의 문맥에서만 유용한 Gems의 경우, gitlab- 접두어를 포함하세요.

기존 Gems의 예시:

  • y-rb: Yrs의 Ruby 바인딩. Yrs “wires”는 Yjs 프레임워크의 Rust 포트입니다.
  • activerecord-gitlab: activerecord 공개 Gem에 GitLab 특정 패치를 추가합니다.
  • gitlab-rspecgitlab-utils: 특정 컨텍스트에서 도움이 되는 GitLab 특정 클래스들 또는 코드를 재사용합니다.

같은 저장소 내에서

기존 코드베이스에서 Gems를 추출할 때, GitLab 단일 저장소의 gems/에 저장하세요.

이는 Gems의 장점(모듈화된 코드, 개발 중에 테스트 실행이 빨라짐)을 제공하며 복잡성(저장소 간의 변경 사항 조정, 새 권한, 여러 프로젝트 등)을 방지합니다.

같은 저장소에 저장된 Gems는 Gemfile에서 path: 구문으로 참조되어야 합니다.

경고: 악의적인 사용자가 추출된 Gems의 이름을 무단으로 사용하는 것을 방지하려면 gem 이름을 예약하는 지침을 따르세요.

새로운 Gem 생성 및 사용

새로운 Gem을 추가하는 예시를 확인하세요: !121676.

  1. Gem 명명 규칙을 따라 Gem에 적합한 이름을 선택하세요.
  2. 다음과 같은 명령어로 gems/<name-of-gem>에 새로운 Gem을 생성하세요: bundle gem gems/<name-of-gem> --no-exe --no-coc --no-ext --no-mit.
  3. gems/<name-of-gem>.git 폴더를 다음 명령어로 삭제하세요: rm -rf gems/<name-of-gem>/.git.
  4. gems/<name-of-gem>/README.md를 편집하여 Gem에 대한 간단한 설명을 제공하세요.
  5. 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
    
  6. gems/<name-of-gem>/.rubocop.yml을 다음과 같이 업데이트하세요:

    inherit_from:
      - ../config/rubocop.yml
    
  7. 새로 추가된 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>"
      
  8. Gemfile에서 Gem을 다음과 같이 참조하세요:

    gem '<name-of-gem>', path: 'gems/<name-of-gem>'
    

Gem에 대한 종속성 지정

젬은 자체 Gemfile을 갖고 있지만, 실제 애플리케이션에서는 모놀리스 GitLab의 최상위 Gemfile을 사용하여 개별 Gemfile 대신 사용됩니다.

즉, 젬의 Gemfile은 최상위 Gemfile과 충돌이 발생할 수 있는 종속성 버전을 사용해서는 안 되며, 가능한 경우 동일한 종속성을 사용해야 합니다.

Rack의 예를 들면, 모놀리스에서 Rack 2를 사용하고 있고 Rack 3로 업그레이드하는 과정이라면, 개발하는 모든 젬은 Rack 2에서도 테스트되어야 하며, CI에서 별도 Gemfile을 사용하는 경우 Rack 3에서도 테스트되어야 합니다. 여기에서 예를 확인하세요.

이는 Rack에만 해당되는 것이 아니라, 모든 종속성에 해당됩니다.

젬 추출 예시

gitlab-utils는 GitLab 개발자들이 사용하는 공통 내재 기능을 구현하는 일련의 클래스를 포함하는 젬입니다. strong_memoize 또는 Gitlab::Utils.to_boolean과 같은 함수가 포함됩니다.

gitlab-database-schema-migrations는 저장소에 있는 데이터베이스 마이그레이션을 개선하는 Rails 프레임워크 확장을 포함하는 잠재적 젬입니다. 이는 GitLab 애플리케이션에 특정한 것이 아니며, 다른 프로젝트에서도 사용이 가능하거나 상류로 갈 수 있습니다.

gitlab-database-load-balancing은 데이터베이스 핸들링을 위한 GitLab 특정 로드 밸런싱을 구현하는 잠재적 젬입니다. 이는 꽤 복잡하며 매우 특정한 코드이므로, 이 복잡성을 거대한 단일 몰리식 코드베이스에서 제거하는 데 도움이 될 수 있도록 격리되고 테스트가 잘 된 젬에 이 복잡성을 유지하는 것이 도움이 될 것입니다.

gitlab-flipper는 기능 플래그를 지원하기 위한 모든 사용자 정의 확장을 구현하는 또 다른 잠재적 젬입니다. 시간이 지남에 따라, 단일 몰리식 코드베이스에 기능 플래그 사용을 확인하는 데 이용되었으며, 기능 플래그의 소유자를 추적하기 위한 일관성 있는 확인 및 다양한 도우미를 추가했습니다. 이는 실제로 GitLab의 비즈니스 로직에 속하지 않으며, 우리의 Flipper 구현을 더 잘 추적하고 GitLab Feature Flags를 개발환경에서 더 쉽게 변경할 수 있도록 도울 수 있습니다.

activerecord-gitlab은 GitLab 특정 Active Record 패치를 추가하는 젬입니다. 이러한 것을 별도로 관리하여 복잡성을 격리하는 것이 매우 바람직합니다.

기타 잠재적인 사용 사례

gitlab-ci-config.gitlab-ci.yml을 구문 분석하는 데 사용하는 모든 CI 코드를 포함하는 잠재적인 젬입니다. 현재 이 코드는 적절한 추상화 부족으로 인해 GitLab 애플리케이션과 약간 연동되어 있습니다. 그러나, 이것을 전용 젬으로 옮기면 GitLab 애플리케이션과 통합 처리를 다루기 위한 다양한 어댑터를 구축할 수 있게 됩니다. 예를 들어, 인터페이스는 includes:를 해결하기 위한 어댑터를 정의할 것입니다. 한 번 gitlab-ci-config 젬이 있다면, GitLab 내외부에서 사용할 수 있게 될 것입니다. 또한, GitLab CLI 내에서도 사용할 수 있게 됩니다.

외부 저장소에

일반적으로 이를 수행하기 전에 신중히 생각해야 합니다. 심각한 단점들이 있기 때문입니다.

외부 저장소에 저장된 젬은 반드시 Gemfile에서 version 구문으로 참조돼야 합니다. 언제나 RubyGems에 발행돼야 합니다.

예시

GitLab에서 여러 외부 젬을 사용합니다: - LabKit Ruby - GitLab Ruby Gems

잠재적인 단점

  • 젬(심지어 GitLab이 유지보수하는 젬도)은 주요 Rails 애플리케이션과 동일한 코드 검토 프로세스를 반드시 거치지 않습니다. 이는 특히 애플리케이션 보안에 매우 중요합니다.
  • 일관된 코드 검토 기준을 지원하는 Danger와 같은 도구를 비롯한 CI/CD를 처음부터 설정해야 합니다.
  • 코드를 별도 프로젝트로 추출하는 것은 기능을 변경하기 위해 최소한 두 개의 머지 리퀘스트가 필요합니다: 기능 변경을 위한 젬에서의 MR과 버전을 올리기 위한 레일 앱에서의 MR.
  • gitlab-rails와의 통합을 위해 두 번째 MR이 필요하므로, 통합 문제가 늦게 발견될 수 있습니다.
  • gitlab-rails와 비교해 리뷰어와 유지보수자의 수가 적기 때문에 코드 리뷰를 받는 데 시간이 더 오래 걸릴 수 있으며 “버스 요인”의 영향이 증가할 수 있습니다.
  • 새 젬 버전이 출시되는 방식에 대한 일관되지 않은 작업 흐름입니다. 현재로서는 라이브러리 유지자가 어떻게 작동할지를 결정할 수 있습니다.
  • 코드의 노출 및 가시성이 gitlab-rails보다 적기 때문에 지식 실로가 발생할 수 있습니다.
  • GitLab 리뷰어를 유지자로 승격시키는 잘 정의된 프로세스가 있습니다. 그러나 추출된 라이브러리에 대해 그렇지 않으며, 이는 코드 검토 기준의 기준을 낮출 가능성을 높입니다.
  • 젬의 사용 용도가 일반 커뮤니티의 요구와 일치하지 않을 수 있습니다. 일반적으로 우리가 자체 젬의 최신 버전을 사용하지 않을 때, 경고 표시일 수 있습니다.

잠재적인 이점

  • CI/CD가 작은 저장소에 대해 실행되기 때문에 더 빠른 피드백 루프가 가능합니다.
  • 프로젝트를 널리 사용하는 것과 외부 기여에서 이점을 얻을 수 있게 되므로, 프로젝트를 전체 커뮤니티에 노출시킬 수 있습니다.
  • 저장소 소유자는 변경 사항을 검토할 최적의 대상일 가능성이 매우 높기 때문에 gitlab-rails에서 적절한 리뷰어를 찾아야 하는 필요성을 줄일 수 있습니다.

Ruby 젬 만들고 발행하기

새로운 젬 프로젝트는 반드시 gitlab-org/ruby/gems 네임스페이스에 생성되어야 합니다:

  1. 젬에 적합한 이름을 결정합니다. GitLab 소유의 젬이라면, 젬 이름에 gitlab- 접두사를 사용하세요. 예를 들어, gitlab-sidekiq-fetcher.
  2. 필요한 경우 지역에서 젬을 만들거나 포크합니다.
  3. rubygems.org에 빈 0.0.1 버전의 젬을 발행하여 젬 이름이 예약되도록 합니다.
  4. 다음을 실행하여 새 젬에 gitlab_rubygems 사용자를 소유자로 추가하십시오:

    gem owner <gem-name> --add gitlab_rubygems
    
  5. 선택 사항입니다. 다음 사용자 중 일부 또는 모두를 공동 소유자로 추가하세요:
  6. 관련 개발자를 선택 사항으로 추가하세요.
  7. https://rubygems.org/gems/<gem-name>을 방문하여 젬이 성공적으로 발행되었고 gitlab_rubygems가 소유자로 등록되었는지 확인하세요.
  8. gitlab-org/ruby/gems 그룹에서 프로젝트를 생성하세요:
    1. 새 프로젝트에 대한 지침을 따르세요.
    2. CI/CD 구성 설정에 대한 지침을 따르세요.
    3. 다음을 .gitlab-ci.yml에 추가하여 새 젬 버전을 릴리스 및 발행하기 위한 gem-release CI 구성 요소를 추가하세요:

      include:
        - component: $CI_SERVER_FQDN/gitlab-org/components/gem-release/gem-release@~latest
      

      이 작업은 젬 빌드 및 발행(젬 패키지를 게시하기 위해 gitlab-org/ruby/gems 그룹에서 상속된 gitlab_rubygems Rubygems.org API 토큰을 사용) 및 태그, 릴리스를 생성하고 릴리스 노트를 채우기 위해 변경 로그 데이터 생성 API 엔드포인트를 이용하는 작업을 합니다.

      변경 로그 항목을 생성해야 하는 경우의 시간과 방법에 대한 지침은 전용 변경 로그 항목 페이지를 참조하세요. GitLab 프로젝트와 일관성 있는 방식에 따라, 젬 프로젝트는 동일한 내용의 변경 로그 YAML 구성 파일을 .gitlab/changelog_config.yml에 정의할 수 있습니다.

    4. 릴리스 프로세스를 간소화하기 위해 .gitlab/merge_request_templates/Release.md MR 템플릿을 만들 수 있으며, 이를 통해 실제 젬 이름으로 변경하세요.
    5. 프로젝트 게시 지침을 따르세요.

주의: 어떤 경우에는 젬을 독자적인 네임스페이스로 이동해야 하는 상황이 발생할 수 있습니다. 예를 들어, 별도 라이브러리로 플러그인을 가질 것이라는 것(즉, 별도의 프로젝트를 가질 것이라는 것)이 명백한 경우, 또는 GitLab 팀 멤버 외부 사용자가 이 프로젝트를 유지보수할 것으로 예상되는 경우가 있습니다. 후자의 상황(외부 GitLab 사용자가 유지보수자인 경우)은 현재 GitLab에서 일할 때 이 프로젝트를 유지보수하고자 하는 사람이 소유권을 유지할 때 적용될 수 있습니다.

vendor/gems/

vendor/의 목적은 외부 의존성을 GitLab 모노 레포에 통합하는 것이지만, 외부 저장소를 가지고 있더라도 단순함을 위해 이를 모노 레포에 저장하려고 합니다.

  • vendor/gems/는 스크립트를 통해 외부 저장소에서 가져오거나 수동으로 가져올 때에만 사용해야 합니다.
  • vendor/gems/는 내부 젬을 저장하는 데 사용해서는 안됩니다.
  • vendor/gems/는 GitLab 모노 레포에서 빌드 가능하도록 수정된 패치를 허용할 수 있습니다.
  • gems/는 GitLab 모노 레포의 일부인 모든 내부 젬을 저장하는 데 사용해야 합니다.
  • RubyGems는 GitLab 모노 레포의 gems/에 없는 모든 외부 저장된 종속성에 사용해야 합니다.

vendor/gems/에 있는 기존 젬 처리

  • 외부 저장소가 없고 현재 vendor/gems/에 저장된 내부 젬에 대해:

    • 다른 저장소에서 사용되는 내부 젬의 경우:

      • 해당 젬을 자체 저장소로 이전합니다.
      • RubyGems를 통해 시작하거나 계속해서 게시할 것입니다.
      • 해당 젬은 Gemfile에 버전으로 참조되고 RubyGems에서 가져올 것입니다.
    • 모노 레포에서만 사용되는 젬의 경우:

      • RubyGems에 새 버전을 더 이상 게시하지 않을 것입니다.
      • 이전에 게시된 버전은 응용 프로그램이 의존할 수 있으므로 이미 게시된 RubyGems에서 가져오지 않을 것입니다.
      • 해당 젬은 gems/로 이동할 것입니다.
      • 해당 젬은 Gemfilepath:를 통해 참조될 것입니다.
  • 모노 레포에서 외부로 가져온 vendor/gems/에 대해:

    • 아직 상류로 올리지 못하거나 상류로 올리지 않을 수 있는 몇 가지 수정이 필요한 경우, 해당 저장소에 유지할 것입니다.
    • 외부에서 게시될 수 있으므로 vendored gems는 예상됩니다.
    • 해당 젬은 우리가 RubyGems에 게시하지 않을 것입니다.
    • 해당 젬은 RubyGems에 의존할 수 없기 때문에 Gemfilepath:를 통해 참조될 것입니다.

rubygems.org에 대한 고려 사항

젬 이름 예약

새 젬을 포함하는 공개 코드를 게시하기 전에 젬 이름 스쿼터가 젬 이름을 가져가는 것을 피하기 위해 젬 이름을 예약할 수 있습니다.

젬 이름을 예약하려면 루비 젬 만들고 게시하기 단계를 따르되 다음과 같이 변경하세요:

  • 버전으로 0.0.0을 사용합니다.
  • 내용이 raise "Reserved for GitLab"lib/NAME.rb 단일 파일을 포함합니다.
  • buildpublish를 수행하고 성공 여부를 확인하기 위해 https://rubygems.org/gems/를 확인하세요.

계정 생성

GitLab에서 작업 목적으로 RubyGems.org에 계정을 만들 계획이라면, 기업 이메일 계정을 사용해야 합니다.

유지자 및 계정 변경

계정 이메일이나 비밀번호, 젬 소유자, 젬 삭제와 같은 모든 변경은 직접적으로 책임 있는 팀에 사전에 커뮤니케이션되어야 합니다. (이슈나 슬랙, #rubygems, #ruby, #development 채널을 통해)