Rails 4.2 발표에는 다가오는 Rails 5에 대한 흥미로운 소식이 담겨 있었습니다. Rails 5는 아마도 Ruby 2.2를 요구할 예정이라고 합니다. 이는 Ruby 2가 제공하는 모든 장점을 제대로 활용하는 최초의 Rails 버전이 된다는 의미입니다.
발표에서는 GC로 관리되는 심볼(garbage collected symbols)과 키워드 인자(keyword arguments)가 언급되었지만, 개인적으로 가장 주목할 만한 Ruby 2 기능은 바로 Module#prepend입니다.
alias_method_chain의 장점과 단점
처음 Rails를 배울 때 alias_method_chain은 정말 놀라운 존재였습니다. Ruby가 얼마나 유연한 언어인지를 보여주는 대표적인 예였죠.
단 한 줄의 코드만으로 메서드의 동작 방식을 완전히 바꿀 수 있었습니다. 원하는 기능을 추가하기 위해 라이브러리 코드를 직접 수정할 필요 없이, 실행 중에 동적으로 덧붙이기만 하면 되는 것입니다. alias_method_chain은 제 첫 gem 패치로 이어졌고, 그것이 첫 풀 리퀘스트로, 다시 첫 오픈소스 기여로 연결되었습니다.
하지만 몽키 패칭(monkey patching)과 마찬가지로 alias_method_chain은 남용되기 시작했고, 그 문제점들이 점차 드러났습니다.
- 생성되는 메서드 이름이 혼란스러워 에러를 찾고 디버깅하기 어렵습니다. 예를 들어 다음 코드를 보겠습니다.
class Person
def greeting
"Hello"
end
end
module GreetingWithExcitement
def self.included(base)
base.class_eval do
alias_method_chain :greeting, :excitement
end
end
def greeting_with_excitement
"#{greeting_without_excitement}!!!!!"
end
end
Person.send(:include, GreetingWithExcitement)
Person#greeting에 에러가 발생했다면, 백트레이스는 실제로는 Person#greeting_without_excitement에서 에러가 났다고 알려줄 것입니다. 그런데 그 메서드는 어디에도 정의되어 있지 않습니다. 도대체 어떤 greeting 메서드에 버그가 있는지 어떻게 알 수 있을까요? 게다가 체인이 길어질수록 메서드 이름은 더욱 혼란스러워집니다.
같은 클래스에 같은 인자로
alias_method_chain을 두 번 호출하면 스택 오버플로우(stack overflow)가 발생할 수 있습니다. (그 이유를 짐작할 수 있나요?)require경로를 일관되게 사용한다면 보통은 일어나지 않지만, Rails 콘솔에 코드를 자주 붙여넣으며 작업한다면 상당히 짜증 나는 문제입니다.Yehuda Katz의 블로그 포스트에서 지적한 나머지 문제들도 있습니다. 이 글은 많은 Rails 개발자들이
alias_method_chain을 버리고 모듈 상속을 사용하기 시작하는 계기가 되었습니다.
그렇다면 왜 아직도 사용되고 있을까요?
대부분의 alias_method_chain은 모듈 안에서 해당 메서드를 오버라이드하고, 그 모듈을 자식 클래스에 include하는 방식으로 대체할 수 있습니다. 하지만 이 방법은 슈퍼클래스를 오버라이드할 때만 동작하고, 클래스 자신을 오버라이드할 때는 그렇지 않습니다. 즉, 다음과 같은 경우입니다.
class ParentClass
def log
puts "In parent"
end
end
class ChildClass < ParentClass
def log
puts "In child"
super
end
def log_with_extra_message
puts "In child, with extra message"
log_without_extra_message
end
alias_method_chain :log, :extra_message
end
ChildClass.new.log를 실행하면 다음과 같은 결과가 출력됩니다.
In child, with extra message
In child
In parent
반면 alias_method_chain 대신 모듈을 사용하면 출력 순서를 이렇게 만들 수 있습니다.
In child
In child, with extra message
In parent
하지만 ChildClass의 log 메서드 자체를 수정하지 않고는 원래의 출력 순서를 재현할 수 없습니다. Ruby의 상속 구조는 그런 식으로 동작하지 않기 때문입니다. 아니, 정확히 말하면 '동작하지 않았다'고 해야겠네요.
Ruby 2.0에서 무엇이 달라졌나?
Ruby 2.0까지는 클래스 '아래'에 코드를 추가하는 방법이 없었고, 위에만 추가할 수 있었습니다. 하지만 prepend를 사용하면 모듈의 메서드로 클래스의 메서드를 오버라이드하면서도, super를 통해 클래스의 원래 구현에 여전히 접근할 수 있습니다. 따라서 앞선 예제를 다음과 같이 작성하면 원래의 출력 순서를 그대로 얻을 수 있습니다.
class ParentClass
def log
puts "In parent"
end
end
module ExtraMessageLogging
def log
puts "In child, with extra message"
super
end
end
class ChildClass < ParentClass
prepend ExtraMessageLogging
def log
puts "In child"
super
end
end
In child, with extra message
In child
In parent
완벽합니다.
prepend가 아직 어렵게 느껴진다면, 다음과 같은 동작을 한다고 생각해 보세요.
class NewChildClass < ChildClass
include ExtraMessageLogging
end
ChildClass = NewChildClass
단, 실제 prepend는 클래스 이름을 건드리지 않으며, 이미 생성된 객체들에도 영향을 줍니다.
(그렇습니다, Ruby에서는 클래스 이름을 재할당할 수 있습니다. 하지만 좋은 아이디어는 아닙니다.)
Rails에는 어떤 의미일까?
결국 alias_method_chain을 사용할 마지막 명분이 Ruby 2.0에서 사라진 것입니다. Rails에 남아 있던 몇 안 되는 alias_method_chain 사용 예 중 하나를 살펴보겠습니다.
require 'active_support/core_ext/module/aliasing'
class Range #:nodoc:
def each_with_time_with_zone(&block)
ensure_iteration_allowed
each_without_time_with_zone(&block)
end
alias_method_chain :each, :time_with_zone
def step_with_time_with_zone(n = 1, &block)
ensure_iteration_allowed
step_without_time_with_zone(n, &block)
end
alias_method_chain :step, :time_with_zone
private
def ensure_iteration_allowed
if first.is_a?(Time)
raise TypeError, "can't iterate from #{first.class}"
end
end
end
이 코드를 모듈을 사용하는 방식으로 바꾸면 다음과 같습니다.
require 'active_support/core_ext/module/aliasing'
module RangeWithTimeWithZoneSupport #:nodoc:
def each(&block)
ensure_iteration_allowed
super(&block)
end
def step(n = 1, &block)
ensure_iteration_allowed
super(n, &block)
end
private
def ensure_iteration_allowed
if first.is_a?(Time)
raise TypeError, "can't iterate from #{first.class}"
end
end
end
Range.send(:prepend, RangeSupportingTimeWithZone)
훨씬 깔끔해졌고, Range#each의 이름도 변경되지 않으며, ensure_iteration_allowed도 몽키 패칭 없이 자연스럽게 통합됩니다.
패치 대신 상속을 활용하세요
Ruby는 엄청난 유연성을 제공하며, 그것이 제가 이 언어를 사랑하는 이유 중 하나입니다. 하지만 Ruby는 강력한 객체 모델도 갖추고 있습니다. 따라서 자신만의 코드를 삽입하고 싶다면, 무작정 해킹하기보다 모듈과 상속을 먼저 활용해 보세요. 코드를 이해하고 디버깅하기가 훨씬 쉬워지고, alias_method_chain 같은 도구가 만들어내는 감지하기 어려운 부작용도 피할 수 있습니다.
alias_method_chain은 Rails에서 접한 가장 멋진 메서드 중 하나였습니다. 하지만 그 남은 날들은 많지 않습니다. 우리는 이미 그것을 넘어섰고, 사라진다 해도 아쉽지 않을 것입니다.