Flying Mate

Rails Caching

소소하지 않은 일상 2008/02/24 16:42 by FlyingMate

Rails Caching

레일스의 캐싱은 (다른 언어의 프레임워크들은 어떻게 하는지 모르지만) 굉장히 친절해서, 컨트롤러에 코드 몇 줄 추가하는 것 만으로도 상당한 수준의 캐싱을 구현할 수 있다. 레일스 캐싱에 대해 정리된 메뉴얼(레일스 관련 서적, 온라인 튜토리얼)은 대게 사용법만을 다루기 때문에, 캐싱 메커니즘을 이해하는 데는 소스코드를 함께 보는 것이 도움이 된다. 소스코드는 해석이 아닌 진실 그 자체이니깐.


캐싱은 액션 컨트롤러(actionpack/action_controller/caching.rb)에 구현되어 있다. 우선 기본적으로 Page Caching, Action Caching, Fragment Caching 세 가지 캐싱이 가능하고 각각은 Caching모듈의 하위 모듈 Pages, Actions, Fragments로 분리되어 있다.

action_controller.rb에서 load path 배열을 담는 전역변수 $:에 action_controller 디렉토리를 추가(unshift)하고, ActiveSupport의 라이브러리들을 사용할 수 있는 상태로 만든다($:.unshift 또는 gem 'active_support'). 그 다음 확장 모듈이 담긴 파일을 먼저 require하고 ActionController.Base.class_eval 블럭에서 그 모듈들을 include한다.

# action_controller.rb
# ...
require 'action_controller/cgi_process'

require 'action_controller/caching'
require 'action_controller/verification'
# ...

ActionController::Base.class_eval do
  # ...
  include ActionController::Cookies
  include ActionController::Caching
  include ActionController::Verification
  # ...
end

ActionController::Caching 도 그 모듈 중 하나로서 include되는데, self.included(base) 메서드 내의 base.class_eval 블럭에서 Pages, Actions, Fragments 모듈을 또 include한다. (self.included에 대해서는 저번 포스트에서 미리 다루었다) 레일스 2.0 이전에는 class_eval내에서 include를 직접 실행하지 않고 base.send(:include, Pages, ...) 이런 식으로 메서드 심벌과 모듈 상수명을 send에 넘겼었다.

# action_controller/caching.rb
# ...
  module Caching
    def self.included(base) #:nodoc:
      base.class_eval do
        include Pages, Actions, Fragments

        if defined? ActiveRecord
          include Sweeping, SqlCache
        end

        @@perform_caching = true
        cattr_accessor :perform_caching
      end
    end
    (..중략..)
  end


프레임워크가 성숙해져 간다는 느낌이 드는 부분은 새로 추가된 if문에서 발견할 수 있는데 ActiveRecord가 정의되어 있어야지만 Sweeping과 SqlCache를 include한다. Sweeping모듈 내의 Sweeper 클래스는 ActiveRecord의 Observer를 상속하는데, 기존에는 ActiveRecord가 당연히 require/include되었을 것으로 가정하고 동작했지만 이제는 DataMapper 등의 다른 모듈을 사용하거나 DB관련 모듈을 사용하지 않는 경우를 고려한 if defined?(ActiveRecord) 구문이 추가되었다.

SqlCache는 새롭게 추가된 캐싱 모듈인데, ActionController::Base의 perform_action 메서드를 ActiveRecord::Base.cache 블럭으로 감싼다. perform_action은 이름에서 알 수 있듯이 레일스 애플리케이션으로 요청된 액션을 수행하는 레일스의 가장 기본적이고 중요한 메서드이다.

요청받은 액션 이름이 존재하지 않을 경우 method_missing으로 전달하는 처리도 한다. 즉, SqlCache가 레일스로 들어오는 모든 요청을 통제하면서 캐싱 처리를 한다는 의미로 볼 수 있다. SqlCache 모듈이 등장하면서 ActionController보다는 ActiveRecord측에 캐싱 관련 코드가 많이 추가되었다. 나중에 자세히 살펴봐야겠다.


Page Caching

대표적인 세 가지 Rails Caching 중 Page Caching의 성능이 가장 월등할 수밖에 없는데 한 번 렌더링된 뷰를 html로 만들어 public폴더에 담고, 해당 url로 get 요청이 있을 때 컨트롤러가 직접 응답하지 않고 만들어져 있는 html을 전송한다. 이 과정에서 상태 코드가 200인지도 체크한다. 페이지 캐싱이 레일스가 응답하는 것보다 100배나(정말?) 빠르다고 주석에 설명하고 있다.

expire_page와 cache_page, caches_page를 정의한다. 이 메서드들은 클래스 변수인 @@perform_caching가 true인지 false인지를 매번 확인하는데(return unless perform_caching), 자바스크립트에는 흔하게 보이는 형태이지만 루비에서 만큼은  DRY스럽지 않다고 느껴지는데 어쩔 수 없어보인다.

cache_page는 path를 인자로 받고, caches_page는 액션이름의 심벌을 인자로 받는데 내부적인 처리는 동일하다. (caches_page는 인자로 받은 액션 각각에 대해 cache_page를 호출할 뿐이다) html(디폴트 확장자로, 변경 가능)파일을 만드는 것(File.open)이 cache_page의 역할이고, 그 파일을 지우는 것(File.delete)이 expire_page의 역할이다.

이 동작들은 benchmark 블럭 내에서 이루어지는데, ActionController의 Benchmarking은
루비의 Benchmark를 확장해서 benchmark 메서드에 log_level과 use_silence 옵션을 추가하는 등의 구현을 담고 있다. 거의 모든 캐싱 동작들이 benchmark 블럭 내에서 이루어지기 때문에 캐싱 성능 측정이 용이하다.

caches_page는 show, create 액션 등 페이지 뷰가 발생하거나 페이지가 최초로 생성되는 액션을 컨트롤러 차원에서 선언해주면 되고, cache_page는 조건부 처리가 필요할 때 액션 내에서 호출하는 용도, 그리고 expire_page는 페이지가 변경되는 시점인 update 등의 액션 내에서 호출하면 된다.


Sweeper

expire_page를 직접 호출하지 않고 좀더 엘레강스하게 자동으로 처리하고 싶다면 sweeper(청소부)를 만들면 된다. sweeper는 ActiveRecord::Base의 자식인 model들이 서식하는 models 폴더에 놓이지만 ActionController::Caching::Sweeper를 상속하는 미운오리X끼인데, 이복 형제
(모델)들을 감시(observe)하고 있다가 그들에게 변화가 생기면(save, create, update) 적당한 after_xxx 메서드를 실행한다.

이 메서드 내에서 expire 처리(FileUtils.rm_rf 등)를 적어주면 되고 Page 캐싱 뿐만 아니라 Action, Fragment 캐싱도 잘 처리(expire_fragment)한다. 아래 코드는 블로그 애플리케이션 Typo의 BlogSweeper의 대략적인 형태이다.

class BlogSweeper < ActionController::Caching::Sweeper
  observe Category, Blog, Sidebar, User, Article # , ...
  
    # ...

  def after_save(record)
    # ...
  end

  def after_destroy(record)
    # ...
  end

    # ...
end

사용하려면 컨트롤러에서 cache_sweeper를 호출해주면 된다.

cache_sweeper :blog_sweeper


Action Caching

페이지 캐싱은 게시물이 모든 사람에게 동일하게 보여질 때만 사용할 수 있다. public폴더는 누구나 접근할 수 있기 때문에 인증이 필요하거나 사용자별로 다른 데이터가 제공되어야 하는 페이지는 페이지 캐싱을 이용할 수 없다. 대신 Action Caching이나 Fragment Caching을 이용해서 before_filter가 인증처리를 한 다음 캐시 파일을 사용자에게 전송해야 하며, 그 파일은 공개되지 않은 곳에 저장되어야 한다.

Action Caching과 Fragment Caching은 기술적으로 동일하며 사용 방식에만 차이가 있다. ActionController::Caching::Actions 모듈은 ActionCacheFilter와 ActionCachePath라는 하위 클래스를 가지고 있고, 모듈 내에서 두 클래스를 사용한다.  ActionCacheFilter.new가 어떻게 사용되는지 보자.


module ClassMethods
  def caches_action(*actions)
    return unless perform_caching
    around_filter(ActionCacheFilter.new(*actions))
  end
end

컨트롤러에서 caches_action을 선언하면, 인자로 받은 액션의 목록을 ActionCacheFilter의 생성자로 넘긴다. ActionCacheFilter 클래스는 이렇게 생겼다.

class ActionCacheFilter
  def initialize(*actions, &block)
    @options = actions.extract_options!
    @actions = Set.new actions
  end

  def before(controller)
    return unless /
      @actions.include?(controller.action_name.intern)
    # ..
  end
 
  def after(controller)
    return unless /
      @actions.include?(controller.action_name.intern)
    # ..
  end

    # ..
end

around_filter는 개념적으로 before_filter와 after_filter를 합친 것이다. around_filter의 인자로 필터 메서드 이름(심벌), 블럭, 필터 클래스의 이름, 필터 클래스의 인스턴스 등 총 4가지를 넘길 수 있다. 필터 메서드의 이름을 넘기는 경우, 해당 필터 메서드는 yield나 block.call을 포함해야 하며 컨트롤러 내의 액션들은 그 안(yield, block.call)에서 처리된다.

around_filter에 블럭을 인자로 넘길 경우 { |controller, action| .. } 이렇게 블럭 지역변수로 controller와 action을 사용해야 한다. 필터 클래스를 사용하는 경우 해당 클래스 내에 after와 before 두 개의 메서드를 정의하거나, 그 대신 filter 메서드 하나를 정의하면 되는데, 필터 클래스의 이름을 넘기는 경우는 이들 메서드를 클래스 메서드로 정의해야 하고, 필터 클래스의 인스턴스를 넘기는 경우는 이들 메서드를 인스턴스 메서드로 정의해야 한다는 차이점이 있다.

ActionCacheFilter 내에서는 before과 after를 정의하고 있으며 @actions에 필터링할 액션 목록을 따로 담아 관리한다. 즉 filter를 컨트롤러에서 사용할 때 :except나 :only 옵션이 했던 일을 @actions 변수가 대신 관리하면서 before과 after 메서드 내에서 @after.include?를 통해 캐싱할 액션들을 필터링하는 것이다.

실질적인 Action 캐싱 과정은 이 ActionCacheFilter 내에서 일어나는데, 자체 캐싱 메커니즘을 따로 갖고 있지 않고 Fragment 캐싱을 사용한다. 즉 Fragments 모듈의 read_fragment와 write_fregment를 이용해 Action 캐싱을 하고 만료를 시킨다. ActionCachePath는 캐싱 파일의 이름과 경로를 관리하는데, 실제 애플리케이션의 url(url_for 헬퍼를 이용해)을 기준으로 그것을 결정한다.

Typo의 경우 플러그인을 통해 추가된 caches_action_with_params 메서드를 사용하고, 이 메서드는 url에 parameter가 붙어있는 페이지까지 캐싱을 한다. #{RAILS_ROOT}/tmp/cache 폴더에 #{hostname}/#{controller.controller_name}/#{controller.action_name}/#{param_string}.cache의 형태로 만들어지는데, 파일을 열어보면 페이지 캐시된 html 파일과 형태가 유사하다.


Fragment Caching

Action Caching의 실질적인 구현은 Fragment Caching을 이용한다. Page Caching은 html 파일을 만들어 public 폴더에 담아두기만 하면 되었다. 즉 파일시스템을 이용하기 때문에 고민할 부분이 많지 않았던 반면 Fragment Caching은 저장 방식에 대해 기본적으로 4가지 선택사항(FileStore, MemoryStore, DRbStore, MemCacheStore)이 주어지고 커스터마이징된 cache_store를 사용할 수도 있다.

MemoryStore는 가장 간단하게 인스턴스 변수(Hash)에 캐시를 저장한다. 인스턴스 변수는 메모리 상에 존재하니 그것을 이용하는 것이다. ActionController::Base의 클래스 변수인 @@allow_concurrency(동시성 허용)가 true일 경우, 약간의 추가작업을 해주는데, 캐시의 읽기/쓰기/삭제 과정을 Mutex 인스턴스(@mutex = Mutex.new)의 synchronize 블럭으로 감싼다. 이 부분을 모듈화해서 ThreadSafety(쓰레드 안전성) 모듈에 담고 MemoryStore.module_eval { include ThreadSafety } 형태로 추가한다.

Mutex는 루비의 Thread 라이브러리에 정의되어 있고, 어느 특정 쓰레드에 자원 사용의 독점권을 부여하는 데 쓰인다. 즉 여러 개의 요청이 동시에 들어와 MemoryStore가 사용하는 인스턴스 변수 내에서 데이터가 섞이거나 잘못 할당되는 것을 방지하기 위해, 요청들을 일단 줄세우고 하나의 요청 쓰레드가 끝나야(lock) 다음 쓰레드를 실행시키는(unlock) 등의 관리를 해준다.

  def synchronize
lock
begin
yield
ensure
unlock
end
end

DRbStore와 MemCacheStore는 이런 MemoryStore를 상속하기 때문에 쓰레드 안전성을 보장받는다고 할 수 있다. DRb에 대해서는 ChadFowler의 Intro to DRb가 좋은 출발점. 다음은 Fragments 모듈 내의 DRbStore 클래스 구현부분이다.

class DRbStore < MemoryStore
  attr_reader :address

  def initialize(address = 'druby://localhost:9192')
    super()
    @address = address
    @data = DRbObject.new(nil, address)
  end
end

DRb는 클라이언트와 서버 둘 다 실행해야 하고, 일단 실행된 후에는 대칭적으로 동작하는데, caching.rb 내에서는 클라이언트 역할만 정의하고 있으므로 DRb 서버측도 구동(DRb.start_service)해줘야 할 것으로 보인다. 이 부분은 좀더 공부해야겠다. 아마존의 simple_db가 이런 개념(객체 공유하기)이 아닐까 추측해보고 있다.

MemCacheStore는 그 유명한 Memcached 메커니즘을 사용하며, 레일스 내에 포함되어 있지 않기 때문에 gem이나 library 설치가 필요하고 이를 require한다. MemCache 부분도 아직 경험해볼 일이 없어서 깊이 이해하지 못했다. DRb와 방식은 유사(루비 객체 주고받기)하되 여러 장소에 분산시켜 공유하는 모델인 듯 하다. 시간날 때 memcache-client gem의 memcache.rb와 루비의 drb.rb를 꼭 뜯어봐야겠다고 벼르고 있다.


Caching in Java

자바개발자를 위한 레일스에서는 캐싱에 대해, 자바에서는 모델을 캐싱하고 레일스에서는 뷰를 캐싱하며, 자바에는 뷰를 캐싱하는 데 널리 쓰는 메커니즘이 딱히 존재하지 않는다고 언급하고 있다. 새로 추가된 SqlCache에 의해 레일스의 모델 캐싱 부분이 좀더 강화되지 않았나 싶다. 그리고 자바에서는 뷰를 전혀 캐싱하지 않는지, 만약 자바에서도 뷰 캐싱이 있다면 어떤 방식으로 처리하는지 궁금하다.

TRACKBACK :: http://flyingmate.net/trackback/41

댓글을 달아 주세요

1  ... 6 7 8 9 10 11 12 13 14  ... 39 
Flying Mate

공지사항

카테고리

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

달력

«   2008/07   »
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31    

믹시