Ruby

[Ruby] 루비 메타프로그래밍(6) - Singleton Class

강씨아저씨 2018. 7. 14. 12:21

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

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

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


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


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


Current Class

우리는 Ruby 프로그래밍을 하면서 self 를 이용해서 항상 Current Object 를 알 수 있다.

그리고 이미 알고 있듯이 클래스 정의시에 함수를 정의하면 그 함수는 항상 Current Class의 함수이다.

Ruby 프로그래밍을 하면서 아래의 3가지만 알고 있어도 대부분의 상황에서 코드를 보고 Current Class를 추정하기는 어렵지 않다. 

  1. Top-Level 에서 Current Object 는 main 이고 Current Class 는 Object 이다.
  2. 함수에서의 Current Class는 Current Object 의 Class 이다.
  3. class 키워드를 이용해서 클래스를 정의할때 Current Class는 정의하는 클래스가 된다.
그런데 우리는 가끔 우리가 사용해야 하는 클래스의 이름을 모를때가 있다. 
다음 예제는 parameter 로 전달받은 Class 객체에 m 함수를 추가하는 코드이다. 
def add_method_to(a_class)
  # TODO: define method m() on a_class
end

a_class가 어떤 Class의 객체인지 모르는상태에서 어떻게 a_class 가 의미하는 Class 에 함수를 추가할 수있을까?


class_eval( )

Module#class_eval 을 이용하면 클래스의 이름을 알지 못하는 상태에서도 해당 클래스에 함수를 추가 할 수있다.

def add_method_to(a_class)
  a_class.class_eval do
    def m; 'Hello!'; end
  end
end

add_method_to String
"abc".m # => "Hello!"

class_eval 은 앞서 배운 BasicObject#instance_eval 과는 매우 다르다.

instance_eval 은 오직 self 만 변경 가능하지만 class_eval 은 self 와 Current Class 둘 다 바꿀수 있다.

class_eval 을 사용한다는것은 class 키워드를 다시 사용한다고 생각하면 된다. 

다만 class_eval 도 지난번에 배운 Flat Scope 가 가능하다는것을 잊으면 안된다.


그러면 class_eval, instance_eval 은 어떤 상황일때 써야할까?

객체에만 접근하고 싶으면 instance_eval 을 객체의 클래스에도 접근하고 싶으면 class_eval 을 사용하면 된다.

쉽게 생각해보면 객체에 접근하고 싶지만 이 객체가 무슨 클래스의 객체인지 신경쓰고 싶지 않으면 instance_eval을 사용하고

Open Class ( http://idea-sketch.tistory.com/36 )를 하고 싶을때는 class_eval 을 사용 하면 된다. 


지금까지 배운 정보를 이용해서 실제 상황에서 어떻게 사용하는지 확인해보자. 


Class Instance Variables

class MyClass
  @my_var = 1
end

클래스 정의시 self 는 클래스 자신을 뜻한다. 

그러므로 예제의 instance_variable 인 @my_var 는 MyClass 에 instance_variable 이다. 

MyClass는 Class 클래스의 객체(http://idea-sketch.tistory.com/36)라는 것을 잊으면 안된다. 

그리고 instance_variable of Class 와 instance_variable of Class Object 는 다르다는것을 혼동하면 안된다.

class MyClass
  @my_var = 1
  def self.read; @my_var; end
  def write; @my_var = 2; end
  def read; @my_var; end
end

obj = MyClass.new
obj.read # => nil
obj.write
obj.read # => 2
MyClass.read # => 1

위의 예제는 두가지 instance_variable 을 정의하고 있다. 

둘은 동일한 이름의 my_var 를 사용하고 있지만 서로 다른 Object의 instance variable 이다.

하나는 self 가 obj 를 뜻하는 obj 객체의 instance variable 이고 다른하나는 self 가 MyClass 를 뜻하는 instance variable 이다. 

그리고 이렇게 MyClass의 instance variable 을 Class Instance Variable 이라고 부른다. 

Class Instance Variable은 오직 클래스 자신만 접근 할수있고 클래스의 객체나 Subclass 에서는 접근 할 수 없다. 


Singleton Method

만약 String 객체에 title? 이라는 함수를 이용해서 해당 string이 title 의 형식에 맞는지 확인한다고 생각해보자.

Open Class 를 이용해서 string 클래스에 title? 함수를 추가해주면 좋지만 모든 string이 title? 함수를 가질 필요는 없고

특별한 상황에서의 string 들만 title? 함수를 갖게 하고 싶다.

이럴때 사용되는것이 Singleton Method 이다.

루비는 특정 객체만 사용할 수 있는 함수를 정의할 수 있고 이렇게 정의된 함수를 Singleton Method 라고 한다. 

str = "just a regular string"
def str.title?
  self.upcase == self
end

str.title? # => false
str.methods.grep(/title?/) # => [:title?]
str.singleton_methods # => [:title?]

위의 예제는 오직 str 객체만 title? 이라는 함수를 갖고 있고 다른 String 클래스의 객체들은 title? 이라는 함수를 갖고있지 않다.

Singleton Method는 위와같은 방법과 Object#define_singleton_method 로도 정의 할 수 있다.


Class Method 에 대한 진실

이전에 배운것과 같이 우리가 정의하는 클래스(class)는 Class클래스의 객체일뿐이고 클래스 이름(ex. MyClass)은 단지 상수일 뿐이다.

이 컨셉에 대해서 알고있다면 객체의 함수를 호출한다는것과 클래스의 함수를 호출하는것은 같은것이라는 것도 알 수 있다.

an_object.a_method
AClass.a_class_method


Class Macros

Ruby 에서 객체는 기본적으로 attribute 를 갖고 있지 않기 때문에 attribute를 갖게 하기 위해서는 reader 와 writer 를 정의해야 한다. 

class MyClass
  def my_attribute=(value)
    @my_attribute = value
  end
  def my_attribute
    @my_attribute
  end
end

obj = MyClass.new
obj.my_attribute = 'x'
obj.my_attribute # => "x"

이렇게 모든 attribute 들마다 reader 와 writer 를 정의하는 일(accessors 라 부른다.)을 매우 지루한 일이된다. 

Ruby 에서는 이렇게 반복되는 작업을 하지않기 위해 Module#attr_reader 과 Module#attr_writer 를 제공한다.

그리고 이둘을 한번에 제공해주는 Module#attr_accessor 도 제공하고 있다.

이렇게 attr_accessor 처럼 클래스에서 미리 약속된 동작을 키워드 처럼 정의하고 사용하는 방법을 Class Macro 라고 부른다.

class MyClass
  attr_accessor :my_attribute
end

Class Macro는 키워드 같아 보이지만 사실은 클래스나 모듈에 정의되어 있는 함수일 뿐이다.  

Class Macro를 사용하는 예를 한번 들어보자. 


 만약 우리가 제공하고 라이브러리가 여러 프로젝트에서 사용되고 있다고 생각해보자. 

그런데 어떠한 이유로 기존에 제공되던 함수들(GetTitle, LEND_TO_USER, title2)이 새로운 이름의 함수들로 변경되어야한다. 

하지만 기존 함수들의 이름을 함부로 변경하면 우리가 제공하고 있는 라이브러리를 사용하는 모든프로젝트 들이 깨질것이다. 

이럴때 외부프로젝트에 영향을 주지 않고 깔금하게 새로운 이름으로 적용하는 방법을 보자.

class Book
  def title # ...
  def subtitle # ...
  def lend_to(user)
    puts "Lending to #{user}"
    # ...
  end

  def self.deprecate(old_method, new_method)
    define_method(old_method) do |*args, &block|
      warn "Warning: #{old_method}() is deprecated. Use #{new_method}()."
      send(new_method, *args, &block)
    end
  end

  deprecate :GetTitle, :title
  deprecate :LEND_TO_USER, :lend_to
  deprecate :title2, :subtitle
end

deprecate 라고 정의한 Class Macro 를 이용해서 자연스럽게 기존에 GetTitle 함수를 title 함수로 변경했다!!


Singleton Class

기존에 Method Lookup 을 설명할때(http://idea-sketch.tistory.com/37) Ruby에서는 ancestors chain을 확인할때 

"one step to the right, then up" 이라는것을 배웠다.

class MyClass
  def my_method; end
end

obj = MyClass.new
obj.my_method

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


그렇다면 객체인 obj 가 singleton method를 정의한다면 무슨일이 생갈까??

def obj.my_singleton_method; end

obj 는 클래스가 아니기 때문에 singleton method 는 obj에 있지 않다. 

그리고 MyClass에 있지도 않다. MyClass에 있으면 MyClass의 모든 객체들이 my_singleton_method를 공유하기 때문이다.

클래스 메소드 또한 Singleton Method의 또다른 예시이다.

def MyClass.my_class_method; end

그러면 Singleton Method는어디에 있는걸까??


지금까지 Ruby에서 객체에 대해서 얘기할때 말하지 않은 한가지 비밀이 있다. 

그것은 바로 모든 객체들은 숨겨진 자신들만의 특별한 클래스를 가지고 있다는 사실이다. 

이렇게 숨겨진 클래스를 Singleton Class 라고 부른다. ( 혹은 metaclass , engineclass 라고도 부른다. )

Object#singleton_class 함수를 통해서 singleton class를 확인할 수있다. 

"abc".singleton_class# => #<Class:#<String:0x331df0>>


다시보는 Method Lookup

Method Lookup을 다시한번 확인해보자.

class C
  def a_method
    'C#a_method()'
  end
end

class D < C; end
obj = D.new
obj.a_method # => "C#a_method()"

이를 ancestors chain 으로 확인해보면 아래와같다. 

obj 의 a_method 를 호출하면 obj의 클래스인 D(right) 에서 a_method를 찾고 없을경우 ancestors chain을 타고 위(up)로 올라간다.

그리고 singleton class는 #이라는 prefix를 갖고 있다. obj의 singleton class 는 #obj 이다. 

C 클래스 또한 Class 의 객체이기때문에 C의 singleton class인 #C가 있다. 


이번에는 singleton class와 method lookup 을 같이 생각해보자.

obj = D.new
obj.singleton_class # => #<Class:#<D:0x007fd96909b588>>

class << obj
  def a_singleton_method
    'obj#a_singleton_method()'
  end
end

obj.singleton_class.superclass # => D

obj의 singleton class 의 superclass는 D 이다.

singleton method 인 obj#a_singleton_method 를 실행할경우 Ruby는 #obj로 부터 a_singleton_method를 찾아보고 

없을경우 찾을때까지 ancestor chain 으로 올라간다. 

Singleton Class 와 상속

다음과 같은 Class Method를 정의했다 생각보자.

class C
  class << self
    def a_class_method
      'C.a_class_method()'
    end
  end
end

이러한 상황에서 지금까지 정의된 singleton class 들의 superclass는 아래와 같다. 

C.singleton_class # => #<Class:C>
D.singleton_class # => #<Class:D>
D.singleton_class.superclass # => #<Class:C>
C.singleton_class.superclass # => #<Class:Object>

정의된 클래스들을 관계를 그림으로 보면 다음과같다.





D 클래스에서 클래스 메소드를 호출하면 ancestors chain을 타고 C 싱글턴 클래스(#C)의 메소드가 호출된다.

D.a_class_method # => "C.a_class_method()"


Class Method

Class Method 는 Singleton Method 이다. 우리는 다음과 같은 3가지 방법으로 Class Method 를 정의할 수 있다. 

def MyClass.a_class_method; end

class MyClass
  def self.another_class_method; end
end

class MyClass
  class << self
    def yet_another_class_method; end
  end
end

첫번째는 MyClass인 이름이 중복되기때문에 리팩토링하기 힘들다.

두번째는 클래스 정의시에 self가 클래스 자신이라는것을 이용해서 Class Method를 정의한다.

마지막 방법이 일반적으로 Class Method 를 정의하는 방법이다. 


Singleton Class, instance_eval 

싱글톤 클래스에 대해 알아봤으니 이제 저번에 설명하지 못한 instance_eval에 대해 더 알아보자. 

위에서 instance_eval 은 오직 self 만 변경 가능하지만 class_eval 은 self 와 Current Class 둘 다 바꿀수 있다고 배웠다.

하지만 사실 instance_eval도 Current Class 를바꿀수있다.

이 예제는 instance_eval을 이용해서 싱글톤 메소드를 정의하는 예제이다.

s1, s2 = "abc", "def"
s1.instance_eval do
  def swoosh!; reverse; end
end
s1.swoosh! # => "cba"
s2.respond_to?(:swoosh!) # => false

instance_eval을 사용해서 s1의 Singleton Method 를 정의했다! 


그리고 이번에는 Class Macro 를 어디서 사용하냐에따라 함수의 정의가 달라지는걸 확인해 보자.

MyClass 의 함수로 정의할땐 아래와 같다. 

class MyClass
  attr_accessor :a
end

obj = MyClass.new
obj.a = 2
obj.a # => 2

이번에는 Class 클래스에서 Class Macro 인 attr_accessor 를 사용해보자.

class MyClass; end
class Class
  attr_accessor :b
end

MyClass.b = 42
MyClass.b # => 42

이 방법을 사용하면 MyClass가 b 라는 attribute를 갖고 있지만 문제는 모든 클래스가 b를 공유한다. 

그래서 이번에는 MyClass 에서만 사용하는 Singleton Method와 Class Attribute를 정의할 수 있다.

class MyClass
  class << self
    attr_accessor :c
  end
end

MyClass.c = 'It works!'
MyClass.c

이는실제로 아래와 같이정의한것과 동일하다.

def MyClass.c=(value)
  @c = value
end
def MyClass.c
  @c
end

이제 지금까지 정의한 정보를 다이어그램으로 보자.



Extends

모듈의 Method를 Singleton Class 에 include 하는방법을 알아보자

module MyModule
  def self.my_method; 'hello'; end
end

class MyClass
  include MyModule
end

MyClass.my_method # NoMethodError!

MyModule 의 Singleton Method를 정의할경우 MyClass의 include 를 하고 함수를 호출하면 오류가난다.

이는 이렇게 수정해야 한다.

module MyModule
➤ def my_method; 'hello'; end
end

class MyClass
➤ class << self
➤   include MyModule
➤ end
end

MyClass.my_method # => "hello"

그리고 이렇게 Singleton Method를 정의하는건 당연히 그냥 객체에도 가능하다. 

module MyModule
  def my_method; 'hello'; end
end

obj = Object.new
class << obj
  include MyModule
end

obj.my_method # => "hello"
obj.singleton_methods # => [:my_method]

Ruby에서는 이를지원해주는 함수인 extends 가 있다.

위의 예제는 이렇게 동일하게 동작한다.

module MyModule
  def my_method; 'hello'; end
end

obj = Object.new
obj.extend MyModule
obj.my_method # => "hello"

class MyClass
  extend MyModule
end
MyClass.my_method # => "hello"

Object#extend 는 Singleton Class 에 모듈을 include 하는 방법이다. 


Method Wrapper( Around Aliases )

Module#alias_method: 를 이용하면 기존의 함수를 다른이름으로도 호출 할 수 있다.

class MyClass
  def my_method; 'my_method()'; end
  alias_method :m, :my_method
end

obj = MyClass.new
obj.my_method # => "my_method()"
obj.m # => "my_method()"

그리고 이렇게 alias 된 함수는 또다시 alias에 사용될 수 있다.

class MyClass
  alias_method :m2, :m
end

obj.m2 # => "my_method()"

그리고 alias 를 이용하면 실제 함수의 이름을 다른 이름의 함수로 바꾸고 실제함수였던 녀석을 바꿀수있다.

class String
  alias_method :real_length, :length
  def length
    real_length > 5 ? 'long' : 'short'
  end
end

"War and Peace".length # => "long"
"War and Peace".real_length # => 13

String#length 는 더이상 문자열의 길이를 반환하지 않는다.

이를 이용하면 다음과 같은 작업도 가능하다.

+ 연산을 했을때 기존의 값에 +1 을 더해주도록 바꿀수도 있다.


class Fixnum
  alias_method :old_plus, :+
  def +(value)
    self.old_plus(value).old_plus(1)
  end
end

1 + 1 # => 3


오늘 포스팅을 통해 다음과 같은 것들을 알아봤다.

  1. class_eval : 객체만 바꾸는게 아니라 객체의 클래스도 변경하고 싶다면 class_eval을 사용하자
  2. Singleton Class : 모든 객체들은 다들 숨겨진 자신만의 singleton class를 갖고 있다. 
  3. Method Lookup : singleton class 도 동일하게 ancestor chain을 탄다.
  4. extends : 모듈을 singleton class에 추가하고 싶을때는 extends를 사용한다.
  5. around aliases : 기존의 함수명과 별개로 다른 함수명을 사용하고 싶을때 사용 가능하며 기존의함수를 wrapping하는 트릭도 가능하다. 

오늘은 여기까지!


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