Flying Mate

'객체'에 해당되는 글 1건

  1. 2008/02/23 레일스의 클래스 확장 방식

레일스 캐싱에 대한 글을 쓰려다가 글이 자꾸 삼천포로 빠지길래 아예 삼천포를 주제로 다시 글을 정리하고 있다. 이 다음 글로 레일스 캐싱을 적어봐야 겠다. 아래 코드는 레일스 캐싱의 구현부분이지만 이 글은 캐싱이 아니라 레일스의('루비의'가 아닌) 클래스 확장 방식을 다루려고 한다. 레일스에서 Action Caching을 구현하는 Actions 모듈은 아래와 같이 시작한다.

module Actions
  def self.included(base)
    base.extend(ClassMethods)
    base.class_eval do
      attr_accessor :rendered_action_cache, /
        :action_cache_path
      alias_method_chain :protected_instance_variables, /
        :action_caching
    end
  end
     
  module ClassMethods
    def caches_action(*actions)
      return unless perform_caching
      around_filter(ActionCacheFilter.new(*actions))
    end      
  end

    (..중략..)
end

self.included(base), base.extend, base.class_eval은 레일스의 모듈 내에서 흔하게 볼 수 있다. self.included(base)를 통해, 액션 컨트롤러(base)가 현재 모듈을 include했을 때의 초기화 부분을 정의하고 있다. base라는 이름의 인자로 넘겨받은 ActionController::Base에 ClassMethods에 담긴 클래스 메서드들을 포함시키고(extends), 인스턴스 변수에 대한 접근자(accessor)와 alias_method_chain 선언을 컨트롤러 내에 추가한다(class_eval).

alias_method_chain는 논란이 많은 메서드인데, 레일스 내에서 메서드 확장을 위해 빈번하게 사용된다. 기존 메서드를 override할 때, override를 정의하는 구현부 내에서 override하기 이전의 기능을 사용하려 할 때 호출한다. 기존에는 두 줄의 alias_method를 통해 아래와 같이 선언을 했다.

alias_method :old_good_method, :good_method
alias_method :good_method, :new_good_method

good_method라는 이름이었던 이전의 메서드(superclass의 메서드 또는 class_eval 등으로 재정의되기 전의 메서드)에 임시적으로 다른 이름을 부여하고(예를 들어 old 접두어 같은), override를 정의할 때의 메서드 이름 역시 새로운 이름을 부여해서(역시 예를 들면 new 접두어 같은) 메서드의 확장을 좀더 명시적으로 드러내는 것이다.

이것을 하나의 메서드로 통합한 것이 alias_method_chain인데, 이 메서드는 active_support/core_ext/module/aliasing.rb에 구현되어 있다. 위에 있는 두 줄의 alias_method를 한 줄의 alias_method_chain으로 선언한 예는 다음과 같다.

alias_method_chain :good_method, :new_feature

이 선언은 액티브 서포트의 메타 프로그래밍에 의해 다음과 같이 변환된다.

alias_method :good_method_without_new_feature, :good_method
alias_method :good_method, :good_method_with_new_feature

이렇게 선언한 후, 기존의 good_method에 override할 내용을 good_method_with_new_feature 메서드에 구현한다. good_method_without_new_feature를 그 내부에서 사용할 수 있다.

def good_method_with_new_feature
  good_method_without_new_feature + "new feature"
end

alias_method_chain 구현부의 변천사를 살펴보면, 처음(active_support-1.3.1)에는 스키니했지만 점차 살을 붙여가고 있다. 1.4대에서
?, !, =로 끝나는 메서드와 블럭 처리가 추가되었고, 2.0.2인 현재 메서드 접근 제어(public/private/protected)까지 처리하고 있다.

def alias_method_chain(target, feature)
alias_method "#{target}_without_#{feature}", target
alias_method target, "#{target}_with_#{feature}"
end

caching을 비롯해 레일스의 라이브러리들 상당수가 class_eval을 통해 base 클래스를 다시 열고 그 안에 alias_method_chain을 두거나 추가적인 메서드를 정의하는 형태로, 또 클래스 메서드는 ClassMethods라는 모듈에 따로 모아 extends하는 형태로 구현되어 있다.

이를 통해 기존의 클래스를 변형하지 않고 모듈화된 라이브러리의 변수와 메서드를 덮어 씌우는(wrapping) 형태로 기능을 확장하는데, 여기서 Decorator 패턴과 AOP(Aspect Oriented Programming)의 특징을 발견할 수 있다고 한다.

자바에서의 Decorator패턴은 Wrapping할 대상 클래스의 superclass인 추상 클래스를 Decorator가 상속해서 메서드를 재정의하되, 그 정의 내에서 기존의 메서드를 호출하는 부분이 중요한데, 레일스에서 class_eval과 alias_method_chain이 했던 역할(기존에는 두 줄의 alias_method가 했던)이 이것과 유사하다고 볼 수 있다.

deepblue님이 소개해주신 The Rails Way에서도 alias_method_chain을 패턴 차원에서 다루고 있는데, 정적 언어에 주어진 제약들 때문에 이런 저런 재미있는 방법(패턴)으로 해결했던 과제들을 동적 언어에서는 너무 싱겁게 풀어버려서 패턴이라는 고상한 단어를 붙이기가 무색해져 버렸다. 두 세 줄 짜리 코드에 패턴이 웬 말인가.

기존의 코딩 습관들에서 좋은 것들을 추출해 패턴으로 만들고, 상황에 따라 패턴을 선택해 코드로 구체화하는 자바와는 반대로 루비는 이미 만들어진 코드에서 패턴의 흔적(?)들을 추출해낼 수 있다. 꿈보다 해몽. 어쩌면 리팩토링이나 코딩 스타일 가이드 조차도 다른 언어에 비해 루비에서 만큼은 큰 의미가 없을지도 모르겠다는 생각이 든다. 이건 좀더 겪어봐야겠다. 레일스의 개발자들 역시 패턴을 고려해 설계하겠지만 기존의 정적 언어에서는 상식이었던 개념들로부터 독립적으로 사고해야 할 것 같다.

위의 Action Caching의 코드로 돌아와서, class_eval에서 정의하고 있는 alias_method_chain은 ActionController의 Base클래스에 정의되어 있는 protected_instance_variables를 확장한다. 기존의 메서드는 특별한 인스턴스 변수 이름들의 목록을 배열로 반환하는데, 재정의된 메서드는 이 배열에 변수 이름 하나를 추가한다.

이 모듈이 include되면 메서드가 이전과 조금 다르게 동작하게 되는 것이다. 사실 이 부분(protected_instance_variables_with/without_xxx)이 크게 중요한 것은 아니지만, alias_method_chain의 동작과 사용예를 잘 이해해야 프레임워크의 다른 부분을 이해하는 데 속도를 더 높일 수 있다. alias_method_chain의 정체가 무엇이고 왜 쓰였는지 이해하는 데 나도 좀 헤맸다.

TRACKBACK :: http://flyingmate.net/trackback/40 관련글 쓰기

댓글을 달아 주세요

1 
Flying Mate

공지사항

카테고리

분류 전체보기 (45)
소소하지 않은 일상 (45)

믹시