GitLab 유틸리티

우리는 개발을 쉽게 도와줄 여러 유틸리티를 개발했습니다:

MergeHash

merge_hash.rb를 참조하세요:

  • 해시 배열을 깊게 병합합니다:

    Gitlab::Utils::MergeHash.merge(
      [{ hello: ["world"] },
       { hello: "Everyone" },
       { hello: { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzien dobry'] } },
        "Goodbye", "Hallo"]
    )
    

    결과는:

    [
      {
        hello:
          [
            "world",
            "Everyone",
            { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzien dobry'] }
          ]
      },
      "Goodbye"
    ]
    
  • 해시의 모든 키와 값을 배열로 추출합니다:

    Gitlab::Utils::MergeHash.crush(
      { hello: "world", this: { crushes: ["an entire", "hash"] } }
    )
    

    결과는:

    [:hello, "world", :this, :crushes, "an entire", "hash"]
    

Override

override.rb를 참조하세요:

  • 이 유틸리티는 한 메서드가 다른 메서드를 오버라이드할 수 있는지 확인하는 데 도움을 줄 수 있습니다. 이는 Java의 @Override 주석 또는 Scala의 override 키워드와 동일한 개념입니다. 그러나 우리는 ENV['STATIC_VERIFICATION']가 설정되어 있을 때만 이 검사를 실행하여 프로덕션 런타임 오버헤드를 피합니다. 이는 다음을 확인하는 데 유용합니다:

    • 오버라이드 메서드에서 오타가 있는지.
    • 오버라이드된 메서드를 이름을 바꾼 경우, 원래 오버라이드 메서드가 무의미해지는지.

      간단한 예는 다음과 같습니다:

      class Base
        def execute
        end
      end
      
      class Derived < Base
        extend ::Gitlab::Utils::Override
      
        override :execute # 여기서 오버라이드 검사 발생
        def execute
        end
      end
      

      이는 모듈에서도 작동합니다:

      module Extension
        extend ::Gitlab::Utils::Override
      
        override :execute # 모듈에서는 즉시 검사를 하지 않습니다
        def execute
        end
      end
      
      class Derived < Base
        prepend Extension # 여기서 오버라이드 검사 발생, 모듈에서는 아님
      end
      

      주의할 점은 검사 발생 조건이 다음 중 하나일 때만 발생한다는 것입니다:

      • 오버라이딩 메서드가 클래스 내에 정의되어 있을 때, 또는:
      • 오버라이딩 메서드가 모듈 내에 정의되어 있고, 클래스 또는 모듈에 prepend 될 때.

      실제로 메서드를 오버라이드할 수 있는 것은 클래스 또는 prepended 모듈뿐입니다. 모듈을 다른 모듈에 포함하거나 확장하는 것은 아무 것도 오버라이드할 수 없습니다.

ActiveSupport::Concern, prependclass_methods와의 상호작용

클래스 메서드를 포함하는 ActiveSupport::Concern를 사용할 때, 일반 Ruby 모듈처럼 예상한 결과를 얻지 못합니다.

우리는 이미 prepend를 가능하게 하기 위해 ActiveSupport::Concern에 대한 패치로서 Prependable을 가지고 있으며, 이는 overrideclass_methods와 어떻게 상호작용하는지에 영향을 미칩니다. 해결 방법으로, define Prependable 모듈에 extend ClassMethods를 추가합니다.

이렇게 하면 위에서 언급한 컨텍스트에서 사용된 class_methods를 검증하기 위해 override를 사용할 수 있습니다. 이 해결 방법은 검증을 실행할 때만 적용되고, 애플리케이션을 실행할 때는 적용되지 않습니다.

다음은 이 해결 방법의 효과를 보여주는 예제 코드 블록입니다:

module Base
  extend ActiveSupport::Concern

  class_methods do
    def f
    end
  end
end

module Derived
  include Base
end

# 해결 방법 없이
Base.f    # => NoMethodError
Derived.f # => nil

# 해결 방법 적용 시
Base.f    # => nil
Derived.f # => nil

StrongMemoize

strong_memoize.rb를 참조하세요:

  • 값이 nil 또는 false일 때도 메모이즈합니다.

    우리는 종종 @value ||= compute를 사용합니다. 그러나 compute가 결국 nil을 반환할 수 있는 경우에는 다시 계산하고 싶지 않을 때 잘 작동하지 않습니다. 대신, defined?를 사용하여 값이 설정되었는지 확인할 수 있습니다. 이러한 패턴을 작성하는 것은 번거로우며, StrongMemoize는 이러한 패턴을 사용할 수 있도록 도와줍니다.

    다음과 같은 패턴을 작성하는 대신:

    class Find
      def result
        return @result if defined?(@result)
    
        @result = search
      end
    end
    

    아래와 같이 작성할 수 있습니다:

    class Find
      include Gitlab::Utils::StrongMemoize
    
      def result
        search
      end
      strong_memoize_attr :result
    
      def enabled?
        Feature.enabled?(:some_feature)
      end
      strong_memoize_attr :enabled?
    end
    

    매개변수가 있는 메서드에 strong_memoize_attr를 사용하는 것은 지원되지 않습니다.

    override와 결합할 때 작동하지 않으며 잘못된 결과를 메모이즈할 수 있습니다.

    대신 strong_memoize_with를 사용하세요.

    # 나쁨
    def expensive_method(arg)
      # ...
    end
    strong_memoize_attr :expensive_method
    
    # 좋음
    def expensive_method(arg)
      strong_memoize_with(:expensive_method, arg) do
        # ...
      end
    end
    

    인자를 받는 메서드를 메모이즈하는 데 도움이 되는 strong_memoize_with도 있습니다.

    인자가 가능한 값이 적거나 반복되는 인자가 있는 루프에서 사용하는 메서드에 사용해야 합니다.

    class Find
      include Gitlab::Utils::StrongMemoize
    
      def result(basic: true)
        strong_memoize_with(:result, basic) do
          search(basic)
        end
      end
    end
    
  • 메모이제이션 지우기

    class Find
      include Gitlab::Utils::StrongMemoize
    end
    
    Find.new.clear_memoization(:result)
    

RequestCache

request_cache.rb를 참조하세요.

이 모듈은 RequestStore에 값을 캐싱하는 간단한 방법을 제공합니다.

캐시 키는 클래스 이름, 메서드 이름, 선택적으로 사용자 정의된 인스턴스 수준 값, 선택적으로 사용자 정의된 메서드 수준 값 및 선택적 메서드 인수를 기반으로 합니다.

인스턴스 수준의 사용자 정의 값만 사용하는 간단한 예는 다음과 같습니다:

class UserAccess
  extend Gitlab::Cache::RequestCache

  request_cache_key do
    [user&.id, project&.id]
  end

  request_cache def can_push_to_branch?(ref)
    # ...
  end
end

이 방식으로 can_push_to_branch?의 결과는 캐시 키를 기반으로 RequestStore.store에 캐시됩니다.

RequestStore가 현재 활성화되어 있지 않은 경우, 해시에 저장되며, 인스턴스 변수에 저장되어 캐시 로직은 동일하게 유지됩니다.

우리는 또한 서로 다른 메서드에 대해 서로 다른 전략을 설정할 수 있습니다:

class Commit
  extend Gitlab::Cache::RequestCache

  def author
    User.find_by_any_email(author_email)
  end
  request_cache(:author) { author_email }
end

ReactiveCaching

ReactiveCaching에 대한 문서를 읽어보세요.

CircuitBreaker

Gitlab::CircuitBreaker는 회로 차단기 보호 기능으로 코드를 실행해야 하는 모든 클래스에 감쌀 수 있습니다.

회로 차단기 기능으로 코드 블록을 감싸는 run_with_circuit 메서드를 제공하며, 이는 연쇄 실패를 방지하고 시스템 복원력을 향상시키는 데 도움이 됩니다.

회로 차단기 패턴에 대한 자세한 정보는 다음을 참조하세요:

CircuitBreaker 사용

CircuitBreaker 랩퍼를 사용하려면:

class MyService
  def call_external_service
    Gitlab::CircuitBreaker.run_with_circuit('ServiceName') do
      # 외부 서비스와 상호작용하는 코드가 여기에 있습니다.

      raise Gitlab::CircuitBreaker::InternalServerError # 문제가 발생하면
    end
  end
end

call_external_service 메서드는 외부 서비스와 상호작용하는 예제 메서드입니다.

외부 서비스와 상호작용하는 코드를 run_with_circuit로 감싸서, 메서드는 회로 차단기 내에서 실행됩니다.

메서드는 InternalServerError 오류를 발생시켜야 하며, 코드 블록 실행 중 발생하면 오류 임계값에 포함됩니다.

회로 차단기는 오류 수와 요청 비율을 추적하며, 설정된 오류 임계값이나 볼륨 임계값에 도달하면 회로를 엽니다.

회로가 열리면 이후 요청은 코드 블록을 실행하지 않고 빠르게 실패하며, 회로 차단기는 주기적으로 서비스의 가용성을 테스트하기 위해 소수의 요청을 허용한 후 다시 회로를 닫습니다.

구성

각 고유 회로에 대한 서비스 이름을 지정해야 하며, 이는 캐시 키로 사용됩니다. 이는 회로를 식별하는 CamelCase 문자열이어야 합니다.

회로 차단기는 회로별로 재정의할 수 있는 기본값을 가지고 있으며, 예를 들어:

Gitlab::CircuitBreaker.run_with_circuit('ServiceName', options = { volume_threshold: 5 }) do
  ...
end

기본값은 다음과 같습니다:

  • exceptions: [Gitlab::CircuitBreaker::InternalServerError]
  • error_threshold: 50
  • volume_threshold: 10
  • sleep_window: 90
  • time_window: 60