Flying Mate

'module'에 해당되는 글 2건

  1. 2008/02/23 레일스의 클래스 확장 방식
  2. 2007/12/04 프레임워크에 대해(2): Prototype.js

레일스 캐싱에 대한 글을 쓰려다가 글이 자꾸 삼천포로 빠지길래 아예 삼천포를 주제로 다시 글을 정리하고 있다. 이 다음 글로 레일스 캐싱을 적어봐야 겠다. 아래 코드는 레일스 캐싱의 구현부분이지만 이 글은 캐싱이 아니라 레일스의('루비의'가 아닌) 클래스 확장 방식을 다루려고 한다. 레일스에서 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 관련글 쓰기

댓글을 달아 주세요


Prototype.js(이하 프로토타입)의 라인수는 1.5.0 버전에서 2500줄이었다가 1.6.0 버전에서 4200줄이 되었다. 엄청난 코드량이다. 프로토타입의 코드들은 메서드명이나 프로퍼티명을 축약하지 않고 의미를 살려 작성되었다. 덕분에 코드량이 많아보이지만 코드만 보고도 로직을 이해하는 것이 가능하다.

특별한 경우(IE의 메모리 누수나 오페라, 사파리 등의 버그 핸들링)가 아니면 주석이 없다. 경쟁 프레임워크인 jQuery(uncompressed 버전)는 변수명을 좀 축약한 대신 주석이 많은 편이다. 게다가 jQuery는 친절하게도 Packed(압축)버전을 제공하고 있고 이 버전을 장려하는 듯, 다운로드 리스트의 가장 상단(또는 좌측)에 두고 있다.

YSlow에서는 Packed나 Minify(코드 압축)를 좋은 웹페이지의 덕목으로 평가한다. 자바스크립트 파일을 압축하면 용량이 1/5 정도로 줄어드는데, 개인적으로는 코드 압축이 전송 비용의 측면에서 어느 정도 효율적일지 조금 의구심이 있다. 가령 프로토타입(1.5.0버전 2500라인)은 압축하지 않아도 70KB밖에 되지 않는데, 이 정도면 사이트 로고 이미지 정도의 크기이다. 코드 압축은 비용 효율 측면 보다는 코드 보안 측면에서 더 의미있어 보인다. 물론 Beautify가 불가능한 압축 방식을 적용했다는 가정 하에.

어쨌든 알려진 바와 같이, 프로토타입과 스크립타큘러스(Script.aculo.us)는 레일스 코어팀의 일부가 개발을 담당했다. 덕분에 RJS를 통해 레일스와 잘 통합이 되고 있고, 레일스나 루비의 철학들이 프로토타입 안에 녹아있다는 느낌을 받기도 한다. 예를 들면, 루비에서 Enumerable 모듈을 통해 객체를 배열처럼 다룰 수 있는 다양한 메서드를 제공하듯, 프로토타입에서도 Enumerable 객체를 생성해 유용한 메서드들을 정의하고 이를 Array, Hash 등에 Object.extend(객체 확장)시킨다.

Ajax의 대표적인 프레임워크로 인식되는 것과는 달리, 사실 프로토타입은 Ajax와 관련된 코드가 많지 않다. Ajax객체와 그 하위 객체들을 정의하는 코드는 750라인부터 1060라인까지 총 300라인 정도이다. (1.6.0에서는 360라인 정도) 나머지 라인은 대게 유틸리티 함수 정의 조금, 그리고 대부분은 객체들의 메서드 정의이다.

기존 자바스크립트 객체인 Object, Array, Number, String 등을 확장해 다양한 메서드를 포함시키고, 주기적인 실행을 처리해주는 PeriodicalExecuter, html문서 내의 엘리먼트를 쉽게 찾아주는 Selector, 특정 패턴을 문자열로 대치시켜주는 Template, 범위의 시작지점과 끝지점을 인자로 받아 Enumerable을 만들어주는 ObjectRange, 폼과 엘리먼트를 관찰해 이벤트 발생시 콜백을 호출해주는 TimedObserver와 EventObserver 등을 정의하고 있다.

Object.extend 메서드는 프로토타입에서 정의한 객체 확장 방식인데 OOP의 상속과 유사한 기능을 하지만 자유도가 더 높고, 구현은 아주 심플하다. 인자로 받은 두 개의 객체 중 두 번째 객체의 프로퍼티 값들을 첫 번째 객체의 프로퍼티에 복사해서 리턴하는 것으로 끝이다.

Object.extend = function(destination, source) {
    for (var property in source) {
       destination[property] = source[property];
    }
    return destination;
}

자바스크립트의 객체가 키/값 형태의 단순한 Dictionary이므로, 키에 대한 값들만 복사해 넣으면 상속의 개념을 구현할 수 있고, Object.extend가 그것을 해준다. 프로토타입은 내부적으로 Object.extend를 자주 사용한다. 특히 객체에 메서드를 추가할 때 사용되는데, 메서드 목록을 포함하는 {}리터럴을 Object.extend의 두 번째 인자에 두는 방식이다.

Object.extend(Number.prototype, {
    toColorPart: function() ...

그리고 프로토타입은 객체 인스턴스를 만드는 편리항 방법을 제공하기 위해 Class라는 객체에 create메서드를 정의하고 있다. 1.5.0에서의 Class.create()는 this.initialize.apply(this, arguments)를 실행하는 익명 함수를 반환하는 것이 전부(총 6라인)였는데, 1.6.0에서는 superclass와 subclass에 대한 처리가 추가되어 30라인으로 늘어났다.

Class.create()의 가장 중요한 역할은 new 키워드를 통해 객체 인스턴스를 만들 때 생성자 역할을 하는 initialize메서드가 호출되도록 초기화하는 것이다. new 키워드가 사용되는 경우는 특정 객체의 인스턴스를 생성할 경우이므로, 인스턴스 생성을 목적으로 하는 객체의 정의에만 Class.create()를 사용하고 있다.

가령 Enumerable은 객체이긴 하지만 인스턴스를 생성하기 위한 객체가 아니며, 상/하위클래스 개념 보다는 루비의 모듈(Module)처럼 믹스해서 사용하는 개념이 강하기 때문에 Class.create를 사용하지 않는다. 반대로 Template이나 Selector는 사용자가 직접 인스턴스를 생성해 활용할 수 있는 객체이므로 Class.create를 통해 객체를 정의한다. 물론 사용자 자신의 객체를 Class.create로 생성하면 자바나 C#의 클래스 정의를 어설프게나마 모사할 수 있다.

객체, 클래스, 인스턴스, 모듈 등 다양한 용어가 나왔는데, 자바스크립트에서는 객체나 인스턴스 개념은 있었지만 클래스나 모듈 등의 개념이 명확하지 않았는데, 프로토타입에 의해 이런 개념들이 좀더 명시적으로 드러났다고 볼 수 있다. 프로토타입의 다양한 객체 정의 방식이 이런 객체의 개념을 다양하게 확장했다. (클래스, 인스턴스, 모듈 모두 객체의 종류)

인스턴스 생성시 초기화되도록
(initialized) Class.create()를 설계했기에 new 키워드로 호출하면 좀 더 그럴싸한 인스턴스(로 모사된 객체)가 생겨났고, Enumerable는 클래스나 인스턴스도 아닌 모듈의 역할을 하며 다중 상속의 개념을 구현하고 있고, Ajax.Base는 Ajax.Updater나 Ajax.Request가 상속하는 기반 클래스의 역할을 한다. 고급언어에나 있던 다양한 객체 형태를 자바스크립트에서도 엇비슷하게 사용할 수 있게 되었다.

프로토타입을 사용하는 이유는 여러가지가 있겠지만, 가장 의미있는 이유는 아마도 이런 설계방식을 개발자 코드에도 자연스럽게 유도해주기 때문이 아닐까. 유틸리티 함수(특히 document.getElementById()를 대신해주는 $())도 쓸모가 많고 기존의 폼과 엘리먼트를 확장해주는 Form과 Element 객체의 메서드들도 편리하지만 그런 것들은 복사해서 붙여넣으면 되는 것이지 프로토타입 프레임워크를 통째로 사용하게 하는 핵심은 아니다. (물론 유틸리티 함수만을 사용하기 위해 프레임워크를 통째로 쓰는 사람도 있을 것이다)

그렇다면 프로토타입을 사용하는 것과, 자바스크립트 UI를 객체지향으로 구현하는 것은 어떤 관계가 있을까? 코드의 형태는 비슷해지겠지만 사실 긴밀한 연결고리는 없어보인다. 프로토타입의 객체들은 동작 구현을 위한 것이 아니라 객체 정의 자체를 위한 것인 반면, 자바스크립트 UI를 객체지향적으로 구현한다는 것은 사용자가 발생시키는 이벤트에 대한 객체간의 연쇄작용(이벤트 핸들링)을 설계한다는 측면이 강해서 둘 사이의 사고방식이 많이 다르다고 본다.

프로토타입이 저수준의 객체를 잘 정의해주기 때문에 개발자는 굳이 객체를 새로 정의하지 않고 function들의 목록만 가지고 애플리케이션을 만들 수 있다. 사용자의 행위(클릭, 입력)에 대해 function들을 실행시켜주는 것이 지금까지 자바스크립트 UI개발의 일반적인 방식이었다.

function기반으로 설계한 코드를 객체지향으로 전환하기 위해서는 사고방식도 바꿔야 한다. 사용자의 행위가 function을 유발하는 것이 아니라, 사용자가 특정 UI 객체에 이벤트를 발생시켜 그 객체와 연관된 객체의 이벤트 핸들러가 반응하는 개념이 되는 것이다. 음, 사실 이는 객체지향의 이슈라기 보다는 옵저버 패턴이나 이벤트 기반 프로그래밍의 영역인 것 같기도 하다. 상속 등의 주제 보다는 이벤트 핸들러를 정의하는 것이 코드의 대부분이니. 이 내용은 주제에서 벗어나므로 따로 글을 정리해야겠다.

프로토타입은 계속 진화하고 있다. 진화의 방식은 객체의 생성과 병합 과정이 아닐까 싶다. 1.5.0에서는 DOM에 엘리먼트를 추가할 때 사용할 수 있도록 Insertion 객체를 제공했다가 1.6.0에서는 Element 객체의 insert 메서드로 병합되었다. Insertion 객체는 정말 억지로 만들었다는 느낌이 들었었는데 아니나다를까 다른 객체의 메서드로 강등되었다. 중요한 역할을 하긴 하지만 객체로 독립하기엔 심심한 감이 있었다.

또 엘리먼트의 좌표값과 관련된 메서드를 제공하는 Position 객체도, 상당수의 메서드를 Element객체에게 양보하게 되었다. 하위호환성을 감안해 호출은 될 수 있도록 Position 객체 내에서 해당 메서드의 선언부분은 남아있지만 정의부분은 모두 Element 메서드를 호출하는 코드로 바뀌었다.

추가하고자 하는 기능 중 기존 객체의 메서드로 추가하기 애매한 것이 있으면 일단 새로운 객체로 분리해 발전시키고, 일정 기간 두고 본 후에 해당 객체가 더 이상 발전할 가능성이 안 보이고 다른 객체의 메서드로 대체가 가능하다고 판단되면 병합하는 과정을 거친다. 특정 버전의 프로토타입을 관찰하는 것도 의미가 있고, 버전 간의 차이를 관찰하는 것도 의미가 있다.

프로토타입 1.5.0 버전은 몇 번 훑어봤지만 1.6.0은 그 상당한 코드량 때문에, 약간의 변경 사항만 파악하고 있고, 구체적인 부분은 좀 더 시간을 두고 살펴봐야겠다. 저번 글에서도 이야기했지만, 개인적으로는 프레임워크를 사용하는 것 보다는 프레임워크를 뜯어보는 것을 더 중요하게 생각한다. 뜯어봐서 이해할 수 있게 되기 전 까지는 물론 사용하는 것으로 만족해야겠지만.

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

댓글을 달아 주세요

1 
Flying Mate

공지사항

카테고리

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

믹시