2009년도에 작성된 내용이지만, 루비와 루비온레일스에서 `delegation(저자譯:기능위임)` 개념을 이해하는데 훌륭한 글이라고 판단되는 두개의 블로그를 공부하고, 나중을 위해서 정리해 둔다.
루비온레일스에서, 하나의 객체가 임의의 behavior(여기서는 속성에 대한 말로 액션 정도로 생각하면 되겠다)를 외부에서 호출할 경우, 실제로는 해당 behavior를 실행시 발생하는 모든 책임을 associate 모델 객체(레일스에서의 관계 모델)로 전가하는 개발방식을 `Delegation Design Pattern(저자譯:기능위임 코딩법)이라고 설명한다.
대개는 `method_missing` 메소드를 이용하여, 정의되어 있지 않은 메소드에 대한 호출을 중간에서 가로채어 임의의 처리루틴(handler)으로 포워딩해 주는 방법으로 해결하지만, 이러한 방법이 반드시 좋을 것만은 아니라고 한다. 루비에서는 이러한 기능위임 코딩법을 구현하기 위한 더 좋은 방법을 제공하고 있다.
루비표준라이브러리에는 이러한 기능위임 코딩법을 지원하기 위한 `Delegate module`이라는 것이 있는데, 일반적인 방법보다 더 복잡해서 기피하는 경향이 있는 것 같다. Simone Carletti의 경우 이 방법을 절대 사용하지 않는다고 한다.
레일스 프로젝트를 개발할 때 `ActiveSupport`가 기본으로 포함되어 있어 `Module#delegate` 확장모듈을 이용하면 이러한 기능위임 코딩법을 보다 명료하고 쉽게 구현할 수 있게 해 준다. 바로 이 delegate 모듈을 임의의 클래스나 모듈에서 사용하면 특정 메소드를 associate 객체로 기능위임(delegate)할 수 있게 되는 것이다.
`User` 모델에 `belongs_to` 로 관계선언을 하는 `Post` 모델에 대한 예를 들어 본다.
class Post belongs_to :user end class User has_many :posts end
이때, 특정 post에 연결된 user의 name을 반환하기 위해 `post.name`을 호출하고자 한다면 어떻게 될까. 보통는 아래와 같이, `name`이라는 메소드를 만들게 될 것이다.
class Post
belongs_to :user
def name
# let's use try to bypass nil-check
user.try(:name)
end
end
글의 주제에 벗어나는 이야기지만, 위의 코드에서 6번 라인에는 `try`라는 메소드를 사용했다. `try` 메소드는 파라메터로 넘겨진 인수에 대해서 nil 값이 넘어 오더라도 에러를 발생하지 않도록 해 주는 편리한 놈이다. 이 메소드를 사용하지 않을 경우에는 user.name unless user.name.nil? 와 같이 코딩해 주어야 에러를 모면하게 될 것이다.
자, 이때 동일한 코드를 아래와 같이 `delegate` 메소드를 사용하여 구현할 수 있다.
class Post belongs_to :user delegate :name, :to => :user, :allow_nil => true end
이 `delegate` 메소드는 어떠한 문맥에서도 사용할 수 있다고 한다. 즉, 위에서 예를 든 ActiveRecord 모델 이외에서도 사용할 수 있는 말이다. 아래에서와 같이 `queue` 래퍼클래스(QueueManager)에서 특정 메소드가 내부 queue를 실행하도록 기능위임할 수 있다.
class QueueManager
attr_accessor :queue
# Delegates some methods to the internal queue
delegate :size, :clear, :to => :queue
def intialize
self.queue = []
end
end
m = QueueManager.new
m.size
# => 0
m.clear
# => []
위임할 메소드를 심볼형태로 인스턴스 변수, 클래스 변수, 또는 상수에 넘겨 줄 수 있다. 이때 최소한 하나의 메소드와 `:to` 옵션이 필요하게 된다.
`delegate` 메소드는 사용할 수 있는 옵션이 더 있다. `:prefix` 옵션을 true로 설정하게 되면 위임될 객체의 이름을 delegate 메소드 이름앞에 추가해 주게 된다. 물론, 객체이름 대신에 임의의 다른 이름을 지정할 수도 있다.
class Post belongs_to :user delegate :name, :to => :user, :prefix => true # post.user_name delegate :name, :to => :user, :prefix => "author" # post.author_name end
`:allow_nil` 옵션을 사용하면 nil 값을 반환하는 객체에 대해서도 해당 delegate 메소드를 호출할 수 있게 해 준다. 이 때는 delegate 메소드를 호출시에 nil 값을 반환하게 된다. 디폴트상태에서는 `NoMethodError` 예외를 발생시키게 된다.
class Post belongs_to :user delegate :name, :to => :user, :prefix => true end Post.new.user_name # raise NoMethodError class Post belongs_to :user delegate :name, :to => :user, :prefix => true, :allow_nil => true end Post.new.user_name # => nil
사실 `:to`옵션은 필수이므로 옵션이라 할 수 없다.
ActiveSupport의 `delegate` 확정모듈에 대해 자세하게 기술되어 있지만, 레일스 문서에 빠져 있어서 소스코드를 직접찾아 보면 그만한 가치가 있을 것이다.
참고원문 : Simone Carletti's Blog
댓글 없음:
댓글 쓰기