리파인먼트
리파인먼트 (Refinements)
Ruby의 열린 클래스(open class) 덕분에 기존 클래스의 기능을 재정의하거나 추가할 수 있어요. 이걸 '몽키 패치(monkey patch)'라고 부르죠. 문제는 이런 변경의 범위가 전역적이라는 거예요. 몽키 패치된 클래스를 쓰는 모든 사용자가 똑같은 변경을 보게 되니까, 의도하지 않은 부작용을 일으키거나 프로그램을 망가뜨릴 수 있어요.
리파인먼트는 몽키 패칭이 다른 사용자들에게 미치는 영향을 줄이기 위해 설계됐어요. 리파인먼트는 클래스를 지역적으로 확장하는 방법을 제공해요. 클래스와 모듈 모두 수정할 수 있죠.
기본적인 리파인먼트를 하나 볼게요.
class C
def foo
puts "C#foo"
end
end
module M
refine C do
def foo
puts "C#foo in M"
end
end
end
먼저 클래스 C가 정의됐어요. 다음으로 Module#refine을 사용해 C에 대한 리파인먼트를 만들었죠.
Module#refine은 클래스(예제에서는 C)에 대한 변경이나 리파인먼트를 담는 익명 모듈을 만들어요. refine 블록 안의 self는 이 익명 모듈이에요. Module#module_eval과 비슷하죠.
using으로 리파인먼트를 활성화해요.
using M
c = C.new
c.foo # prints "C#foo in M"
출처: Ruby 공식 문서
범위 (Scope)
리파인먼트는 최상위(top-level)와 클래스·모듈 안에서 활성화할 수 있어요. 메서드 스코프에서는 활성화할 수 없어요. 리파인먼트는 현재 클래스나 모듈 정의가 끝날 때까지, 최상위에서 썼다면 현재 파일이 끝날 때까지 활성화돼요.
Kernel#eval에 전달되는 문자열 안에서도 리파인먼트를 활성화할 수 있어요. 이 경우 eval 문자열이 끝날 때까지 활성화되죠.
리파인먼트는 어휘적(lexical) 스코프를 가져요. 리파인먼트는 using 호출 이후의 스코프에서만 활성화돼요. using 문장 이전의 코드에는 리파인먼트가 적용되지 않아요.
제어 흐름이 그 스코프 밖으로 옮겨지면 리파인먼트는 비활성화돼요. 즉, 현재 스코프 밖에서 정의된 파일을 require/load하거나 메서드를 호출하면 리파인먼트가 비활성화된다는 뜻이에요.
class C
end
module M
refine C do
def foo
puts "C#foo in M"
end
end
end
def call_foo(x)
x.foo
end
using M
x = C.new
x.foo # prints "C#foo in M"
call_foo(x) #=> raises NoMethodError
x.foo는 using M 이후라 리파인먼트가 적용돼 출력되지만, call_foo 메서드는 using 이전에 정의된 스코프에서 실행되므로 리파인먼트 없이 x.foo를 호출하다 NoMethodError가 나는 거예요.
메서드가 리파인먼트가 활성화된 스코프에서 정의됐다면, 그 메서드를 호출할 때도 리파인먼트가 활성화돼요. 여러 파일에 걸친 예시를 볼게요.
c.rb:
class C
end
m.rb:
require "c"
module M
refine C do
def foo
puts "C#foo in M"
end
end
end
m_user.rb:
require "m"
using M
class MUser
def call_foo(x)
x.foo
end
end
main.rb:
require "m_user"
x = C.new
m_user = MUser.new
m_user.call_foo(x) # prints "C#foo in M"
x.foo #=> raises NoMethodError
MUser#call_foo가 정의된 m_user.rb에서 리파인먼트 M이 활성화돼 있으므로, main.rb에서 call_foo를 호출할 때도 리파인먼트가 활성화되는 거예요.
using은 메서드이기 때문에, 리파인먼트는 호출됐을 때만 활성화돼요. 리파인먼트 M이 활성화되는 곳과 아닌 곳의 예시를 볼게요.
파일 안에서:
# not activated here
using M
# activated here
class Foo
# activated here
def foo
# activated here
end
# activated here
end
# activated here
클래스 안에서:
# not activated here
class Foo
# not activated here
def foo
# not activated here
end
using M
# activated here
def bar
# activated here
end
# activated here
end
# not activated here
클래스 Foo가 나중에 다시 열리면(재정의되면) M의 리파인먼트가 자동으로 활성화되지 않는다는 점에 주의하세요.
eval 안에서:
# not activated here
eval <<EOF
# not activated here
using M
# activated here
EOF
# not activated here
평가되지 않을 때:
# not activated here
if false
using M
end
# not activated here
같은 모듈 안의 여러 refine 블록에서 리파인먼트 여러 개를 정의하면, 리파인먼트된 메서드(아래 예시의 to_json 메서드 중 하나)가 호출될 때 같은 모듈의 모든 리파인먼트가 활성화돼요.
module ToJSON
refine Integer do
def to_json
to_s
end
end
refine Array do
def to_json
"[" + map { |i| i.to_json }.join(",") + "]"
end
end
refine Hash do
def to_json
"{" + map { |k, v| k.to_s.dump + ":" + v.to_json }.join(",") + "}"
end
end
end
using ToJSON
p [{1=>2}, {3=>4}].to_json # prints "[{\"1\":2},{\"3\":4}]"
메서드 탐색 (Method Lookup)
클래스 C의 인스턴스에 대한 메서드를 찾을 때 Ruby는 다음 순서로 확인해요.
C에 대해 리파인먼트가 활성화돼 있다면, 활성화된 역순으로:
C에 대한 리파인먼트에서 prepend된 모듈C에 대한 리파인먼트C에 대한 리파인먼트에서 include된 모듈C의 prepend된 모듈CC의 include된 모듈
어느 지점에서도 메서드를 찾지 못했다면, 이 과정을 C의 슈퍼클래스로 반복해요.
하위 클래스의 메서드가 슈퍼클래스의 리파인먼트보다 우선한다는 점에 주의하세요. 예를 들어 / 메서드가 Numeric의 리파인먼트에 정의돼 있다면, 1 / 2는 원래 Integer#/를 호출해요. Integer는 Numeric의 하위 클래스라서 슈퍼클래스 Numeric의 리파인먼트보다 먼저 탐색되거든요. 메서드 /가 자식인 Integer에도 있으므로, 메서드 탐색이 슈퍼클래스까지 올라가지 않는 거예요.
하지만 foo라는 메서드가 리파인먼트로 Numeric에 정의돼 있다면, 1.foo는 그 메서드를 호출해요. foo는 Integer에 없으니까요.
super
super가 호출되면 메서드 탐색은 다음을 확인해요.
- 현재 클래스의 include된 모듈. (현재 클래스가 리파인먼트일 수도 있다는 점에 주의하세요.)
- 현재 클래스가 리파인먼트라면, 위 '메서드 탐색' 섹션에 나온 대로 진행돼요.
- 현재 클래스에 직접 슈퍼클래스가 있다면, 그 슈퍼클래스를 사용해 위 '메서드 탐색' 섹션대로 진행돼요.
참고로 리파인먼트의 메서드 안에서의 super는, 같은 컨텍스트에서 다른 리파인먼트가 활성화돼 있어도 리파인먼트된 클래스의 메서드를 호출해요. 이 규칙은 리파인먼트의 메서드 안에서의 super에만 해당하고, 리파인먼트에 include된 모듈의 메서드 안에서의 super에는 적용되지 않아요.
메서드 인트로스펙션 (Methods Introspection)
Kernel#method나 Kernel#methods 같은 인트로스펙션 메서드를 사용할 때는 리파인먼트가 적용되지 않아요.
이 동작은 미래에 바뀔 수 있어요.
Module#include에 의한 리파인먼트 상속
모듈 X가 모듈 Y에 include되면, Y는 X로부터 리파인먼트를 상속받아요.
예를 들어 다음 코드에서 C는 A와 B로부터 리파인먼트를 상속받아요.
module A
refine X do ... end
refine Y do ... end
end
module B
refine Z do ... end
end
module C
include A
include B
end
using C
# Refinements in A and B are activated here.
하위(descendant)에 있는 리파인먼트가 조상(ancestor)의 리파인먼트보다 우선순위가 높아요.
더 알아보기 (Learn more)
- 리파인먼트 구현의 현재 스펙은 github.com/ruby/ruby/wiki/Refinements-Spec에서 볼 수 있어요. 스펙에 더 자세한 내용도 들어 있어요.
Module#refine,Kernel#using의 구체적인 사용법은 각 메서드 문서를 참고하세요.