Ruby 3 주의사항들

이 섹션에서는 Ruby 3 지원 작업 중 발견된 여러 문제들을 문서화하였으며, 이러한 문제들은 미묘한 버그나 이해하기 어려운 테스트 실패를 유발했습니다. 정기적으로 루비 코드를 작성하는 모든 GitLab 기여자들은 이러한 문제에 친숙해지는 것을 권장합니다.

루비 3 언어와 표준 라이브러리의 전체 변경 사항 목록을 보려면 루비 변경 사항을 참조하세요.

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)

루비 2.7에서 이 프로그램의 출력은 필요한 인수의 수에 따라 해시 항목을 람다에 제공하는 방식이 다르다는 것을 시사합니다.

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

루비 3은 이 동작을 일관되게 만들고 항상 해시 항목을 단일 [키, 값] 배열로 제공하려고 시도합니다.

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

2.7와 3.0 양쪽에서 작동하는 코드를 작성하려면 다음 옵션을 고려하세요:

  • 항상 람다 본문을 블록으로 전달: { a: 1 }.each { |a, b| p [a, b] }.
  • 람다 인수를 해체: { a: 1 }.each(&->((a, b)) { p [a, b] }).

우리는 항상 블록을 명시적으로 전달하고 블록 매개변수로 두 개의 필수 인수를 선호합니다.

더 자세한 정보는 루비 이슈 12706를 참조하세요.

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

루비에서 일반적으로 사용되는 관용구 중 하나는 &:<symbol>을 사용하여 Proc 객체를 얻고 이를 고차 함수에 전달하는 것입니다.

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

루비는 &:<symbol>Symbol#to_proc로 변환합니다. 여기에서는 메서드 _수신자_를 첫 번째 인수(여기서는 Integer)로 호출하고 모든 메서드 _인수_를 나머지 인수로 호출합니다.

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

p = :foo.to_proc # 일반적으로 `&:foo`를 통해 이루어집니다

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

루비 2.7은 이 Proc 객체에 대해 필수 매개변수가 없고 선택적 매개변수가 하나라고 보고하는 반면, 루비 3은 이 Proc 객체에 대해 한 개의 필수 매개변수와 하나의 선택적 매개변수가 있다고 보고합니다. 루비 2.7은 잘못되었습니다: 첫 번째 인수는 항상 전달해야 하며, 이는 Proc 객체가 나타내는 메서드의 수신자이며, 메서드는 수신자 없이 호출될 수 없습니다.

루비 3은 이를 수정하였으며, Proc 객체의 인수 수나 매개변수 목록을 테스트하는 코드는 이제 실패할 수 있으며 업데이트 되어야 합니다.

더 자세한 정보는 루비 이슈 16260를 참조하세요.

OpenStruct는 필드를 게으르게 평가하지 않습니다

루비 3에서 OpenStruct 구현이 일부 재작성되어 동작이 변경되었습니다. 루비 2.7에서 OpenStruct는 메서드에 처음 액세스할 때 메서드를 게으르게 정의합니다. 그에 반해 루비 3.0에서는 초기화기에서 이러한 메서드를 즉시 정의하며, 이는 OpenStruct에서 상속하고 이러한 메서드를 재정의하는 클래스들을 망가뜨릴 수 있습니다.

이러한 이유로 OpenStruct를 상속하지 마십시오. 이상적으로는 아예 사용하지 않는 것이 좋습니다. OpenStruct문제가 있다고 여겨집니다. 새 코드를 작성할 때는 더 단순하지만 유연성은 떨어지는 Struct를 대신 사용하세요.

RegexpRange 인스턴스가 동결됨

루비 3에서는 더 이상 Regexp 또는 Range 인스턴스를 명시적으로 동결할 필요가 없습니다. 루비 3는 생성 시 자동으로 이들을 동결합니다.

이에 미묘한 부작용이 있습니다. 이러한 유형에 대한 메서드 호출을 踰 성하는 테스트는 이제 오류로 실패하게 됩니다. 왜냐하면 RSpec은 동결된 객체에 대해 메서드 호출을 스텁할 수 없기 때문입니다.

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

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

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

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

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

이 문제는 미패치된 Ruby 3.0.2를 실행하는 사용자에게만 영향을 미치며, 이는 루비를 수동으로 설치하거나 asdf와 같은 도구를 사용할 때 발생할 수 있습니다. gitlab-development-kit (GDK) 사용자들도 이 문제의 영향을 받습니다.

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

메서드가 스터빙된 경우 DeprecationToolkit에서 사용되지 않는 것으로 간주되지 않습니다

우리는 deprecation_toolkit을 Ruby 2에서 사용되지 않는 기능을 사용할 때 빠르게 실패하도록 의존하고 Ruby 3에서 제거된 것을 사용하고 있습니다. Ruby 2에서 Ruby 3으로의 전환 중에 발생하는 일반적인 문제는 Ruby 3.0에서 위치 및 키워드 인수의 분리와 관련이 있습니다.

불행하게도 테스트에서 이러한 메서드를 스터빙한 경우, 사용되지 않는 것으로 간주되지 않습니다. 우리는 deprecation_toolkit을 통해 이 경고에 대한 자동 검출을 실행하지만, 이는 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 인수 매처가 간략한 해시 구문에 실패합니다

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

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 인수 매처를 사용하고 있지만 메서드가 해시를 취하기 때문에 발생합니다. 이는 a: 42가 먼저 해시로 변환되어 Ruby 2에서 작동하며 RSpec은 해시 인수 매처를 사용하게 됩니다.

해결책은 간략한 구문을 사용하지 않고 메서드가 옵션 해시를 취할 때마다 실제 Hash를 전달하는 것입니다:

# 키-값 쌍 주위의 중괄호에 유의하십시오.
expect(subject).to receive(:m).with({ a: 42 })

자세한 정보는 RSpec의 공식 이슈 보고서를 참조하십시오.