Ruby의 리파인먼트(Refinement)를 사용해 본 적이 없다면, 처음 접했을 때 꽤 놀랄 수 있습니다. 리파인먼트는 원래 몽키 패칭(monkey patching)을 대체하기 위해 도입된 기능입니다. 그래서 ActiveSupport의 hours 메서드처럼 기존 클래스에 메서드를 얹는 코드를 아래와 같이 작성할 수 있으리라고 자연스럽게 기대하게 됩니다.
module TimeExtension
refine Fixnum do
def hours
self * 60
end
end
end
class MyFramework
using TimeExtension
end
class MyApp < MyFramework
def index
1.hours
end
end
MyApp.new.index # undefined method `hours' for 1:Fixnum (NoMethodError)
그런데 위 코드를 실제로 실행해 보면 의도대로 동작하지 않습니다. 대체 무슨 일이 일어난 걸까요?
훌륭한 아이디어 대부분이 그렇듯이, 리파인먼트의 원래 개념 역시 냉철한 현실과 맞닿으며 여러 차례 조정을 거쳐야 했습니다. 앞선 코드에서 나타나는 예상 밖의 동작은 바로 그 조정 중 하나, 즉 "리파인먼트는 렉시컬 스코프(lexical scope)를 따른다"는 규칙 때문에 발생합니다.
렉시컬 스코프란 무엇인가?
어떤 것이 '렉시컬하다'는 말은 텍스트, 즉 화면에 보이는 코드 그 자체와 관련이 있다는 뜻입니다. 그 코드가 실제로 무엇을 의미하는지와는 무관합니다.
두 줄의 코드가 같은 렉시컬 스코프에 속한다는 것은 단순히 두 코드가 동일한 코드 블록 안에 존재한다는 의미입니다. 그 블록이 평가되어 어떤 값이 되는지는 전혀 중요하지 않습니다.
말로 설명하는 것보다 예제를 보는 편이 훨씬 빠릅니다.
class B
# x와 y는 같은 렉시컬 스코프를 공유합니다
x = 1
y = 1
end
class B
# z는 같은 클래스 본문에 있지만 x, y와는 다른 렉시컬 스코프를 가집니다
z = 3
end
리파인먼트는 렉시컬 스코프를 따른다
using 키워드로 리파인먼트를 적용하면, 그 리파인먼트는 오직 해당 렉시컬 스코프 안에서만 보입니다.
다음 예제로 무슨 뜻인지 확인해 보겠습니다.
module TimeExtension
refine Fixnum do
def hours
self * 60
end
end
end
class MyApp
using TimeExtension
def index
1.hours
end
end
class MyApp
def show
2.hours
end
end
MyApp.new.show # undefined method `hours' for 1:Fixnum (NoMethodError)
index와 show 메서드가 모두 같은 클래스에 속해 있음에도, 리파인먼트에 접근할 수 있는 것은 index뿐입니다. using 구문과 같은 렉시컬 스코프를 공유하는 메서드가 index 하나뿐이기 때문입니다.
다소 이상하게 느껴질 수 있습니다. Ruby의 거의 모든 요소는 동적으로 동작하기 때문입니다. 클래스에 메서드를 추가하면 클래스 정의를 벗어난 뒤에도 그 메서드는 그대로 남아 있습니다. 하지만 리파인먼트는 다르게 동작합니다.
이 특성은 처음에는 잘 눈에 띄지 않는 여러 가지 결과를 낳습니다.
리파인먼트 메서드는 동적으로 호출할 수 없다
send 메서드는 using 구문이 있는 코드 블록과 같은 블록 안에서 정의된 것이 아니므로 리파인먼트를 볼 수 없습니다. 따라서 아래 코드는 동작하지 않습니다.
class MyApp
using TimeExtension
def index
1.send(:hours)
end
end
리파인먼트 메서드의 존재 여부도 조회할 수 없다
respond_to? 역시 같은 코드 블록 안에 있지 않기 때문에, send와 같은 이유로 동작하지 않습니다.
class MyApp
using TimeExtension
def index
1.respond_to?(:hours)
end
end
마치며
이 글이 리파인먼트를 둘러싼 혼란을 조금이나마 해소하는 데 도움이 되길 바랍니다. 리파인먼트는 분명 잠재력 있는 유용한 기능이지만, 처음 사용하면 다소 까다롭게 느껴질 수 있습니다.
참고로 예제에 등장하는 Fixnum은 Ruby 2.4부터 Integer로 통합되며 사라진 클래스입니다. 최신 Ruby 버전에서 동일한 예제를 실행하려면 refine Integer do로 바꿔 주세요.