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:50volume_threshold:10sleep_window:90time_window:60