리파인먼트

리파인먼트 (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.foousing 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된 모듈
  • C
  • C의 include된 모듈

어느 지점에서도 메서드를 찾지 못했다면, 이 과정을 C의 슈퍼클래스로 반복해요.

하위 클래스의 메서드가 슈퍼클래스의 리파인먼트보다 우선한다는 점에 주의하세요. 예를 들어 / 메서드가 Numeric의 리파인먼트에 정의돼 있다면, 1 / 2는 원래 Integer#/를 호출해요. IntegerNumeric의 하위 클래스라서 슈퍼클래스 Numeric의 리파인먼트보다 먼저 탐색되거든요. 메서드 /가 자식인 Integer에도 있으므로, 메서드 탐색이 슈퍼클래스까지 올라가지 않는 거예요.

하지만 foo라는 메서드가 리파인먼트로 Numeric에 정의돼 있다면, 1.foo는 그 메서드를 호출해요. fooInteger에 없으니까요.

super

super가 호출되면 메서드 탐색은 다음을 확인해요.

  • 현재 클래스의 include된 모듈. (현재 클래스가 리파인먼트일 수도 있다는 점에 주의하세요.)
  • 현재 클래스가 리파인먼트라면, 위 '메서드 탐색' 섹션에 나온 대로 진행돼요.
  • 현재 클래스에 직접 슈퍼클래스가 있다면, 그 슈퍼클래스를 사용해 위 '메서드 탐색' 섹션대로 진행돼요.

참고로 리파인먼트의 메서드 안에서의 super는, 같은 컨텍스트에서 다른 리파인먼트가 활성화돼 있어도 리파인먼트된 클래스의 메서드를 호출해요. 이 규칙은 리파인먼트의 메서드 안에서의 super에만 해당하고, 리파인먼트에 include된 모듈의 메서드 안에서의 super에는 적용되지 않아요.

메서드 인트로스펙션 (Methods Introspection)

Kernel#methodKernel#methods 같은 인트로스펙션 메서드를 사용할 때는 리파인먼트가 적용되지 않아요.

이 동작은 미래에 바뀔 수 있어요.

Module#include에 의한 리파인먼트 상속

모듈 X가 모듈 Y에 include되면, YX로부터 리파인먼트를 상속받아요.

예를 들어 다음 코드에서 CAB로부터 리파인먼트를 상속받아요.

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의 구체적인 사용법은 각 메서드 문서를 참고하세요.