메타프로그래밍(Metaprogramming)은 매우 멋져 들리는 단어지만, 과연 실제로 좋은 선택일까요?
물론 상황에 따라 유용할 수 있습니다. 하지만 많은 개발자들이 메타프로그래밍을 사용할 때 따르는 숨겨진 비용을 인식하지 못한 채 사용합니다.
먼저 서로 같은 이해를 바탕으로 논의하기 위해, 메타프로그래밍이 정확히 무엇인지부터 짚어보겠습니다.
메타프로그래밍이란 무엇인가?
여기서는 다음 중 하나에 해당하는 기법을 모두 메타프로그래밍으로 정의합니다.
- 코드 구조 자체를 변경하는 방법 — 예:
define_method - 문자열을 실제 루비 코드처럼 실행하는 방법 — 예:
instance_eval - 특정 이벤트에 반응해 동작하는 방법 — 예:
method_missing
그렇다면 메타프로그래밍의 비용에는 어떤 것들이 있을까요? 크게 세 가지로 분류할 수 있습니다.
- 속도(Speed)
- 가독성(Readability)
- 검색성(Searchability)
참고: 네 번째 비용으로 보안(Security)을 꼽기도 합니다.
eval계열 메서드는 전달되는 문자열에 대해 어떠한 보안 검사도 수행하지 않기 때문입니다. 검증은 전적으로 개발자 본인의 책임입니다.
이제 각각의 비용을 하나씩 자세히 살펴보겠습니다.
1. 속도
첫 번째 비용은 속도입니다. 대부분의 메타프로그래밍 메서드는 일반 메서드보다 느립니다.
다음은 이를 확인할 수 있는 벤치마크 코드입니다.
require 'benchmark/ips'
class Thing
def method_missing(name, *args)
end
def normal_method
end
define_method(:speak) {}
end
t = Thing.new
Benchmark.ips do |x|
x.report("normal method") { t.normal_method }
x.report("missing method") { t.abc }
x.report("defined method") { t.speak }
x.compare!
end
측정 결과 (Ruby 2.2.4):
normal method: 7344529.4 i/s defined method: 5766584.9 i/s - 1.34x slower missing method: 4777911.7 i/s - 1.54x slower
결과에서 알 수 있듯이, 두 메타프로그래밍 메서드(define_method, method_missing)는 일반 메서드보다 상당히 느립니다.
그런데 여기서 흥미로운 사실 하나를 발견했습니다.
위 결과는 Ruby 2.2.4에서 측정한 것이지만, 같은 벤치마크를 Ruby 2.3 또는 Ruby 2.4에서 실행하면 오히려 메타프로그래밍 메서드가 더 느려 보이는 현상이 나타납니다.
Ruby 2.4 벤치마크 결과:
normal method: 8252851.6 i/s defined method: 6153202.9 i/s - 1.39x slower missing method: 4557376.3 i/s - 1.87x slower
우연이 아님을 확인하기 위해 이 벤치마크를 여러 번 반복 실행했습니다.
하지만 초당 반복 횟수(i/s)를 잘 살펴보면, Ruby 2.3부터 일반 메서드 자체가 더 빨라졌다는 것을 알 수 있습니다. 상대적으로 method_missing이 훨씬 느려 보이는 이유가 바로 이것입니다 🙂
2. 가독성
instance_eval / class_eval 메서드를 사용하면 에러 메시지가 도움이 되지 않는 경우가 많습니다.
다음 코드를 살펴보세요.
class Thing
class_eval("def self.foo; raise 'something went wrong'; end")
end
Thing.foo
이 코드는 다음과 같은 에러를 발생시킵니다.
(eval):1:in 'foo': 'something went wrong...' (RuntimeError)
눈치챘겠지만, 파일 이름이 사라지고 eval로 표시되며 올바른 줄 번호도 출력되지 않습니다. 다행히 해결 방법이 있습니다. 이 eval 계열 메서드는 추가로 두 개의 매개변수를 받습니다.
- 파일 이름(file name)
- 줄 번호(line number)
내장 상수인 __FILE__과 __LINE__을 class_eval의 매개변수로 전달하면, 에러 메시지에 정확한 정보가 표시됩니다.
예제:
class Thing
class_eval(
"def foo; raise 'something went right'; end",
__FILE__,
__LINE__
)
end
왜 이것이 기본 동작이 아닌지는 의문입니다. 하지만 이런 메서드를 사용할 계획이라면 반드시 기억해 둬야 할 팁입니다 🙂
3. 검색성
메타프로그래밍은 코드의 검색성을 떨어뜨리고, 문서화 품질 저하로 접근성을 해치며, 디버깅을 더 어렵게 만듭니다.
특정 메서드의 정의를 찾으려고 할 때, 메타프로그래밍으로 정의된 메서드는 CTRL+F(또는 여러분이 사용하는 단축키)로 찾을 수 없습니다. 특히 메서드 이름이 런타임에 동적으로 생성되는 경우에는 더욱 그렇습니다.
다음 예제는 메타프로그래밍으로 3개의 메서드를 정의합니다.
class RubyBlog
def create_post_tags
types = ['computer_science', 'tools', 'advanced_ruby']
types.each do |type|
define_singleton_method(type + "_tag") { puts "This post is about #{type}" }
end
end
end
rb = RubyBlog.new
rb.create_post_tags
rb.computer_science_tag
Yard나 RDoc 같은 문서 생성 도구는 이러한 메서드를 찾아 목록에 표시하지 못합니다.
이 도구들은 클래스와 메서드를 찾기 위해 "정적 분석(Static Analysis)"이라는 기법을 사용하는데, 이 기법은 def 문법으로 직접 정의된 메서드만 탐지할 수 있기 때문입니다.
위 예제로 yard doc을 직접 실행해 보면, 발견되는 메서드가 create_post_tags뿐이라는 것을 확인할 수 있습니다.
실행 결과는 다음과 같습니다:

@method 태그를 사용해 yard에게 추가 메서드를 문서화하도록 지시하는 방법도 있지만, 항상 실용적이지는 않습니다.
예제:
class Thing # @method build_report define_method(:build_report) end
또한 grep, ack 또는 에디터의 검색 기능으로 메서드 정의를 찾을 때도, 메타프로그래밍으로 정의된 메서드는 일반 메서드보다 훨씬 찾기 어렵습니다.
"저는 Sidekiq에서 메타프로그래밍을 거의 사용하지 않습니다. 95%의 경우 코드를 돕기보다는 오히려 더 불투명하게 만든다고 생각하기 때문입니다." – Mike Perham, Sidekiq 제작자
결론
메타프로그래밍이 나쁘기만 한 것은 아닙니다. 적절한 상황에서 활용하면 코드를 더 유연하게 만들 수 있는 강력한 도구입니다.
중요한 것은 이러한 추가 비용을 인지하고 있어야, 더 나은 설계 결정을 내릴 수 있다는 점입니다.
이 글이 도움이 되었다면 주변에 공유하는 것도 잊지 마세요 🙂