본문 바로가기
카테고리 없음

쉽게 위임하기2

by 경자꿈사 2024. 10. 29.

'쉽게 위임하기' 첫번 째 글에서는 단순한 위임을 사용하여 클래스의 기능을 확장시키려고 할 때 Forwardable 모듈을 사용하면 쉽고 간단하게 위임을 처리할 수 있다는 걸 알게 되었다.
아래 예제로 사용했던 Calculator 클래스의 코드를 다시 가져와 봤다.
Calculator 클래스는 Forwardable 모듈이 제공하는 def_delegator 메서드를 사용하여 해당 객체에 대한 fib 메서드 호출을 @fib 객체에 위임하였다.
def_delegator 메서드에 의해 Calculator 클래스에 fib 인스턴스 메서드가 정의(또는 재정의)되는데, fib 메서드는 단순히 @fib 객체의 get 메서드를 호출한 결과를 그대로 반환하는 일을 수행한다.

class Calculator
  extend Forwardable
  def_delegator :@fib, :get, :fib

  def initialize(fib)
    @fib = fib
  end

  def add(a, b)
    a + b
  end
  
  def sub(a, b)
    a - b
  end  
  
  def mul(a, b)
    a * b
  end  
  
  def div(a, b)
    a / b.to_f
  end  
end

 

그러면 def_delegator와 같은 기능을 하는 메서드를 직접 만들어 보자.
우선 이전 '메서드 기능 확장하기' 글에서 만들었던 Memoizable 모듈의 코드를 다시 한번 살펴보자.
아래 Memoizable 모듈의 memoize 메서드의 코드를 보면 내부에서 define_method를 호출하고 있는데, define_method 메서드는 인수로 메서드 이름을 받고 블록으로 메서드 내용을 받아 메서드를 정의(또는 재정의)해준다.
결국, Memoizable 모듈을 인클루드한 클래스가 memoize 메서드를 호출하게 되면 해당 클래스의 인스턴스 메서드가 정의된다.

module Memoizable
  def memoize(method_name)
    org_method = "#{method_name}__memoize"
    alias_method org_method, method_name
    memo = {}
    define_method(method_name) do |*args|
      memo[args] ||= send(org_method, *args)
    end
  end
end

 

Memoizable 모듈의 코드를 참고하여 이와 비슷하게 아래처럼 MyForwardable 모듈을 만들고 def_delegator 메서드를 작성해 보자.
파라미터는 세 개가 필요한데, 첫 번째 dg_iv_name는 위임 대상 객체를 참조하고 있는 인스턴스 변수의 이름이고, 두 번째 dg_method_name는 그 대상 객체에 대해 호출할 메서드 이름이다.
마지막 세 번째 method_name은 def_delegator 메서드를 호출하는 클래스가 실제 위임하려고 하는 메서드의 이름인데, 기본값으로 두 번째 파라미터의 값을 지정했다.
그래서 세 번째 파라미터를 생략하고 호출하면 위임 대상 객체의 메서드와 같은 이름의 메서드가 정의된다.

define_method에 전달하는 블록을 보면 먼저 instance_variable_get 메서드를 사용하여 dg_iv_name에 할당된 인스턴스 변수의 이름으로 그 값(위임 대상 객체)을 가져오고 
그 객체에 대해 dg_method_name에 할당된 이름의 메서드를 send 메서드를 사용하여 호출하고 있다.
파라미터 이름에서 'dg'는 'delegation'을 'iv'는 'instance variable'을 뜻한다.

module MyForwardable
  def def_delegator(dg_iv_name, dg_method_name, method_name = dg_method_name)
    define_method(method_name) do |*args|
      instance_variable_get(dg_iv_name).send(dg_method_name, *args)
    end
  end
end

 

MyForwardable 모듈을 테스트해 보기 위해 먼저 MyForwardable 모듈의 코드를 D:/blog/ruby/delegation 폴더 아래  my_forwardable.rb 파일에 저장하자.
그리고 해당 폴더에서 irb를 실행한 후 아래 그림처럼 코드를 입력해 보고 결과를 확인해 보자.

Foo 클래스에서 extend로 MyForwardable 모듈을 인클루드했는데, 그러면 Foo 클래스는 MyForwardable 모듈에서 정의한 def_delegator 메서드를 사용할 수 있게 된다.
(인클루드를 통한 믹스인과 관련하여 좀 더 자세히 알고 싶으면 '클래스와 모듈' 글을 참고하길 바란다)
그리고 def_delegator 메서드를 사용하여 Foo 클래스의 객체에 대해 fib 메서드가 호출될 경우 @fib 인스턴스 변수가 참조하는 객체에 위임(get 메서드를 호출)하도록 했다.
이 def_delegator 메서드 호출로 인해 Foo 클래스에는 fib 라는 인스턴스 메서드가 새롭게 정의된다.
만약, def_delegator 메서드 호출 시 마지막 인수인 :fib를 생략했다면, Foo 클래스에 (@fib 객체에 대해 호출할 메서드와) 같은 이름인 'get' 메서드가 정의되었을 것이다.

그러면 이번에는 한 번에 여러 메서드에 대해 위임 처리를 할 수 있는 def_delegators 메서드를 만들어 보자.
아래 def_delegators 메서드의 코드를 보면 가변 인수를 사용하여, 위임 대상 메서드를 원하는 만큼 인수로 넘길 수 있도록 했다.
그리고 each 메서드를 사용한 블록 반복 호출을 통해 실제 위임 처리는 블록 안에서 def_delegator 메서드를 호출하도록 하였다.

module MyForwardable
  def def_delegator(dg_iv_name, dg_method_name, method_name = dg_method_name)
    define_method(method_name) do |*args|
      instance_variable_get(dg_iv_name).send(dg_method_name, *args)
    end
  end
  
  def def_delegators(dg_iv_name, *dg_method_names)
    dg_method_names.each do |dg_method_name|    
      def_delegator(dg_iv_name, dg_method_name)
    end
  end  
end

 

def_delegators 메서드에 대해 아래 그림처럼 테스트를 진행해 보자.
이전 글에서 Forwardable 모듈의 def_delegators 메서드를 테스트 했을 때와 거의 동일한 코드이다.
결과를 보면 Foo 클래스의 객체에 대해 aaa, bbb, ccc 세 메서드를 호출할 수 있게 된 것을 볼 수 있다.

Forwardable 모듈의 def_delegator 메서드를 사용하면 이름을 다르게 지정할 수 있지만 한 번에 하나의 메서드에 대해서만 위임 처리를 할 수 있고, 
def_delegators 메서드를 사용하면 한 번에 여러 메서드에 대해 위임 처리를 할 수 있지만 이름을 다르게 지정할 수가 없다.
끝으로 우리가 만든 MyForwardable 모듈의 def_delegators 메서드에 이름을 다르게 지정할 수 있는 기능을 추가해 보자.
어떻게 하면 대상 메서드의 이름을 여러 개 받으면서도 각각에 대해 따로 지정할 이름을 함께 받을 수 있을까? 
여러 가지 방법이 있겠지만, 가장 먼저 떠오르는 것이 해시이므로 해시를 사용해서 코드를 수정해 보자.

아래 수정한 def_delegators 메서드의 코드를 보면 기존과 달라진 것은 each 메서드에 전달하는 블록의 내용이 조금 바뀌었다.
블록 파라미터 dg_method_name에 전달된 값이 심볼 또는 문자열인지 아니면 해시인지에 따라 처리를 다르게 하고 있다.
심볼 또는 문자열이라면 기존과 동일하게 같은 이름으로 정의하도록 (세 번째 인수를 생략한 채로) def_delegator 메서드를 호출하고 있고, 
해시일 경우에는 다시 해시 객체에 대해 each 메서드를 호출하여 모든 키값 쌍에 대해 def_delegator 메서드를 호출한다.
이 때 블록에 전달되는 첫 번째 파라미터(dg_m_name)의 값이 위임 대상 객체에 대해 호출할 메서드의 이름이고 두 번째 파라미터(m_name)의 값이 위임을 하는 클래스에 정의할 메서드의 이름이다.

module MyForwardable
  def def_delegator(dg_iv_name, dg_method_name, method_name = dg_method_name)
    define_method(method_name) do |*args|
      instance_variable_get(dg_iv_name).send(dg_method_name, *args)
    end
  end
  
  def def_delegators(dg_iv_name, *dg_method_names)
    dg_method_names.each do |dg_method_name|    
      if dg_method_name.is_a?(Symbol) || dg_method_name.is_a?(String)      
        def_delegator(dg_iv_name, dg_method_name)
      elsif dg_method_name.is_a?(Hash)
        dg_method_name.each { |dg_m_name, m_name| def_delegator(dg_iv_name, dg_m_name, m_name) }
      end
    end
  end  
end

 

아래 그림의 테스트 코드를 보면 수정한 def_delegators 메서드를 어떻게 호출하는지 알 수 있다.

def_delegators 호출 코드를 보면 메서드 aaa는 같은 이름을 그대로 사용하도록 그냥 심볼을 인수로 전달했고, 메서드 bbb와 메서드 ccc에 대해서는 각각 메서드 이름을 'my_bbb'와 'my_ccc'로 정의하도록 인수를 해시로 전달했다.
이전에 설명했듯이, 해시가 메서드의 마지막 인수일 경우에는 '{' 와 '}' 를 생략할 수 있다.
젤 마지막 코드의 결과를 보면 Foo 클래스의 인스턴스 메서드 목록에 우리가 직접 지정한 메서드 이름인 'my_bbb'와  'my_ccc' 가 보인다.

See you again~~