InfoGrab DocsInfoGrab Docs

Ruby 3 주의사항

요약

이 섹션에서는 Ruby 3 지원 작업 중 발견된 여러 문제를 설명합니다. Ruby 3 언어 및 표준 라이브러리의 전체 변경 사항 목록은 Ruby Changes를 참조하세요. Ruby 2.7에서 이 프로그램의 출력은 해시 항목을 람다에 yield하는 동작이 필수 인수의 수에 따라 다르게 동작함을 보여줍니다:

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

Ruby 3 언어 및 표준 라이브러리의 전체 변경 사항 목록은 Ruby Changes를 참조하세요.

Hash#each가 람다에 2-요소 배열을 일관되게 yield함#

다음 코드 스니펫을 살펴보세요:

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에서 이 프로그램의 출력은 해시 항목을 람다에 yield하는 동작이 필수 인수의 수에 따라 다르게 동작함을 보여줍니다:

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

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

# 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 이슈 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 # This usually happens via a conversion through `&: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 객체의 arity나 파라미터 목록을 테스트하는 코드가 이제 실패할 수 있으며 업데이트해야 합니다.

자세한 내용은 Ruby 이슈 16260을 참조하세요.

OpenStruct가 필드를 지연 평가하지 않음#

OpenStruct 구현은 Ruby 3에서 부분적으로 재작성되어 동작 변경이 발생했습니다. Ruby 2.7에서 OpenStruct는 메서드가 처음 접근될 때 메서드를 지연(lazy)으로 정의합니다. Ruby 3.0에서는 초기화 메서드에서 즉시(eager) 이 메서드들을 정의하므로, OpenStruct를 상속하고 이 메서드들을 오버라이드하는 클래스가 깨질 수 있습니다.

이러한 이유로 OpenStruct를 상속하지 마세요. 이상적으로는 아예 사용하지 마세요. OpenStruct문제가 있는 것으로 간주됩니다. 새 코드를 작성할 때는 구현이 더 단순하지만 유연성이 낮은 Struct를 사용하세요.

RegexpRange 인스턴스가 동결됨#

Ruby 3은 Regexp 또는 Range 인스턴스를 생성 시 자동으로 동결(freeze)하므로 더 이상 명시적으로 동결할 필요가 없습니다.

이는 미묘한 부작용을 야기합니다: 이러한 타입의 메서드 호출을 스텁하는 테스트가 RSpec이 동결된 객체를 스텁할 수 없어 오류와 함께 실패합니다:

# Ruby 2.7: works
# Ruby 3.0: error: "can't modify frozen object"
allow(subject.function_returning_range).to receive(:max).and_return(42)

동결된 객체에서 메서드 호출을 스텁하지 않도록 영향받는 테스트를 재작성하세요. 위 예시는 다음과 같이 재작성할 수 있습니다:

# Works with any Ruby version
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를 실행하는 사용자에게만 영향을 미칩니다. 이는 asdf와 같은 도구를 통해 Ruby를 수동으로 설치한 경우에 해당합니다. gitlab-development-kit (GDK) 사용자도 이 문제의 영향을 받습니다.

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

irb 및 rails console에서 테스트하기#

또 다른 주의사항은 irb/rails c에서 테스트할 때 지원 중단 경고가 표시되지 않는다는 것입니다. Ruby 2.7.x의 irb에는 지원 중단 경고가 표시되지 않도록 하는 버그가 있기 때문입니다.

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

  • f(**{k: v})

  • f(k: v)

RSpec에서 인수 매처가 단축 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를 전달하는 것입니다:

# Note the braces around the key-value pair.
expect(subject).to receive(:m).with({ a: 42 })

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

Ruby 3 주의사항

GitLab v19.1
원문 보기
요약

이 섹션에서는 Ruby 3 지원 작업 중 발견된 여러 문제를 설명합니다. Ruby 3 언어 및 표준 라이브러리의 전체 변경 사항 목록은 Ruby Changes를 참조하세요. Ruby 2.7에서 이 프로그램의 출력은 해시 항목을 람다에 yield하는 동작이 필수 인수의 수에 따라 다르게 동작함을 보여줍니다:

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

Ruby 3 언어 및 표준 라이브러리의 전체 변경 사항 목록은 Ruby Changes를 참조하세요.

Hash#each가 람다에 2-요소 배열을 일관되게 yield함#

다음 코드 스니펫을 살펴보세요:

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에서 이 프로그램의 출력은 해시 항목을 람다에 yield하는 동작이 필수 인수의 수에 따라 다르게 동작함을 보여줍니다:

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

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

# 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 이슈 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 # This usually happens via a conversion through `&: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 객체의 arity나 파라미터 목록을 테스트하는 코드가 이제 실패할 수 있으며 업데이트해야 합니다.

자세한 내용은 Ruby 이슈 16260을 참조하세요.

OpenStruct가 필드를 지연 평가하지 않음#

OpenStruct 구현은 Ruby 3에서 부분적으로 재작성되어 동작 변경이 발생했습니다. Ruby 2.7에서 OpenStruct는 메서드가 처음 접근될 때 메서드를 지연(lazy)으로 정의합니다. Ruby 3.0에서는 초기화 메서드에서 즉시(eager) 이 메서드들을 정의하므로, OpenStruct를 상속하고 이 메서드들을 오버라이드하는 클래스가 깨질 수 있습니다.

이러한 이유로 OpenStruct를 상속하지 마세요. 이상적으로는 아예 사용하지 마세요. OpenStruct문제가 있는 것으로 간주됩니다. 새 코드를 작성할 때는 구현이 더 단순하지만 유연성이 낮은 Struct를 사용하세요.

RegexpRange 인스턴스가 동결됨#

Ruby 3은 Regexp 또는 Range 인스턴스를 생성 시 자동으로 동결(freeze)하므로 더 이상 명시적으로 동결할 필요가 없습니다.

이는 미묘한 부작용을 야기합니다: 이러한 타입의 메서드 호출을 스텁하는 테스트가 RSpec이 동결된 객체를 스텁할 수 없어 오류와 함께 실패합니다:

# Ruby 2.7: works
# Ruby 3.0: error: "can't modify frozen object"
allow(subject.function_returning_range).to receive(:max).and_return(42)

동결된 객체에서 메서드 호출을 스텁하지 않도록 영향받는 테스트를 재작성하세요. 위 예시는 다음과 같이 재작성할 수 있습니다:

# Works with any Ruby version
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를 실행하는 사용자에게만 영향을 미칩니다. 이는 asdf와 같은 도구를 통해 Ruby를 수동으로 설치한 경우에 해당합니다. gitlab-development-kit (GDK) 사용자도 이 문제의 영향을 받습니다.

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

irb 및 rails console에서 테스트하기#

또 다른 주의사항은 irb/rails c에서 테스트할 때 지원 중단 경고가 표시되지 않는다는 것입니다. Ruby 2.7.x의 irb에는 지원 중단 경고가 표시되지 않도록 하는 버그가 있기 때문입니다.

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

  • f(**{k: v})

  • f(k: v)

RSpec에서 인수 매처가 단축 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를 전달하는 것입니다:

# Note the braces around the key-value pair.
expect(subject).to receive(:m).with({ a: 42 })

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