Ruby 3의 주의사항

이 섹션에서는 Ruby 3 지원을 작업하면서 발견한 여러 문제를 문서화하였으며, 이는 미세한 버그 또는 이해하기 어려운 테스트 실패로 이어졌습니다. 우리는 정기적으로 Ruby 코드를 작성하는 모든 GitLab 기여자가 이러한 문제에 익숙해지기를 권장합니다.

Ruby 3 언어와 표준 라이브러리에 대한 변경 목록을 찾으려면 Ruby Changes를 참조하세요.

Hash#each는 항상 2-element 배열을 람다에 전달합니다

다음 코드 스니펫을 고려하세요:

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: prints [[:rest]] (-1)
# Ruby 3.0: prints [[:req], [:rest]] (-2)
puts "#{p.parameters} (#{p.arity})"

Ruby 2.7은 이 Proc 객체에 대해 필수 매개변수 0개와 선택적 매개변수 1개를 보고하는 반면, Ruby 3는 필수 매개변수 1개와 선택적 매개변수 1개를 보고합니다. Ruby 2.7은 잘못되었습니다: 첫 번째 인수는 항상 전달되어야 하며, 이는 Proc 객체가 나타내는 메서드의 받는 주체입니다. 메서드는 받는 주체 없이 호출될 수 없습니다.

Ruby 3은 이를 수정합니다: Proc 객체의 아리티나 매개변수 목록을 테스트하는 코드는 이제 깨질 수 있으며, 업데이트해야 합니다.

자세한 정보는 Ruby issue 16260를 참조하세요.

OpenStruct는 필드를 지연 평가하지 않습니다

OpenStruct 구현은 Ruby 3에서 부분적으로 재작성되어 행동 변경이 있었습니다.

Ruby 2.7에서는 OpenStruct가 메서드를 처음 접근할 때 지연적으로 정의합니다.

하지만 Ruby 3.0에서는 초기화 시 이러한 메서드를 즉시 정의하므로, OpenStruct에서 상속받아 이러한 메서드를 재정의하는 클래스에 문제가 발생할 수 있습니다.

이러한 이유로 OpenStruct에서 상속하지 마세요. 이상적으로는 전혀 사용하지 않는 것이 좋습니다.

OpenStruct문제가 있다고 간주됩니다.

새로운 코드를 작성할 때는 구현이 더 간단하지만 유연성이 떨어지는 Struct를 선호하세요.

RegexpRange 인스턴스는 동결됩니다

이제 Regexp 또는 Range 인스턴스를 명시적으로 동결할 필요가 없습니다.

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에는 테이블 테스트가 정수 값으로 구성된 테이블 값이 있을 때 실패하는 알려진 버그가 있습니다.

이유는 문제 337614에 문서화되어 있습니다.

이 문제는 Ruby에서 수정되었으며, 그 수정 사항은 Ruby 3.0.3에 포함될 것으로 예상됩니다.

이 문제는 패치되지 않은 Ruby 3.0.2를 실행하는 사용자만 영향을 받습니다.

이 경우는 Ruby를 수동으로 설치했거나 asdf와 같은 도구를 통해 설치했을 가능성이 높습니다.

gitlab-development-kit (GDK) 사용자도 이 문제의 영향을 받습니다.

빌드 이미지는 이 버그를 해결하는 패치를 포함하고 있기 때문에 영향을 받지 않습니다.

메서드가 스텁된 경우 DeprecationToolkit에서는 비추천 사항이 포착되지 않습니다

우리는 Ruby 2에서 비추천되었고 Ruby 3에서 제거된 기능을 사용할 때 빠르게 실패하기 위해 deprecation_toolkit에 의존합니다.

Ruby 2에서 Ruby 3로의 전환 중에 포착된 일반적인 문제는 Ruby 3.0의 위치 인수와 키워드 인수의 분리와 관련이 있습니다.

불행히도, 만약 작성자가 테스트에서 이러한 메서드를 스텁했다면, 비추천 사항은 포착되지 않습니다.

우리는 deprecation_toolkit를 통해 이 경고에 대한 자동 감지를 실행하지만, 이는 Kernel#warn이 경고를 발생시킨다는 사실에 의존합니다.

따라서 이 호출을 스텁하면 경고 호출이 실질적으로 제거되므로 deprecation_toolkit은 비추천 경고를 보지 못하게 됩니다.

구현을 스텁하면 그 경고가 제거되며, 우리는 그것을 포착하지 못하므로 빌드는 정상적으로 진행됩니다.

더 많은 맥락은 문제 364099를 참조하세요.

irbrails console에서의 테스트

또 다른 함정은 irb/rails c에서 테스트할 경우 비활성화된 경고가 있다는 것입니다.

Ruby 2.7.x의 irb에는 버그가 있어 비활성화된 경고가 표시되지 않습니다.

코드를 작성하고 코드 리뷰를 수행할 때 f({k: v}) 형태의 메서드 호출에 특히 주의하세요.

이는 Ruby 2에서는 fHash 또는 키워드 인수를 받을 때 유효하지만, Ruby 3에서는 f가 오직 Hash를 받을 때만 유효하다고 간주합니다.

Ruby 3의 호환성을 위해, 키워드 인수를 f가 받는 경우 다음 호출 중 하나로 변경해야 합니다:

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

RSpec with 인수 매처가 숏핸드 Hash 구문에서 실패합니다

키워드 인수(“kwargs”)는 Ruby 3에서 일급 개념이기 때문에, 키워드 인수가 더 이상 내부 Hash 인스턴스로 변환되지 않습니다.

이로 인해 수신자가 kwargs 대신 위치 인수 해시를 받을 때 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의 공식 문제 보고서를 참조하세요.