Hash#each
는 람다에 2요소 배열을 일관되게 생성함Symbol#to_proc
는 람다와 일관된 서명 메타데이터를 반환함OpenStruct
는 필드를 느리게 평가하지 않음Regexp
및Range
인스턴스는 불변- 테이블 테스트가 Ruby 3.0.2에서 실패함
- 메서드가 스텁된 경우 DeprecationToolkit에서 사용되지 않는 메서드를 잡지 못합니다.
irb
및rails console
에서의 테스트- RSpec
with
인자 매처는 간소화된 Hash 구문에 실패합니다
Ruby 3 주의사항
이 섹션은 Ruby 3 지원 작업 중 발견된 여러 문제를 문서화한 것입니다. 이러한 문제로는 알아내기 어려운 미묘한 버그나 테스트 실패로 이어질 수 있었습니다. Ruby 코드를 정기적으로 작성하는 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에서 이 프로그램의 출력은 요구되는 인수가 몇 개인지에 따라 해시 엔트리를 람다에 생성하는 방법이 다르다는 것을 시사합니다.
# 루비 2.7
{ a: 1 }.each(&foo_lambda) # [:a, 1] 출력
{ a: 1 }.each(&bar_lambda) # [[:a, 1], 2] 출력
루비 3은 이 동작을 일관성 있게 만들고 항상 해시 엔트리를 단일 [key, value]
배열로 생성을 시도합니다.
# 루비 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`를 통해 이루어집니다
# 루비 2.7: [[:rest]] (-1) 출력
# 루비 3.0: [[:req], [:rest]] (-2) 출력
puts "#{p.parameters} (#{p.arity})"
루비 2.7은 이 Proc
개체에 대해 필요한 인수가 없고 선택적인 하나의 매개변수를 보고하는 반면, 루비 3은 하나의 필수 매개변수와 선택적 하나의 매개변수를 보고합니다. 루비 2.7은 올바르지 않습니다. 첫 번째 인수는 항상 전달되어야 하며, 이는 Proc
개체가 나타내는 메서드의 수신자이며, 메서드는 수신자 없이 호출할 수 없습니다.
루비 3은 이를 수정하였습니다. Proc
개체의 인수 또는 매개변수 디렉터리을 테스트하는 코드는 이제 망가질 수 있으며 업데이트해야 합니다.
자세한 내용은 루비 이슈 16260을 참조하세요.
OpenStruct
는 필드를 느리게 평가하지 않음
루비 3에서 OpenStruct
구현이 일부 재작성되어 동작이 변경되었습니다. 루비 2.7에서 OpenStruct
은 메소드가 처음 액세스될 때 메소드를 지연하여 정의했지만, 루비 3.0에서는 초기화기에서 이러한 메소드를 즉시 정의합니다. 이는 OpenStruct
을 상속하고 이러한 메소드를 덮어쓰는 클래스를 방해할 수 있습니다.
이러한 이유로 OpenStruct
에서 상속하지 않는 것이 좋습니다. 이상적으로는 전혀 사용하지 않는 것이 좋습니다. OpenStruct
은 문제가 많다고 여겨집니다. 새로운 코드를 작성할 때는 더 단순한 Struct
를 사용하는 것이 좋습니다. 이는 구현이 더 간단하지만 덜 유연합니다.
Regexp
및 Range
인스턴스는 불변
이제 Regexp
또는 Range
인스턴스를 명시적으로 동결할 필요가 없습니다. 왜냐하면 루비 3가 생성 시 자동으로 이들을 동결하기 때문입니다.
이것은 미묘한 부작용을 가지고 있습니다. 이들 유형의 메소드 호출을 대역잡는 테스트는 더 이상 실패하지 않습니다. 왜냐하면 RSpec이 동결된 객체를 대역잡을 수 없기 때문입니다.
# 루비 2.7: 동작
# 루비 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에 문서화되어 있습니다. 이 문제는 Ruby에서 수정되었으며 해당 수정이 Ruby 3.0.3에 포함될 것으로 예상됩니다.
이 문제는 패치되지 않은 Ruby 3.0.2를 사용하는 사용자에게만 영향을 미칩니다. 이는 매뉴얼으로 또는 asdf
와 같은 도구를 통해 Ruby를 설치한 사용자에게도 해당됩니다. gitlab-development-kit (GDK)
사용자도 이 문제에 영향을 받습니다.
빌드 이미지에는이 버그를 해결하는 패치 세트가 포함되어 있으므로 영향을받지 않습니다.
메서드가 스텁된 경우 DeprecationToolkit에서 사용되지 않는 메서드를 잡지 못합니다.
우리는 deprecation_toolkit
을 의존하고 있어요. Ruby 2에서는 사용이 중지되었고 Ruby 3에서는 제거된 기능을 사용할 때 빠르게 실패하도록 하기 위해서 말이에요.
Ruby 2에서 Ruby 3으로의 전환 중 발견된 일반적인 문제 중 하나는 Ruby 3.0에서의 위치 및 키워드 인수 구분와 관련이 있어요.
안타깝게도 만약 저자가 테스트에서 이러한 메서드를 스텁했다면, 사용되지 않는 메서드는 잡히지 않을 거에요.
우리는 deprecation_toolkit
을 통해 테스트에서 이 경고를 자동으로 감지하지만, 이는 Kernel#warn
이 경고를 내보내기 때문에 의존하고 있죠. 따라서 이 호출을 스텁하면 경고가 제거되어 deprecation_toolkit
이 사용되지 않기 때문에 사용되지 않게 돼요.
이 구현을 스텁하면 그 경고가 제거되어 결국 우리는 제거하지 못할 거에요. 그래서 빌드가 성공하게 됩니다.
더 많은 맥락은 이슈 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
가 키워드 인수를 취할 때 다음 중 하나의 호출로 변경되어야 합니다:
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의 공식 이슈 보고서를 참조하세요.