Ruby 3 주의사항

이 섹션은 Ruby 3 지원을 작업하면서 발견한 여러 문제에 대해 문서화하고, 숨겨진 버그나 이해하기 어려운 테스트 실패로 이어지는 문제점을 다룹니다. 정기적으로 Ruby 코드를 작성하는 GitLab 기여자는 이러한 문제에 익숙해지도록 권장합니다.

Ruby 3 언어와 표준 라이브러리의 완전한 변경 디렉터리을 보려면 Ruby Changes를 참조하세요.

Hash#each가 항상 람다에 2개 요소 배열을 일관되게 생성

다음 코드 스니펫을 고려해보세요.

def foo(a, b)
  p [a, b]
end

def bar(a, b = 2)
  p [a, b]
end

foo_lambda = method(:foo).to_proc
bar_lambda = method(:bar).to_proc

{ a: 1 }.each(&foo_lambda)
{ a: 1 }.each(&bar_lambda)

Ruby 2.7에서 이 프로그램의 출력은 필수 인수의 수에 따라 해시 항목을 람다에 전달하는 방식이 다른 것을 시사합니다.

# Ruby 2.7
{ a: 1 }.each(&foo_lambda) # prints [:a, 1]
{ a: 1 }.each(&bar_lambda) # prints [[:a, 1], 2]

Ruby 3은 이 동작을 일관되게 만들고 항상 해시 항목을 단일 [key, value] 배열로 생성하려고 시도합니다.

# Ruby 3.0
{ a: 1 }.each(&foo_lambda) # `foo': wrong number of arguments (given 1, expected 2) (ArgumentError)
{ a: 1 }.each(&bar_lambda) # prints [[:a, 1], 2]

2.7와 3.0 모두에서 작동하는 코드를 작성하려면 다음 옵션을 고려하세요. - 항상 람다 본문을 블록으로 전달합니다: { a: 1 }.each { |a, b| p [a, b] }. - 람다 인수를 분해합니다: { a: 1 }.each(&->((a, b)) { p [a, b] }).

항상 블록을 명시적으로 전달하는 것을 권장하며, 블록 매개변수로 두 개의 필수 인수를 선호합니다.

더 많은 정보는 Ruby issue 12706를 참조하십시오.

Symbol#to_proc가 람다와 일관된 서명 메타데이터를 반환

Ruby에서 일반적인 관용구 중 하나는 &:<symbol> 약식을 사용하여 Proc 객체를 얻고 고차원 함수에 전달하는 것입니다.

[1, 2, 3].each(&:to_s)

Ruby는 &:<symbol>Symbol#to_proc로 변환합니다. 이를 호출할 때 메서드 _수신자_를 첫 번째 인수(여기서 Integer)로 사용하고, 메서드 _인수_를 나머지 인수로 사용합니다(여기서는 없음).

Ruby 2.7과 Ruby 3에서 동일하게 작동합니다. Ruby 3이 다른 점은 이 Proc 객체를 캡처하고 호출 서명을 검사할 때입니다. 이는 DSL을 작성하거나 메타 프로그래밍의 다른 형태를 사용할 때 자주 발생합니다.

p = :foo.to_proc # 일반적으로 `&:foo`를 통해 발생함

# Ruby 2.7: [[:rest]] (-1) 출력
# Ruby 3.0: [[:req], [:rest]] (-2) 출력
puts "#{p.parameters} (#{p.arity})"

Ruby 2.7은 이 Proc 객체에 대해 필수 매개변수가 없고 선택적 매개변수가 하나 있음을 보고하는 반면, Ruby 3은 하나의 필수 매개변수와 선택적 매개변수 하나가 있다고 보고합니다. Ruby 2.7은 잘못되었습니다. 첫 번째 인수는 반드시 전달되어야 하며, 이것은 Proc 객체가 표현하는 메서드의 수신자이기 때문에 메서드는 수신자 없이 호출될 수 없습니다.

Ruby 3은 이를 정정했습니다. Proc 객체의 매개변수 디렉터리이나 서명을 테스트하는 코드는 이제 망가질 수 있으며 업데이트되어야 합니다.

더 많은 정보는 Ruby issue 16260을 참조하십시오.

OpenStruct은 필드를 느리게 평가하지 않음

Ruby 3에서 OpenStruct 구현이 일부 변경되어 동작이 변경되었습니다. Ruby 2.7에서 OpenStruct은 메서드가 처음 액세스될 때 메서드를 느리게 정의합니다. Ruby 3.0에서는 초기화 중에 이러한 메서드를 즉시 정의하므로, OpenStruct를 상속하고 이러한 메서드를 재정의하는 클래스가 손상될 수 있습니다.

이러한 이유로 OpenStruct를 상속하지 마십시오. 이상적으로는 아예 사용하지 않는 것이 좋습니다. 새 코드를 작성할 때는 더 간단한 구현인 Struct를 사용하세요. 더 유연하지만 구현이 간단합니다.

RegexpRange 인스턴스는 동결됨

이제 RegexpRange 인스턴스를 명시적으로 동결할 필요가 없습니다. Ruby 3는 이들을 자동으로 생성 시 동결합니다.

이것에는 미묘한 부작용이 있습니다. 이러한 유형에 대한 메서드 호출을 스텁하려는 테스트는 이제 RSpec이 동결된 객체에 대한 메서드 호출을 스텁할 수 없어 오류가 발생합니다.

# Ruby 2.7: 작동
# Ruby 3.0: 오류: "can't modify frozen object"
allow(subject.function_returning_range).to receive(:max).and_return(42)

동결된 객체에 대한 메서드 호출을 스텁하는 테스트를 다시 작성하세요. 위 예시는 다음과 같이 다시 작성할 수 있습니다.

# 모든 Ruby 버전에서 작동
allow(subject).to receive(:function_returning_range).and_return(1..42)

Ruby 3.0.2에서 테이블 테스트 실패

Ruby 3.0.2에는 정수 값을 가진 테이블 값이 있을 때 테이블 테스트가 실패하는 알려진 버그가 있습니다. 원인은 issue 337614에 문서화되어 있습니다. 이 문제는 Ruby에서 수정되었으며 해당 수정은 Ruby 3.0.3에 포함될 예정입니다.

이 문제는 패치되지 않은 Ruby 3.0.2를 실행하는 사용자에게만 영향을 미칩니다. 이는 매뉴얼으로 또는 asdf와 같은 도구를 통해 Ruby를 설치한 경우일 가능성이 높습니다. gitlab-development-kit (GDK) 사용자도 이 문제의 영향을 받습니다.

빌드 이미지는 해당 버그를 해결하는 패치 세트를 포함하고 있어 영향을 받지 않습니다.

더 이상 필요하지 않은 기능을 스텁하면 Deprecations이 DeprecationToolkit에서 포착되지 않음

Ruby 2에서 사용이 중지된 기능이나 Ruby 3에서 제거된 기능을 사용할 때 deprecation_toolkit을 통해 빠르게 실패하도록 의존합니다. Ruby 3.0에서의 Ruby 3.0의 위치 및 키워드 인수 분리에 관련된 전환 중에 발생하는 일반적인 문제 중 하나는 메소드가 스텁된 경우에 Deprecations가 포착되지 않습니다. 우리는 테스트에서 이러한 경고를 자동으로 감지하지만, Kernel#warn이 경고를 생성하도록 의존하므로 이를 스텁하면 경고가 발생하지 않게 되어 deprecation_toolkit은 사용중인 코드를 보지 못합니다. 구현을 스텁하면 이 경고가 제거되어 결과적으로 빌드가 성공하게 됩니다.

더 많은 맥락을 보려면 issue 364099를 참조하세요.

irbrails console에서의 테스트

또 다른 함정은 irb/rails c에서의 테스트가 더 이상 irb에서 사용되는 Ruby 2.7.x에서는 버그 때문에 버전 정보 경고를 숨기게 된다는 것입니다. 이 버그 때문에 버전 정보 경고가 표시되지 않습니다.

코드 작성 및 코드 리뷰를 수행할 때, f({k: v}) 형식의 메소드 호출에 특별한 주의를 기울이세요. Ruby 2에서는 fHash 또는 키워드 인수를 사용하는 경우에만 유효하지만, Ruby 3에서는 fHash를 사용하는 경우에만 유효합니다. Ruby 3 호환성을 위해, f가 키워드 인수를 사용하는 경우 다음 중 하나로 변경되어야 합니다:

  • f(**{k: v})
  • f(k: v)

RSpec with 인자 매처는 간소화된 Hash 구문에서 실패합니다

Ruby 3에서 키워드 인수(“kwargs”)가 일급 개념이기 때문에 키워드 인수가 더 이상 내부 Hash 인스턴스로 변환되지 않습니다. 이로 인해 수신자가 위치 인수 옵션 해시를 사용하는 경우 RSpec 메소드 인자 매처가 실패합니다.

def m(options={}); end
expect(subject).to receive(:m).with(a: 42)

Ruby 3에서는 이 기대사항이 다음과 같은 오류로 실패합니다:

  Failure/Error:
     
     #<subject> received :m with unexpected arguments
       expected: ({:a=>42})
            got: ({:a=>42})

이는 RSpec가 여기서 kwargs 인자 매처를 사용하지만 메소드가 해시를 취하고 있기 때문에 발생합니다. Ruby 2에서는 a: 42가 먼저 해시로 변환되고 RSpec가 해시 인자 매처를 사용하기 때문에 작동합니다.

해결책은 간소화된 구문을 사용하지 않고 옵션 해시를 취하는 메소드를 알고 있는 경우 실제 Hash를 전달하는 것입니다:

# 키-값 쌍 주위의 중괄호에 주목하세요.
expect(subject).to receive(:m).with({ a: 42 })

자세한 내용은 RSpec의 공식 이슈 보고서를 참조하세요.