Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

메타프로그래밍의 숨겨진 비용: 속도, 가독성, 검색성 문제

메타프로그래밍(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

YardRDoc 같은 문서 생성 도구는 이러한 메서드를 찾아 목록에 표시하지 못합니다.

이 도구들은 클래스와 메서드를 찾기 위해 "정적 분석(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 제작자

결론

메타프로그래밍이 나쁘기만 한 것은 아닙니다. 적절한 상황에서 활용하면 코드를 더 유연하게 만들 수 있는 강력한 도구입니다.

중요한 것은 이러한 추가 비용을 인지하고 있어야, 더 나은 설계 결정을 내릴 수 있다는 점입니다.

이 글이 도움이 되었다면 주변에 공유하는 것도 잊지 마세요 🙂