Hash#each
는 항상 2개 요소 배열을 lambda에 노출합니다Symbol#to_proc
는 람다와 일치하는 시그니처 메타데이터를 반환합니다OpenStruct
는 필드를 게으르게 계산하지 않습니다Regexp
및Range
인스턴스가 동결됨- Ruby 3.0.2에서 테이블 테스트가 실패함
- DeprecationToolkit에서 메서드를 스텁할 경우 Deprecation이 감지되지 않음
irb
및rails console
에서의 테스트- RSpec
with
인자 매처는 간략한 Hash 구문에서 실패함
Ruby 3의 함정들
이 섹션은 Ruby 3 support 작업 중 발견한 여러 문제와, 숨겨진 버그나 이해하기 어려운 테스트 실패로 이어지는 문제들을 문서화합니다. Ruby 코드를 정기적으로 작성하는 GitLab 기여자들은 이러한 문제들을 숙지하는 것을 권장합니다.
Ruby 3 언어와 표준 라이브러리의 변경 사항에 대한 전체 목록을 보려면 Ruby 변경 사항을 참조하세요.
Hash#each
는 항상 2개 요소 배열을 lambda에 노출합니다
다음 코드 조각을 고려해보세요.
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) # 출력: [:a, 1]
{ a: 1 }.each(&bar_lambda) # 출력: [[: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) # 출력: [[: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
객체에 대해 필수 인자가 0개이고 선택적 인자가 1개라고 보고하며, Ruby 3는 이 Proc
객체에 대해 필수 인자 1개와 선택적 인자 1개를 보고합니다. Ruby 2.7는 올바르지 않습니다: 첫 번째 인자는 항상 전달되어야 하며, 이것은 Proc
객체가 표현하는 메소드의 수신자이며, 메소드는 수신자 없이 호출될 수 없습니다.
Ruby 3은 이를 수정했습니다: Proc
객체를 검사하는 코드는 이제 깨질 수 있으며 업데이트해야 할 수도 있습니다.
더 많은 정보는 Ruby issue 16260을 참조하세요.
OpenStruct
는 필드를 게으르게 계산하지 않습니다
Ruby 3에서 OpenStruct
구현이 일부 변경되어 동작이 변했습니다. Ruby 2.7에서는 OpenStruct
가 처음 액세스될 때 메서드를 게으르게 정의했습니다. Ruby 3.0에서는 초기화될 때 이러한 메서드를 즉시 정의하므로, OpenStruct
를 상속하고 이러한 메서드를 재정의하는 클래스가 깨질 수 있습니다.
이러한 이유로 OpenStruct
를 상속하지 않습니다. 이상적으로는 아예 사용하지 않는 것이 좋습니다. OpenStruct
는 문제가 있다고 여겨집니다. 새 코드를 작성할 때는 보다 단순하지만 덜 유연한 Struct
를 대신 사용하는 것이 좋습니다.
Regexp
및 Range
인스턴스가 동결됨
이제는 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에서 메서드를 스텁할 경우 Deprecation이 감지되지 않음
우리는 Ruby 2에서는 사용이 중단되고 Ruby 3에서는 제거된 기능을 사용할 때 빠르게 실패하도록 deprecation_toolkit
에 의존하고 있습니다. Ruby 2에서 Ruby 3으로의 전환 중에 발견된 흔한 문제 중 하나는 Ruby 3.0에서 위치 및 키워드 인자의 분리와 관련이 있습니다.
안타깝게도, 작성자가 이러한 메서드를 테스트에서 스텁했다면 Deprecation은 감지되지 않습니다. 우리는 deprecation_toolkit
을 통해 이 경고에 대한 자동 감지를 실행하지만, 이는 Kernel#warn
이 경고를 방출하는 사실에 의존하고 있으므로 이 호출을 스텁 처리하면 사실상 경고가 제거되어 deprecation_toolkit
이 더 이상 deprecation 경고를 보지 못할 것입니다. 구현을 스텁 처리하면 해당 경고가 제거되고 결국 우리는 이를 인식하지 못하기 때문에 빌드는 성공적으로 실행됩니다.
더 많은 정보는 이슈 364099를 참조하세요.
irb
및 rails console
에서의 테스트
또 다른 함정은 irb
/rails c
에서의 테스트입니다. Ruby 2.7.x에서 irb
는 버그로 인해 중단 경고를 표시하지 않습니다.
코드 작성 및 코드 리뷰 시에 f({k: v})
형식의 메서드 호출에 특별한 주의를 기울여야 합니다. 이는 Ruby 2에서 f
가 Hash
또는 키워드 인자를 사용하는 경우 유효하지만, Ruby 3에서는 f
가 Hash
를 사용하는 경우에만 유효합니다. Ruby 3에서는 만일 f
가 키워드 인자를 받는 경우에만 유효합니다.
Ruby 3 호환성을 위해 이는 다음 호출 중 하나로 변경되어야 합니다:
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에서 이 기대는 다음과 같은 오류로 실패합니다:
실패/Error:
#<subject>은(는) 예상치 않은 인자와 함께 :m을(를) 수신받았습니다
예상: ({:a=>42})
받은: ({:a=>42})
이는 RSpec이 여기서 kwargs 인자 매처를 사용하기 때문에 발생합니다. 그러나 메서드가 해시를 가져야 하는데 해시가 아닌 경우입니다. 이는 a: 42
가 먼저 해시로 변환되어 사용되고 RSpec은 해시 인자 매처를 사용하기 때문에 Ruby 2에서 작동합니다.
해결책은 간략한 구문을 사용하지 않고 실제 Hash
를 전달하는 것입니다. 이렇게 알고 있는 메서드가 옵션 해시를 받는다면 실제 Hash
를 전달해야 합니다.
# key-value 쌍 주위의 중괄호를 주의하세요.
expect(subject).to receive(:m).with({ a: 42 })
더 자세한 정보는 RSpec의 공식 이슈 보고서를 참조하세요.