InfoGrab DocsInfoGrab Docs

GitLab 유틸리티

요약

개발을 돕기 위해 여러 유틸리티를 만들어 두었습니다. executable.rb를 참고합니다. 서비스 클래스에 클래스 수준 execute 메서드를 추가합니다. 두 방식 모두 MyService.new(project, user).execute의 단축 표현으로 MyService.execute(project, user)를 지원합니다.

개발을 돕기 위해 여러 유틸리티를 만들어 두었습니다.

Executable#

executable.rb를 참고합니다.

  • 서비스 클래스에 클래스 수준 execute 메서드를 추가합니다. 이 메서드는 주어진 인수로 새 인스턴스를 만들고 그 인스턴스에서 execute를 호출합니다.

    # Before
    class MyService
      def self.execute(project, user)
        new(project, user).execute
      end
    end
    
    # After
    class MyService
      include Gitlab::Utils::Executable
    end
    

    두 방식 모두 MyService.new(project, user).execute의 단축 표현으로 MyService.execute(project, user)를 지원합니다.

  • 호출 후에 서비스 인스턴스가 필요하지 않은 경우 이 믹스인을 사용합니다. 인스턴스가 필요한 호출자는 new를 그대로 사용할 수 있습니다.

  • 이 메서드는 ...로 인수를 전달하므로 initialize와 어긋날 일이 없습니다. 인수 목록을 다시 적는 위임 메서드는 initialize가 바뀌면 어긋납니다.

  • include는 싱글턴 조상 체인에서 모듈을 상위 클래스보다 앞에 둡니다. 따라서 이 믹스인은 상위 클래스에서 상속한 클래스 수준 execute를 재정의합니다.

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 # Override check happens here
        def execute
        end
      end
      

      모듈에서도 동작합니다.

      module Extension
        extend ::Gitlab::Utils::Override
      
        override :execute # Modules do not check this immediately
        def execute
        end
      end
      
      class Derived < Base
        prepend Extension # Override check happens here, not in the module
      end
      

      이 검사는 다음 두 경우에만 일어납니다.

      • 재정의 메서드가 클래스에 정의된 경우입니다.
      • 재정의 메서드가 모듈에 정의되어 있고 그 모듈이 클래스나 모듈에 prepend 된 경우입니다.

      실제로 메서드를 재정의할 수 있는 것은 클래스 또는 prepend 된 모듈뿐이기 때문입니다. 모듈을 다른 모듈에 include 하거나 extend 하는 것으로는 아무것도 재정의하지 못합니다.

ActiveSupport::Concern, prepend, class_methods와의 상호작용#

클래스 메서드를 포함하는 ActiveSupport::Concern을 사용하면 기대한 결과가 나오지 않습니다. ActiveSupport::Concern 이 일반 Ruby 모듈과 다르게 동작하기 때문입니다.

prepend를 쓸 수 있도록 ActiveSupport::Concern을 패치한 Prependable 이 이미 있으므로, 이것이 override 및 class_methods와 어떻게 상호작용하는지가 문제가 됩니다. 우회 방법으로 ClassMethods를 정의 중인 Prependable 모듈에 extend 합니다.

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

다음 코드 블록은 이 우회 방법의 효과를 보여 주는 예시입니다.

module Base
  extend ActiveSupport::Concern

  class_methods do
    def f
    end
  end
end

module Derived
  include Base
end

# Without the workaround
Base.f    # => NoMethodError
Derived.f # => nil

# With the workaround
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를 사용합니다.

    # bad
    def expensive_method(arg)
      # ...
    end
    strong_memoize_attr :expensive_method
    
    # good
    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 문서를 참고합니다.

TokenAuthenticatable#

TokenAuthenticatable 문서를 참고합니다.

CircuitBreaker#

Gitlab::CircuitBreaker는 서킷 브레이커 보호 아래에서 코드를 실행해야 하는 모든 클래스를 감쌀 수 있습니다. 코드 블록을 서킷 브레이커 기능으로 감싸는 run_with_circuit 메서드를 제공하며, 이는 연쇄 장애를 막고 시스템 복원력을 높이는 데 도움이 됩니다. 서킷 브레이커 패턴에 대한 자세한 내용은 다음을 참고합니다.

CircuitBreaker 사용#

CircuitBreaker 래퍼는 다음과 같이 사용합니다.

class MyService
  def call_external_service
    Gitlab::CircuitBreaker.run_with_circuit('ServiceName') do
      # Code that interacts with external service goes here

      raise Gitlab::CircuitBreaker::InternalServerError # if there is an issue
    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

GitLab 유틸리티

GitLab v19.4
원문 보기

요약

개발을 돕기 위해 여러 유틸리티를 만들어 두었습니다. executable.rb를 참고합니다. 서비스 클래스에 클래스 수준 execute 메서드를 추가합니다. 두 방식 모두 MyService.new(project, user).execute의 단축 표현으로 MyService.execute(project, user)를 지원합니다.

개발을 돕기 위해 여러 유틸리티를 만들어 두었습니다.

Executable#

executable.rb를 참고합니다.

  • 서비스 클래스에 클래스 수준 execute 메서드를 추가합니다. 이 메서드는 주어진 인수로 새 인스턴스를 만들고 그 인스턴스에서 execute를 호출합니다.

    # Before
    class MyService
      def self.execute(project, user)
        new(project, user).execute
      end
    end
    
    # After
    class MyService
      include Gitlab::Utils::Executable
    end
    

    두 방식 모두 MyService.new(project, user).execute의 단축 표현으로 MyService.execute(project, user)를 지원합니다.

  • 호출 후에 서비스 인스턴스가 필요하지 않은 경우 이 믹스인을 사용합니다. 인스턴스가 필요한 호출자는 new를 그대로 사용할 수 있습니다.

  • 이 메서드는 ...로 인수를 전달하므로 initialize와 어긋날 일이 없습니다. 인수 목록을 다시 적는 위임 메서드는 initialize가 바뀌면 어긋납니다.

  • include는 싱글턴 조상 체인에서 모듈을 상위 클래스보다 앞에 둡니다. 따라서 이 믹스인은 상위 클래스에서 상속한 클래스 수준 execute를 재정의합니다.

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 # Override check happens here
        def execute
        end
      end
      

      모듈에서도 동작합니다.

      module Extension
        extend ::Gitlab::Utils::Override
      
        override :execute # Modules do not check this immediately
        def execute
        end
      end
      
      class Derived < Base
        prepend Extension # Override check happens here, not in the module
      end
      

      이 검사는 다음 두 경우에만 일어납니다.

      • 재정의 메서드가 클래스에 정의된 경우입니다.
      • 재정의 메서드가 모듈에 정의되어 있고 그 모듈이 클래스나 모듈에 prepend 된 경우입니다.

      실제로 메서드를 재정의할 수 있는 것은 클래스 또는 prepend 된 모듈뿐이기 때문입니다. 모듈을 다른 모듈에 include 하거나 extend 하는 것으로는 아무것도 재정의하지 못합니다.

ActiveSupport::Concern, prepend, class_methods와의 상호작용#

클래스 메서드를 포함하는 ActiveSupport::Concern을 사용하면 기대한 결과가 나오지 않습니다. ActiveSupport::Concern 이 일반 Ruby 모듈과 다르게 동작하기 때문입니다.

prepend를 쓸 수 있도록 ActiveSupport::Concern을 패치한 Prependable 이 이미 있으므로, 이것이 override 및 class_methods와 어떻게 상호작용하는지가 문제가 됩니다. 우회 방법으로 ClassMethods를 정의 중인 Prependable 모듈에 extend 합니다.

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

다음 코드 블록은 이 우회 방법의 효과를 보여 주는 예시입니다.

module Base
  extend ActiveSupport::Concern

  class_methods do
    def f
    end
  end
end

module Derived
  include Base
end

# Without the workaround
Base.f    # => NoMethodError
Derived.f # => nil

# With the workaround
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를 사용합니다.

    # bad
    def expensive_method(arg)
      # ...
    end
    strong_memoize_attr :expensive_method
    
    # good
    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 문서를 참고합니다.

TokenAuthenticatable#

TokenAuthenticatable 문서를 참고합니다.

CircuitBreaker#

Gitlab::CircuitBreaker는 서킷 브레이커 보호 아래에서 코드를 실행해야 하는 모든 클래스를 감쌀 수 있습니다. 코드 블록을 서킷 브레이커 기능으로 감싸는 run_with_circuit 메서드를 제공하며, 이는 연쇄 장애를 막고 시스템 복원력을 높이는 데 도움이 됩니다. 서킷 브레이커 패턴에 대한 자세한 내용은 다음을 참고합니다.

CircuitBreaker 사용#

CircuitBreaker 래퍼는 다음과 같이 사용합니다.

class MyService
  def call_external_service
    Gitlab::CircuitBreaker.run_with_circuit('ServiceName') do
      # Code that interacts with external service goes here

      raise Gitlab::CircuitBreaker::InternalServerError # if there is an issue
    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