티스토리 뷰

Ruby

[Ruby] 루비 메타프로그래밍(1) - Object Model

강씨아저씨 2018. 6. 16. 12:12

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

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

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


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


Metaprogramming 이란?

Ruby 를 사용하면서 접하게된 개념이다. 우선 언제나 그렇듯 wiki의 정의부터 읽어보자.

메타프로그래밍(metaprogramming)이란 "자기 자신 혹은 다른 컴퓨터 프로그램을 데이터로 처리함으로써 프로그램을 작성·수정하는 프로그램을 작성하는 것을 말한다. 넓은 의미에서, 런 타임에 수행해야 할 작업의 일부를 컴파일 타임 동안 수행하는 프로그램을 말하기도 한다."  

라고 wiki에 소개되어있다. 메타프로그래밍을 소개하는 다른 블로그에서 보면 '코드를 작성하는 코드' 혹은 '프로그램을 위한 프로그램' 이라고 소개한다.

그런데 이렇게만 들었을때는 쉽게 이해가 가지 않는다... 코드를 작성하는 코드란 대체 뭘까? 


책과 블로그를 보고 내가 이해한 메타프로그래밍의 정의는 아래와 같다. 

"메타프로그래밍은 프로그램이 실행중에 다른프로그래밍 코드를 읽고, 생성하고, 변경할 수 있게 하며 심지어는 자신의 구조도 스스로 수정이 가능하게 하는 프로그래밍 방법이다." 


넓은 의미에서의 메타프로그래밍을 예를 들어보자..

주석이 추가된 Java코드를 작성하면 이를 읽고 XML 파일을 생성해내는 코드 생성기가 있다고 치자.. 넓은 의미에서 보면 이 XML 생성기는 메타 프로그래밍의 한 예이다. 실행중에 Java코드를 읽고 XML파일을 생성하기 때문이다. 


조금더 좁은 의미에서의 메타프로그래밍은 "실행중 스스로의 구조를 조작(생성,수정,삭제)하는 프로그래밍"을 메타프로그래밍이라 이해했다. 그리고 이렇게 실행중 자신의 구조를 스스로 구조를 조작하는 것을 코드를 작성하는 코드 라고 표현한다고 이해했다. 

여러곳에서 소개하는 메타프로그래밍의 예로는 C++ 템플릿 메타프로그래밍이 있다. 


그리고 책에서는 Ruby는 컴파일없이 런타임시점에 코드를 읽기 때문에 메타프로그래밍에 친화적인 언어라고 소개한다. 

실제로 Ruby에서는 실행중 클래스에 Method를 추가하거나 Method를 변경하는 등의 메타프로그래밍이 가능하다.

# 아무 함수도 없는 EvalClass를 정의 한다.
class EvalClass
end 
instance = EvalClass.new 

# hello 라는 함수가 없는 것을 확인 한다.
instance.hello # undefined method 'hello' for #<EvalClass ....>

# 실행중 class_eval  을 이용해서 없던 함수를 추가한다.
EvalClass.class_eval do
  def hello
    puts 'class_eval bind method'
end
end	
instance.hello # => class_eval bind method

# 실행중 class_eval 을 이용해서 방금 추가했던 함수를 같은이름의 새로운함수로 교체한다!'
EvalClass.class_eval do
  def hello
    puts 'class_eval modify method'
  end
end	
instance.hello # => class_eval modify method

EvalClass 클래스에 정의되지 않았던 함수를 정의 시킬수 있고 실행중간에 바꿀수도 있다!


Object Model

메타프로그래밍은 앞에서 설명한것과 같이 스스로의 구조를 조작 할 수 있기 때문에 무엇이 좋고 나쁜지를 알아야 제대로 사용 할 수 있다.

우선 알아야할 것은 Ruby의 모든 Constructs 는 Object Model 이라고 불리는 시스템 안에서 존재한다는 것이다. 

Object Model 은 "이 함수는 어디에서 정의된 함수인가?", "이 Module을 include 하면 어떤일이 생길까?" 에 대한 답을 구하는 과정을 통해서 이해할 수 있다.



Open Classes(오픈클래스)

오픈 클래스는 정의가 끝난 클래스를 확장하는 기능을 루비에서는 Open Classes(오픈클래스)라 부른다.

정의가 끝난 클래스를 확장한다는것이 어떤것인지 예제를 통해서 보자. 

만약 텍스트에서 숫자와 문자만을 뽑아서 나타내는 alphanumeric 기능이 필요하다고 가정해보자. 

일반적으로는 아래와 같은 함수를 만들것이다. 

def to_alphanumeric(s)
  s.gsub(/[^\w\s]/, '')
end

그리고 이 함수를 사용할때는 다음과 같은 방법으로 사용할 것이다. 

to_alphanumeric('#3, the *Magic, Number*?')  # => 3 the Magic Number


하지만 to_alphanumeric 함수는 String 객체들만 사용하기때문에 별도의 전역함수보다는 String 클래스의 함수로 있는것이 더 객체지향적인 구조다.


그렇다면 조금더 객체지향적인 구조로 변경하기위해서는 아래와 같이 바꿀수 있다. 

class String
  def to_alphanumeric
    gsub(/[^\w\s]/, '')
  end
end

그리고 함수 사용법도 아래와 같이 변경된다.

'#3, the *Magic, Number*?'.to_alphanumeric  # => 3 the Magic Number


우리는 방금 이미 정의가 끝난 클래스인 String 클래스에 to_alphanumeric 이라는 함수를 추가하면서 기능을 확장했다.


Open Classes 의 문제점

오픈클래스는 강력한 기능이지만 강력한 만큼 주의할 점이 있다.

예를들어 위의 예제처럼 String 클래스가 가지고 있는 함수에 replace라는 함수를 새로 추가했을때 

String 클래스에는 이미 replace라는 함수가 정의되어 있다면 문제가 발생한다.

물론 본인이 String에 replace를 추가했다면 본인은 replace가 어떠한 기능을 하고 있는지 알고 있기때문에
오류가 없을수 있지만 누군가 다른 사람이 기존에 작성한 소스코드에서 String의 replace 함수를
사용하고 있었다면 그 사람이 작성한 소스코드는 갑자기 엉뚱한 동작을 하게 될 것이고
왜 엉뚱하게 동작하는지를 찾아내기란 쉽지 않을것이다. 


그래서 일부 개발자들은 이러한 기능의 확장을 지양하려고 하고 Monkeypatch 라고 부르기도 한다. 


Object(객체) 안에는 무엇이 있을까?

Instance Variables(인스턴스 변수)

가장 중요하게 알아야 할 것은 객체안에는 인스턴스 변수가 포함되어 있다는 것이다. 

그리고 우리는 객체안에 있는 인스턴스변수를 아래와 같이 확인할 수 있다.

class MyClass
  def my_method
    @v = 1
  end
end

obj = MyClass.new
obj.class # MyClass
# obj 의 인스턴스 변수 목록을 조회해 본다.
obj.instance_variables # => []

# my_method 함수를 호출해서 @v에 1을 할당한다. 
obj.my_method # => 1

# 다시한번 obj의 인스턴스 변수 목록을 조회해 본다.
obj.instance_variables # => [:@v]


여기서 또 한가지 알아야 할 점은 인스턴스 변수인 @v는 my_method 함수를 호출 하기 전까지는 obj의  존재하지 않았다는 것이다. 

객체의 인스턴스 변수는 해당 변수를 사용할때 그 객체의 인스턴스 변수로 포함된다.


Methods(함수)

객체는 인스턴스 변수뿐만 아니라 함수도 가지고있는 것 처럼 보인다

객체가 가지고있는 함수들을 조회하기 위해서는 Object#methods 함수를 호출하면 확인 할 수있다. 

(위의 예제에서 사용한 obj를 포함한 대부분의 객체들은 Object를 상속받고 있다.)

obj.methods.grep(/my/) # => [:my_method]

하지만 만약 Ruby interpreter를 가지고 obj를 확인해보면 객체(obj)는 인스턴스 변수와 클래스에 대한 레퍼런스만 갖고 있고 함수에 대한 정보는 갖고 있지 않다. 그렇다면 실제 함수에 대한 정보는 어디에 있는걸까?

같은 클래스를 공유하는 객체들은 같은 함수를 가지고 있기 때문에 함수는 객체가 아닌 클래스에서 가지고 있다. 




함수에 대해 정리하자면 my_method( )를 사용하기 위해서는 MyClass의 객체가 필요하고 

my_method( )는 MyClass에 정의 되어 있는 MyClass의 instance_method이다.

그리고 이러한 함수는 객체입장에서 얘기할때는 method 이고 클래스입장에서 얘기할때는 instance_method 이다. 



인스턴스 변수와 함수에 대해서 정리하자면 

인스턴스 변수는 객체 안에 존재하고 객체가 사용하는 함수는 클래스에 존재하고 클래스의 모든 객체들은 함수를 공유한다. 


String.instance_methods == "abc".methods # => true
String.methods == "abc".methods # => false



Classes 에 대한 진실

클래스에 대해서 가장 중요한 사실은 우리가 정의한 클래스도 Class클래스의 객체 라는 것이다. 


처음에 내가 가장이해가 가지 않았던 부분이다... 

기존에 내가 알던 클래스는 역할에 맞는 함수와 변수를 가지고 있는 추상적개념이었는데 어떻게 클래스가 객체이지!? 라고 생각했었다.

그러나 Ruby에서 우리가 정의한 클래스는 Class클래스의 객체이다! 코드로 보자면 대략 이런느낌이다.


MyClass = Class.new do  
end

# MyClass 라는 이름의 객체는 Class 클래스에서 .new로 생성된 객체이다!
# 위의 코드는 아래의 코드와 동일하다.
class MyClass
end

그렇다면 직접 클래스의 정보를 확인해 보자. 

"hello".class # => String
String.class # => Class


Class클래스가 가지고 있는 instance_methods를 확인해 보면 다음과 같다.  

Class.instance_methods(false) # => [:allocate, :new, :superclass]


Class클래스가 new, allocate 함수를 갖고 있기 때문에 우리가 정의한 클래스(MyClass)에서 #new를 사용해서 객체생성이 가능하다!

그리고 superclass 는 상속관계에 있는 부모 클래스를 나타내는 함수이다. 

String 클래스의 superclass 를 쭉 확인해 보면 다음과 같다. 

String.superclass # => Object
Object.superclass # => BasicObject
BasicObject.superclass # => nil

String 클래스는 Object 클래스를 상속받았고 Object 클래스는 BasicObject 클래스를 상속받았다.

그러면 Class의 superclass는 누구일까?


Module

다음을 보면 알 수 있듯이 Class 클래스의 superclass는 Module 이다.

Class.superclass # => Module

정리해보자면 Class는 추가적으로 allocate, new, superclass 라는 instance_method를 갖고있는 Module 이다. 

우리가 정의한 Module을 객체로 생성할 수 없는 이유는 Module의 instance_method 에는 allocate, new가 없기 때문이다.


그러면 Module의 superclass는 무엇일까?

Module.superclass # => Object
Object.superclass # => BasicObject
BasicObject.superclass # => nil


그런데 Class, Module, Object, BasicObject의 class를 조회해 보면 어떻게 될까?

Class.class # => Class
Module.class # => Class
Object.class # => Class
BasicObject.class #=> Class


이러한 관계를 그림으로 그려보면 다음과 같다. 


관계를 보면 알 수 있듯이 Class는 Module을 상속받고 있고 Module은 Object를 상속받고 Object는 BasicObject를 상속받고 있다.

그리고 BasicObject, Object, Module, Class 또한 모두 Class클래스의 객체이다!

그런데 이상한점은 Class는 Module 을 상속받는데 Module 은 Class의 객체이다....

그러면 닭이 먼저 인가 달걀이 먼저인가 Module, Object, BasicObject이 먼저인가 Class가 먼저인가... @_@....

뭐가 어떻게 되는거지..!?!? 가능한건가!?!?!

어떻게 이러한 관계를 가질수 있는지에 대해서 궁금해서 쭉 찾아봤는데 이렇다한 설명이 없어서 그냥 맨땅에 해딩으로 찾아봤다..


이런 상속관계는 어떻게 만들어 진걸까? 정확히는 알 수 없지만 Ruby의 소스코드를 받아보면 대충은 예상할 수 있다... 

(정확하지 않을수 있다..)

class.c
Init_class_hierarchy(void)
{
    rb_cBasicObject = boot_defclass("BasicObject", 0);
    rb_cObject = boot_defclass("Object", rb_cBasicObject);
    rb_gc_register_mark_object(rb_cObject);

    /* resolve class name ASAP for order-independence */
    rb_class_name(rb_cObject);

    rb_cModule = boot_defclass("Module", rb_cObject);
    rb_cClass =  boot_defclass("Class",  rb_cModule);

    rb_const_set(rb_cObject, rb_intern_const("BasicObject"), rb_cBasicObject);
    RBASIC_SET_CLASS(rb_cClass, rb_cClass);
    RBASIC_SET_CLASS(rb_cModule, rb_cClass);
    RBASIC_SET_CLASS(rb_cObject, rb_cClass);
    RBASIC_SET_CLASS(rb_cBasicObject, rb_cClass);
}


위의코드와 같이 BasicObject, Object, Module, Class를 먼저 정의하고 상속관계를 정의한다.

그다음에 각각의 Class의 객체들이 가지고 있는 함수들은 아래의 코드를 보면 다음과 같이 정의한다... 

object.c
void
InitVM_Object(void)
{
    Init_class_hierarchy();

#if 0
    // teach RDoc about these classes
    rb_cBasicObject = rb_define_class("BasicObject", Qnil);
    rb_cObject = rb_define_class("Object", rb_cBasicObject);
    rb_cModule = rb_define_class("Module", rb_cObject);
    rb_cClass =  rb_define_class("Class",  rb_cModule);
#endif

#undef rb_intern
#define rb_intern(str) rb_intern_const(str)

    rb_define_private_method(rb_cBasicObject, "initialize", rb_obj_dummy, 0);
    rb_define_alloc_func(rb_cBasicObject, rb_class_allocate_instance);
    rb_define_method(rb_cBasicObject, "==", rb_obj_equal, 1);
    rb_define_method(rb_cBasicObject, "equal?", rb_obj_equal, 1);
    rb_define_method(rb_cBasicObject, "!", rb_obj_not, 0);
    rb_define_method(rb_cBasicObject, "!=", rb_obj_not_equal, 1);

    rb_define_private_method(rb_cBasicObject, "singleton_method_added", rb_obj_dummy, 1);
    rb_define_private_method(rb_cBasicObject, "singleton_method_removed", rb_obj_dummy, 1);
    rb_define_private_method(rb_cBasicObject, "singleton_method_undefined", rb_obj_dummy, 1);

    rb_mKernel = rb_define_module("Kernel");
    rb_include_module(rb_cObject, rb_mKernel);
    rb_define_private_method(rb_cClass, "inherited", rb_obj_dummy, 1);
    rb_define_private_method(rb_cModule, "included", rb_obj_dummy, 1);
    rb_define_private_method(rb_cModule, "extended", rb_obj_dummy, 1);
    rb_define_private_method(rb_cModule, "prepended", rb_obj_dummy, 1);
    rb_define_private_method(rb_cModule, "method_added", rb_obj_dummy, 1);
    rb_define_private_method(rb_cModule, "method_removed", rb_obj_dummy, 1);
    rb_define_private_method(rb_cModule, "method_undefined", rb_obj_dummy, 1);
    ....
    rb_define_method(rb_mKernel, "class", rb_obj_class, 0);
    .... 
    rb_define_method(rb_cModule, "freeze", rb_mod_freeze, 0);
    rb_define_method(rb_cModule, "===", rb_mod_eqq, 1);
    rb_define_method(rb_cModule, "==", rb_obj_equal, 1);
    rb_define_method(rb_cModule, "<=>",  rb_mod_cmp, 1);
    rb_define_method(rb_cModule, "<",  rb_mod_lt, 1);
    rb_define_method(rb_cModule, "<=", rb_class_inherited_p, 1);
    rb_define_method(rb_cModule, ">",  rb_mod_gt, 1);
    rb_define_method(rb_cModule, ">=", rb_mod_ge, 1);
    rb_define_method(rb_cModule, "initialize_copy", rb_mod_init_copy, 1); /* in class.c */
    rb_define_method(rb_cModule, "to_s", rb_mod_to_s, 0);
    rb_define_alias(rb_cModule, "inspect", "to_s");
    rb_define_method(rb_cModule, "included_modules", rb_mod_included_modules, 0); /* in class.c */
    rb_define_method(rb_cModule, "include?", rb_mod_include_p, 1); /* in class.c */
    rb_define_method(rb_cModule, "name", rb_mod_name, 0);  /* in variable.c */
    rb_define_method(rb_cModule, "ancestors", rb_mod_ancestors, 0); /* in class.c */

    rb_define_method(rb_cModule, "attr", rb_mod_attr, -1);
    rb_define_method(rb_cModule, "attr_reader", rb_mod_attr_reader, -1);
    rb_define_method(rb_cModule, "attr_writer", rb_mod_attr_writer, -1);
    rb_define_method(rb_cModule, "attr_accessor", rb_mod_attr_accessor, -1);

    ....
    rb_define_method(rb_cModule, "singleton_class?", rb_mod_singleton_p, 0);

    rb_define_method(rb_cClass, "allocate", rb_class_alloc, 0);
    rb_define_method(rb_cClass, "new", rb_class_s_new, -1);
    rb_define_method(rb_cClass, "initialize", rb_class_initialize, -1);
    rb_define_method(rb_cClass, "superclass", rb_class_superclass, 0);
    rb_define_alloc_func(rb_cClass, rb_class_s_alloc);
    rb_undef_method(rb_cClass, "extend_object");
    rb_undef_method(rb_cClass, "append_features");
    rb_undef_method(rb_cClass, "prepend_features");
	....
}




오늘 알아본 내용을 요약하면

1. 메타프로그래밍은 실행중 스스로의 구조를 조작할 수 있게 하는 방법이다. 

2. OpenClasses(오픈클래스)는 이미 정의된 클래스의 기능을 확장하는 방법이다. 

2-1. 강력한 기능이지만 잘못쓰면 부작용이 있다.

2-2. 일부개발자들은 이러한 부작용때문에 오픈클래스를 지양하고 Monkeypatch 라고 부른다.

3. Object는 인스턴스 변수와 함수를 가지고 있다.

3-1. 정확히 Object(객체)는 인스턴스를 가지고 있고 함수는 Object의 클래스가 가지고 있다. 

4. 클래스 또한 Class클래스의 Object(객체)이다!

4-1 Class클래스는 Module을 상속받았다!

4-2 Module의 부모클래스를 찾아가다 보면 BasicObject까지 간다!


오늘은 힘들어서 여기까지! 더이상 못하겠다!!!

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

댓글