루비(Ruby)의 강점은 간결함에서 기인한다. 간결함은 단순히 결과로 보이는 코드 자체만을 의미하지는 않는다. 이 글에서는 루비의 동작을 설명하는 오브젝트 모델(Object Model)의 간결함과 일관성에 주목한다. 루비에서는 모든 것이 오브젝트이다. 인스턴스(Instance)도 오브젝트이고 클래스(Class)도 오브젝트이다. 따라서 하나의 오브젝트 모델로 모든 동작 설명을 할 수 있다. 오브젝트 상호 간 또는 오브젝트와 메쏘드간의 관계는 오브젝트 모델을 통해서 정의가 된다. 가령 "이 메쏘드는 어떤 클래스에서 정의되어 있는가?", "이 모듈을 인클루드(include)하면 어떤 메쏘드를 사용할 수 있는가?", "같은 클래스로부터 생성된 두 인스턴스는 서로의 프라이빗 메쏘드(private method)를 사용할 수 있는가?" 와 같은 질문에 대한 답변은 그 언어가 어떤 오브젝트 모델을 가지고 있는가에 달라진다. 이러한 단순함은 메타프로그래밍(Metaprogramming)과 같이 서로 다른 두 영역을 다뤄야 하는 곳에서 위력을 발휘한다. 메타프로그래밍은 간단하게 말하자면 코드를 작성하는 코드(writing code that writes code)를 만드는 기법이다. 이를 루비 관점에서 조금 더 정확하게 표현하자면 변수, 클래스, 메쏘드와 같은 언어의 구성 요소를 런타임 시에 구현할 수 있다는 말과 같다.
메쏘드 룩업(Method Lookup)
아래는 새로운 클래스를 만들고, 클래스의 인스턴스에서 메쏘드를 실행하는 간단한 예제이다.

class ParentsClass
  def parents_method
    puts "parents_instance_method"
  end
end

class MyClass < ParentsClass
  def my_method
    puts "my_instance_method"
  end
end

me = MyClass.new
me.my_method # => "my_instance_method"
me.parents_method # => "parents_instance_method"
        
위 코드 중 me.my_method의 동작을 오브젝트 모델로 설명을 하면 다음과 같다.
  1. me 리시버(receiver)의 클래스를 찾는다.
    me.class => MyClass
  2. MyClass에서 my_method()가 정의되었는지를 확인한다.
    MyClass.instance_methods(false).grep(/my_method/) => [:my_method]
  3. MyClass#my_method()가 발견되었으므로 리시버 me는 my_method()를 실행한다.
me.parents_method는 다음과 같은 절차를 따른다.
  1. me 리시버(receiver)의 클래스를 찾는다.
    me.class => MyClass
  2. MyClass에서 parents_method()가 정의되었는지를 확인한다.
    MyClass.instance_methods(false).grep(/parents_method/) => []
  3. MyClass#parents_method()가 발견되지 않았으므로 MyClass의 상위클래스(superclass)를 찾는다.
    MyClass.superclass => ParentsClass
  4. ParentsClass에서 parents_method()가 정의되었는지를 확인한다.
    ParentsClass.instance_methods(false).grep(/parents_method/) => [:parents_method]
  5. ParentsClass#parents_method()가 발견되었으므로 리시버 me는 parents_method()를 실행한다.
만약 ParentsClass에서도 해당 메쏘드를 찾을 수 없다면 이 클래스의 상위 클래스인 Object 클래스에서 찾고, 여기서도 발견 안되면 그 다음 상위 클래스에서 찾게되는데 이러한 검색 경로는 ancestors()로 확인할 수 있다.
MyClass.ancestors # => [MyClass, ParentsClass, Object, Kernel, BasicObject]
이와 같은 원칙 - 즉 리시버의 클래스를 찾아서 메쏘드의 존재를 확인하고 없으면 상위 클래스로 옮겨 가는 방식 - 은 클래스 오브젝트에 대해서도 동일하게 적용된다. 위 예제 코드에서 MyClass.new는 다음과 같이 설명할 수 있다.
  1. MyClass 리시버(receiver)의 클래스를 찾는다.
    MyClass.class => Class
  2. Class에서 new()가 정의되었는지를 확인한다.
    Class에서.instance_methods(false).grep(/new/) => [:new]
  3. Class#new()가 발견되었으므로 리시버 MyClass는 new()를 실행한다.
만약 여기서도 발견이 되지 않았다면 Class.superclass인 Module 클래스에서 찾고, 그래도 발견이 안되면 Class.ancestors() 상의 경로에서 메쏘드 룩업이 이루어진다.
Class.ancestors # => [Class, Module, Object, Kernel, BasicObject]
루비 커뮤니티에서는 위의 내용을 시각화하여 아래와 같은 그림을 그린 후 "오른쪽으로 한번 그리고 위로(One step to the right, then up)"라는 표현으로 설명하곤 하는데 복잡한 상속과 모듈 구성을 가진 코드에서 전체적인 개요를 파악하는데 도움이된다.
me MyClass #my_method ParentsClass #parents_method BasicObject ... Kernel ... Object ... Class #new Module #ancestors BasicObject ... Kernel ... Object ... class class superclass superclass superclass superclass
이제 우리는 위와 같은 그림이 주어지면 MyClass.ancestors()는 동작하고 me.ancestors()는 안되는지를 단번에 알 수 있어야 한다. 그런데 이 그림으로만 보면 Class.ancestors()는 사용할 수 없는 것처럼 보이지만 우리는 이미 앞서 사용한 적이 있다. 어떻게 동작할 수 있었을까? 다시 한 번 위에서 했던 절차대로 진행을 해보자.
  1. Class 리시버(receiver)의 클래스를 찾는다.
    Class.class => Class
  2. Class에서 ancestors()가 정의되었는지를 확인한다.
    Class에서.instance_methods(false).grep(/ancestors/) => []
  3. Class#ancestors()가 발견되지 않았으므로 Class의 상위클래스(superclass)를 찾는다. Class.superclass => Module
  4. Module ancestors()가 정의되었는지를 확인한다.
    Module.instance_methods(false).grep(/ancestors/) => [:ancestors]
  5. Module#ancestors()가 발견되었으므로 리시버 Class는 ancestors()를 실행한다.
Class Class #new Module #ancestors class superclass
마지막으로 한가지 예를 더 살펴보자. 처음 그림을 보면 BasicObject, Kernel, Object는 양쪽 모두의 ancestor 체인에 포함된 것을 알 수 있다. 따라서 Kernel에서 정의한 object_id()는 me.object_id()와 같은 형태로도 사용할 수 있고, MyClass.object_id()처럼도 사용할 수 있다.
클래스 메쏘드(Class Method)
아래 코드는 인스턴스 메쏘드와 클래스 메쏘드의 정의하고 사용하는 법을 보여주는 것으로 익숙하면서도 평범한 코드이다.

class MyClass < ParentsClass
  def self.my_class_method
    puts "my_class_method"
  end

  def my_method
    puts "my_instance_method"
  end
end

me = MyClass.new
me.my_method # => "my_instance_method"
MyClass.my_class_method # => "my_class_method"
        
그런데 my_class_method는 오브젝트 모델로 설명하면 어디에 위치하는 것일까? 우리는 위에서 MyClass.my_class_method()와 같은 형태로 사용되기 위해서는 MyClass의 클래스인 Class 클래스와 그 ancestor 경로 중 어딘가에 my_class_method()가 정의되어 있어야 함을 이미 알고 있다. 하지만 경로 상의 어떤 클래스에서도 이 메쏘드는 발견되지 않는다. 만약 이 경로 상에 존재한다면 ParentsClass.my_class_method()도 가능하다는 말인데 실제 이렇게는 사용할 수 없는 것으로 봐서 이 경로에 없는 것은 일견 타당한 면이 있다. 답을 찾기 위해서 다음과 같은 오브젝트 모델을 먼저 그려보자.
MyClass ??? #my_class_method Class #new class superclass
오브젝트 모델상으로는 완벽한 설명이 가능해지는데 실제로 이렇게 존재하는 것일까? 답은 "그렇다"이다. 여기서 물음표로 표시된 클래스는 MyClass의 싱글톤(Singleton) 클래스로서 오직 MyClass만을 위해 존재한다. 실제로 MyClass.singleton_class를 입력하면 #<Class:MyClass>가 출력되는데 MyClass의 싱글톤 클래스를 의미한다.
그렇다면 혹시 me와 같은 인스턴스도 싱글톤 클래스를 가질 수 있는지가 궁금해진다. 결론은 "역시 가질 수 있다"이다. 아래의 예를 보면 me와 brother는 모두 MyClass.new를 통해서 생성된 인스턴스이다. 그런데 me.my_singleton()은 실행 가능한 반면 brother.my_singleton()은 불가하다. 이 말은 my_singleton()은 오직 me 만을 위한 싱글톤 클래스에 정의되어 있어야 함을 의미하는 것이다.

me = MyClass.new
brother = MyClass.new

def me.my_singleton()
  puts "my_singleton_method"
end
me.my_singleton # => "my_singleton_method"
brother.my_singleton # => undefined method
        
아래 그림은 싱글톤 클래스을 고려한 수정된 오브젝트 모델이다. 그림에서 ^는 싱글톤 클래스임을 표현하기 위해 사용한 기호이다. MyClass의 싱글톤 클래스의 상위 클래스는 ParentsClass의 싱글톤 클래스임을 주목한다.
me ^me #my_singleton MyClass #my_method ParentsClass #parents_method Object ... ^MyClass #my_class_method ^ParentsClass ... Class #new right right up up up up up
instance_eval()과 class_eval()
BasicObject#instance_eval()Module#class_eval()은 사용 방식은 유사하나 사용 목적은 전혀 다른 두 메쏘드이다. 형식면에서 볼 때 두 경우 모두 주어진 블럭(block)내 코드를 리시버(receiver)의 컨텍스트(context)상에서 실행한다. 이 말은 블럭 안에서는 self가 리시버로 바뀐다는 것을 의미한다. 일반적으로 블럭은 정의될 때 그 순간의 binding - self, 로컬 변수, 인스턴스 변수 등 - 을 그대로 캡쳐해서 블럭 안으로 끌고 가기 때문에 self의 변경이 일어나지 않는 것이 원칙이기 때문에 *_eval()의 동작 방식은 매우 예외적이다.

class MyClass
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def self.my_name
    puts "my name is #{name}."
  end

  def my_name
    puts "my name is #{name}."
  end
end
        

me = MyClass.new("me")

me.instance_eval do
  my_name
end
# => my name is me.

MyClass.instance_eval do
  my_name
end
# => my name is MyClass.

MyClass.class_eval do
  my_name
end
# => my name is MyClass.
위의 예에서는 *_eval()의 블럭 내부에서 리시버를 지정하지 않은 상태로 my_name()을 호출하고 있다. 이런 경우에는 self.my_name()이 적용되므로 self에 따라서 호출되는 my_name()이 정해진다. 결과를 통해서 알 수 있듯이 블럭 안에서는 *_eval()의 리시버가 self의 역할을 하고있다. 여기서 한가지 더 언급할 내용은 class_eval은 오직 클래스에 대해서만 사용할 수 있는 반면에 instance_eval은 클래스와 인스턴스 모두에 사용할 수 있다는 점인데, class_eval()은 Module에 정의되어 있고 instance_eval()는 BasicObject에 정의되어 있음을 생각하면 당연한 결과라고 할 수 있다. 이 외에도 class_eval과 instance_eval은 블럭안에서 Current Class가 적용되는 방식에서 차이점을 보인다. 이 차이점을 이해하기 위해서 블럭 안에서 메쏘드를 만들고 이 메쏘드가 어떤 클래스에 저장되는지를 살펴보자.

MyClass.class_eval do
  def method_A
  end
end

me.instance_eval do
  def method_B
  end
end

MyClass.instance_eval do
  def method_C
  end
end
me ^me #method_B MyClass #method_A ^MyClass #method_C right right up
위 그림은 me.method_A, me.method_B, MyClass.method_C 형식으로 메쏘드를 사용할 수 있음을 보여주고 있는데 어떻게 이런 결과가 나온 것일까? 루비에서 메쏘드는 정의되는 시점의 Current Class의 소속이 되는 규칙이 있다. class_eval은 블럭 안에서 이 Current Class를 self로 변경한다. 따라서 위 예에서는 MyClass가 Current Class가 되고, method_A는 MyClass에 속한 메쏘드가 되어 MyClass의 인스턴스에서 사용할 수 있게 되는 것이다. 한편 instance_eval는 블럭 안에서 Current Class를 self의 싱글톤 클래스로 변경한다. 위 두번째 예에서 블럭안의 self는 me이므로 method_B는 me의 싱글톤 클래스(^me)의 소속이 되고, 마지막 예에서는 블럭안의 self가 MyClass이므로 MyClass의 싱글톤 클래스(^MyClass)에 method_C가 소속된다. 지금까지 *_eval()이 동작하는 방식과 차이점을 살펴 보았다. 그런데 사실 이러한 문법적인 이해보다 중요한 것은 실전에서 이런 메쏘들이 어떻게 활용되는가를 아는 것이다. 일반론적으로 말하자면 class_eval은 동적으로 클래스를 열고 그 안에 메쏘드를 추가하기 위한 절차를 편하게 수행하기 위해서 사용하며, instance_eval은 블럭안에서 self 값을 변경해야 하는 상황(예, 테스팅에서 프라이빗 메쏘드 접근)에 대처하기 위해 사용한다. 구체적인 사용 사례는 메타프로그래밍을 주제로 하는 글에서 별도로 다루기로 한다.