티스토리 뷰

오늘의 포스팅 내용은 Ruby Metaprogramming 이다. 

포스팅의 내용은 사내에서 Ruby Metaprogramming 에 대한 발표를 위해서 아래의 책을 읽고 내용들을 정리한것이다.

https://books.google.co.kr/books/about/Metaprogramming_Ruby_2.html?id=V0iToAEACAAJ&source=kp_cover&redir_esc=y


내용이 많기때문에 시리즈로 소개할 예정이다. 


저번 포스팅(Object Model - http://idea-sketch.tistory.com/36 )에 이어서 다음편이다. 


함수를 사용하면 무슨일이 생길까?

Ruby에서는 함수를 사용할때 아래의 2가지 작업을 한다.

  1. 해당 함수를 찾는다. 이 과정을 Method Lookup 이라고 한다. 
  2. 해당 함수를 실행한다. 함수를 실행하기 위해서 Ruby는 self 를 필요로 한다.  

Method Lookup

가장 간단한 상황을 예로들면 Ruby는 함수를 사용할때 해당 객체의 클래스를 확인하고 그곳에서 함수를 찾는다.

조금더 복잡한 상황을 설명하려면 우선 receiver ancestors chain 에 대해서 이해해야 한다. 


receiver는 함수를 호출한 Object 이다. 예를들어 my_class.hello( ) 를 호출했을때 my_class는 receiver 이다.


ancestors chain 은 함수를 호출한 my_class 에서 해당 함수가 없을경우 my_class 의 superclass 에서 다시 함수를 찾고

그곳에도 없을경우  superclass의 superclass에서 해당 함수를 찾아보고 이를 BasicObject 까지가는 과정이다. 

( ancestors chain 에서는 include 된 module 에서도 함수를 찾는 과정이 있다. 이는 아래에서 설명할 예정이다. )


즉 우리가 함수를 사용한다면 Ruby는 receiver로 부터 시작해서 해당 함수를 찾을때까지 ancestors chain을 수행한다.


이를 코드와 그림으로 예를 들어보면 다음과 같다. 

class MyClass
  def hello
    puts 'MyClass hello instance method'
  end
end

class MySubClass < MyClass
end

my_sub_class = MySubClass.new
my_sub_class.hello() # =>  MyClass hello instance method



다시한번 설명하자면 my_sub_class.hello( ) 를 실행하면 Ruby는 receiver 인
my_sub_class 에서 hello( ) 함수를 찾아보고 없을경우 오른쪽인 MySubClass로 이동한다.
MySubClass 에서 다시한번 hello( ) 함수를 찾아보고 없을경우 위로 올라간다.

MyClass에서 hello( ) 함수를 찾고 이를 실행한다.

(MyClass는 별도로 상속을 받지 않았기 때문에 기본적으로 Object를 상속받는다.)


이러한 과정을 "one step to the right, then up"  라고 부른다. 


MySubClass의 ancestors chain을 확인해보면 아래와 같다. 

MySubClass.ancestors # => [MySubClass, MyClass, Object, Kernel, BasicObject]


여기서 처음보는 녀석인 Kernel이 있다! 여기에 있는 Kernel은 Class가 아닌 Module 이다. 


Module 과 Lookup

우리는 방금 ancestors chain이 클래스 로부터 superclass 방향으로 진행한다고 이해했다. 

하지만 실은 include 된 모듈(Module) 들도 ancestors chain 에 대상에 들어간다. 

Ruby는 모듈을 클래스 혹은 다른 모듈에 include 했을때 해당 모듈을 ancestors chain에 넣는다. 


예를들면 아래와 같다.

module MyModule
  def hello
    'MyModule hello instance method'
  end
end

class MyClass
  include MyModule
end

class MySubClass < MyClass
end

MySubClass.ancestors # =>[MySubClass, MyClass, MyModule, Object, Kernel, BasicObject]


Ruby 2.0 에서부터는 모듈을 insert 하는 다른방법인 prepend 가 추가되었다. 

prepend는 include와 비슷하게 동작하지만 prepend 하는 모듈 혹은 클래스 보다 ancestors chain 의 앞에 들어간다



다시한번 예를 들어보겠다.

module MyModule
  def hello
    'MyModule hello instance method'
  end
end

class MyClass
  prepend MyModule
end

class MySubClass < MyClass
end

MySubClass.ancestors # =>[MySubClass, MyModule, MyClass, Object, Kernel, BasicObject]


그림으로 정리해보면 아래와 같다. 




다중 include


그렇다면 include를 다중으로 사용하면 ancestors chain 은 어떻게 될까?

코드로 확인해 보자.

module Module0
end

module Module1
end

module Module2
  include Module1
  include Module0
end

module Module3
  prepend Module1
  include Module2
end

Module3.ancestors  # => [Module1, Module3, Module2, Module0]


위의 코드에서 보는거와 같이 Module3는 Module1을 prepend 했고 그 후 Module2를 include 했다.  

그런데 Module2 또한 Module1을 include 했다 그러면 Module1은 어떻게 되는걸까? 이때 Module3의 ancestors chain 에는

Module1이 이미 prepend로 들어가 있기때문에 Module2에서의 Module1 include는 무시된다. 

Ruby는 include, prepend를 수행할때 ancestors chain에 해당 Module이 존재할경우 무시한다.


Kernel

Ruby는 print와 같이 우리의 코드 어디에서나 사용할 수 있는 함수들을 가지고 있다. 그리고 이러한 함수들은 실제로 

우리코드 어느곳에서나 사용하고 있다. 

이렇게 print와 같이 어디서나 사용되는 함수들은 사실 Kernel 의 private_instance_method 이다.

Kernel.private_instance_methods.grep(/^pr/) # => [:printf, :print, :proc]


그러면 Kernel의 함수가 어떠한 원리로 모든 코드에서 사용이 가능한걸까?

Object 는 Kernel을 include 하고있고 모든 클래스들은 Object를 상속받고 있기때문에

모든 클래스는 Kernel을 ancestors chain에 포함하고 있다!!


함수 실행

Ruby에서 함수를 실행하면 아까 위에서 설명했듯이 2가지의 작업을 실행한다. 

  1. 함수를 receiver와 ancestors chain을 이용해서 찾는다. 
  2. 함수를 실행한다. 

이번에는 2. 함수의 실행에 대해서 알아보자.


우리가 Ruby interpreter 입장에서 생각해 봤을때 아래와 같은 코드가 있다 치자.

def hello
  temp = @v+1
  other_method(temp)
end

그리고 누군가가 hello 함수를 실행했을때 interpreter인 우리는 2가지 질문에 답해야 한다. 

  1. @v 는 어떤 객체의 instance_variable 인가?
  2. 어떤 객체의 other_method(temp) 함수를 호출해야 하는가?

위의 2가지 질문에 대한 대답은 모두 receiver 이다.  Ruby는 함수를 실행할때 receiver 에 대한 reference를 기억해 놓는다.

아래와 같은 코드가 있을때 결과가 어떨지 한번 예상해보자. 

module Printable
  def print
    puts 'Printable print'
  end

  def prepare_cover
    puts 'Printable prepare_cover'
  end
end

module Document
  def print_to_screen
    prepare_cover # => self.prepare_cover 와 동일하다.
    format_for_screen # => self.format_for_screen 과 동일하다.
    puts "#{self.object_id}"
    print # => self.print 와 동일하다.
  end
  
  def format_for_screen
    puts 'Document format_for_screen'
  end
  
  def print
    puts 'Document print'
  end
end

class Book
  include Document
  include Printable
end

book = Book.new
book.object_id # => 2494200
book.print_to_screen 

결과는 다음와 같다. 

# => Printable prepare_cover
# => Document format_for_screen
# => 2494200
# => Printable print

Document 에 정의되어있는 print_to_screen( ) 의 함수내부에서 print( ) 함수를 호출 할때 

Document 에 정의되어있는 print( ) 를 호출하지 않은 이유는 무엇일까?

그 이유는 Book 클래스의 include 순서때문이다.

지금 Book클래스의 ancestors chain을 확인해보면 아래와 같다. 

Book.ancestors # =>  [Book, Printable, Document, Object, Kernel, BasicObject]

위와 같이 Printable이 Document보다 ancestors chain 이 앞에 있기 때문에 

book.print_to_screen 함수를 호출하면 ancestors chain을 타고 Document의 print_to_screen 함수를 실행하게 되고

print_to_screen 함수내의 함수들은 다시 self 기준으로 ancestors chain을 실행하는데 

지금 self 는 receiver 인 book 객체이므로 Printable의 print 함수가 호출된다! 


self 키워드

Ruby의 객체 안에서 실행되는 모든 코드는 self 라는 키워드를 통해서 Current Object 에 접근할 수 있다.

아래의 코드를 보자.

class MyClass
  def self_test
    @var = 20 # An instance variable of self
    my_method() # Same as self.my_method()
    self
  end
  def my_method
    @var = @var + 1
  end
end
my_class = MyClass.new
my_class.self_test # => <MyClass:0x00000002719948 @var="21">


우리가 self_test 함수를 호출하면 self 는 receiver인 my_class 가 된다. 그래서 @var 는 my_class 의 instance variable 이고

my_method( ) 를 실행할때 self 는 여전히 my_class 이기때문에 my_class.my_method( ) 와 동일하다. 

특별한 2가지 경우를 제외하고는 self는 항상 마지막 함수 호출의 Object 이다. 


Top Level

우리는 어떤 객체의 함수를 호출할때 해당 객체가 self 가 된다는것을 확인했다.  

그렇다면 아직 아무 함수도 호출하지 않았을때의 self 는 무엇일까? 아래의 코드로 확인해보자. 

self # => main
self.class # => Object

Ruby를 실행하면 Ruby Interpreter 가 우리를 위해 main 이라는 이름의 객체를 설정해 놓는다. 

이 main 이라는 객체는 call stack에 가장 위에있기 때문에 top-level context 라고 불린다. 


클래스정의 에서의 self

Ruby에서 클래스 와 모듈을 정의하는 중에 사용하는 self 는 클래스 나 모듈 자신을 뜻한다. 

class MyClass
  self  # => MyClass
end

지금은 당연하고 큰 의미 없어 보이는 이 개념은 나중에 이 책에서 소개하는 메인 개념이 될것이다. 



오늘 알아본 내용은 정리하자면 다음과 같다. 

  1. 함수를 호출하면 함수를 찾는 Method Lookup 과 함수를 실행하는 과정을 진행한다.
  2. Method Lookup 은  ancestors chain에 따라 순서대로 검사하며 이를 "one step to the right, then up"  라고 부른다. 
  3. ancestors chain 은 Module 도 넣을수 있는데 Module을 넣기위해서는 include 와 prepend 가 가능하다. 
  4. 이미 ancestors chain 에 포함된 Module 이나 Class를 넣으려고하면 무시한다.

오늘은 여기까지!

누군가에게 도움이 되었길 바라면서 오늘의 포스팅 끝~




댓글