'루비'에 해당되는 글 3건

  1. 2008/09/27 액티브 서포트 - core_ext/array* (2)
  2. 2008/09/15 액티브 서포트 - cache*
  3. 2008/09/01 액티브 서포트 - b*.rb - buffered_logger.rb (2)

순서대로라면 callbacks.rb와 clean_logger.rb 를 봐야 할테지만 core_ext가 막 땡겨서 이거 먼저 보기로 했다.

core_ext는 이름에서도 알 수 있듯이 ruby 코어를 확장한 것이다. 루비의 클래스는 열려 있기 때문에 얼마든지 언어가 정의하고 있는 구현을 바꾸거나 추가할 수 있다.

첫번째는 정말 빠져서는 안될 자료구조, Array를 더 확장한다. 우왕국!

array.rb

ActiveSupport::CoreExtensions::Array::Access

Access 모듈은  from과 to를 구현한다. 이거, 진짜 자주 만들어내는 코든데-_-; 이런게 있었네?

이 코드에서는 저 간단한 코드보다도 명확한 주석이 눈에 확 들어온다. 아. 주석은 저렇게 다는거구나.

  1.         # Returns the tail of the array from +position+.
            #
            #   %w( a b c d ).from(0)  # => %w( a b c d )
            #   %w( a b c d ).from(2)  # => %w( c d )
            #   %w( a b c d ).from(10) # => nil
            def from(position)
              self[position..-1]
            end
           
            # Returns the beginning of the array up to +position+.
            #
            #   %w( a b c d ).to(0)  # => %w( a )
            #   %w( a b c d ).to(2)  # => %w( a b c )
            #   %w( a b c d ).to(10) # => %w( a b c d )
            def to(position)
              self[0..position]
            end

 

ActiveSupport::CoreExtensions::Array::Conversions

Conversions 모듈은 Array를 다른 것으로 바꾸는 일에 집중한다. to_sentence는 배열을 문자열로 만들어 주는데, 어떤 식으로 바꾸는 고 하니, %w(a b c)를 "a, b, and c" 로 바꾸는 식이다. 여기서 b 뒤의 ,를 빼느냐 마느냐의 옵션도 줄 수 있고, and 대신 뭘 쓸 것인가도 옵션으로 줄 수 있다. 물론 이것들은 전부 파라메터로.. 이 메서드는 코드는 좀 복잡해 보이지만, 역시 주석은 세줄로 끝난다. 주석은 이렇게 달아야 된다.-_-

  1.       # Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options:
          # * <tt>:connector</tt> - The word used to join the last element in arrays with two or more elements (default: "and")
          # * <tt>:skip_last_comma</tt> - Set to true to return "a, b and c" instead of "a, b, and c".
          def to_sentence(options = {})
            options.assert_valid_keys(:connector, :skip_last_comma)
            options.reverse_merge! :connector => 'and', :skip_last_comma => false
            options[:connector] = "#{options[:connector]} " unless options[:connector].nil? || options[:connector].strip == ''
  2.         case length
              when 0
                ""
              when 1
                self[0].to_s
              when 2
                "#{self[0]} #{options[:connector]}#{self[1]}"
              else
                "#{self[0...-1].join(', ')}#{options[:skip_last_comma] ? '' : ','} #{options[:connector]}#{self[-1]}"
            end
          end

assert_valid_keys(*valid_keys)는 core_ext/hash에서 확장된 메서드인데, hash가 가진 키 중에 valid_keys 안에 없는 키가 존재하면 ArgumentError를 발생시킨다. 즉, 여기서는 options에 :connector와 skip_last_comma 외의 다른 키가 존재하면 에러를 내는 일을 하는 코드가 첫 줄이다.

그 아래 reverse_merge! 는 똑같은 일을 merge를 이용해서 하려면 아래처럼 되어야 한다.

  1. options = {:connector => 'and', :skip_last_comma => false}.merge(options)

난 아직 이 코드가 더 눈에 익숙하게 들어오지만, options가 두 번 등장한다는 점에서 reverse_merge!가 좀 더 나을수도 있겠다고 세뇌해야겠다. dry dry ...

 

그 아래 살짝 내려가 self.included(base) 는 to_s를 재정의할 수 있도록 한다. 코드는 이런 식이다.

  1.         def self.included(base) #:nodoc:
              base.class_eval do
                alias_method :to_default_s, :to_s
                alias_method :to_s, :to_formatted_s
              end
            end

to_s를 to_default_s로 바꾸고, to_formatted_s를 to_s로 바꾼다. to_formatted_s는 바로 아래에 있다.

  1.       # Converts a collection of elements into a formatted string by calling
          # <tt>to_s</tt> on all elements and joining them:
          #
          #   Blog.find(:all).to_formatted_s # => "First PostSecond PostThird Post"
          #
          # Adding in the <tt>:db</tt> argument as the format yields a prettier
          # output:
          #
          #   Blog.find(:all).to_formatted_s(:db) # => "First Post,Second Post,Third Post"
          def to_formatted_s(format = :default)
            case format
              when :db
                if respond_to?(:empty?) && self.empty?
                  "null"
                else
                  collect { |element| element.id }.join(",")
                end
              else
                to_default_s
              end
            end

역시 주석이 최고. :db 라는 인자를 주면 , 로 join하여 돌려준다. 없으면, 그냥 to_default_s를 쓴다.

결과적으로 보면 :db 라는 format을 구현하기 위해 이렇게 한 것일텐데, super 대신 메서드 이름을 바꾸는 것이 얼마나 장점이 있는가가 조금 의문으로 다가온다. 이 코드를 읽기 않은 레일스 개발자가 Array의 to_s를 잘 못 확장하면 무한루프에 빠지는 등의 문제가 생길 수도 있기 때문이다. 흠. 더 읽다 보면 이해가 되려나?

그 아래 to_xml도 있다. to_xml도 꽤 많은 일을 하는데, 이에 대한 것은 다음에 필요할 때 읽기로 하고 넘어감. (주석도 코드도 많다)

ActiveSupport::CoreExtensions::Array::ExtractOptions

ExtractOptions 모듈은 extract_options! 메서드 하나를 정의한다. 이 메서드는 Array 마지막 아이템이 해시일 경우 그것을 리턴하고, 아니면 빈 해시를 리턴한다. 이 메서드는 레일스 전체에서 48곳에서나!!(2.1.0 현재) 사용되고 있다. 레일스 메서드 인자 관례에서 그만큼 많이 사용되고 있는 것이다.
코드의 길이대비 사용 빈도가 정말 최고가 아닐까? 다른 메서드는 어떤지 궁금하고나. ㅎㅎ

  1.         # Extracts options from a set of arguments. Removes and returns the last
            # element in the array if it's a hash, otherwise returns a blank hash.
            #
            #   def options(*args)
            #     args.extract_options!
            #   end
            #
            #   options(1, 2)           # => {}
            #   options(1, 2, :a => :b) # => {:a=>:b}
  2.         def extract_options!
              last.is_a?(::Hash) ? pop : {}
            end

 

ActiveSupport::CoreExtensions::Array::Grouping

Grouping 모듈은 in_groups_of(number, fill_with = nil, &block), split(value = nil, &block) 메서드를 제공한다.

in_groups_of 는 배열 아이템들을 number 씩 묶어 주는 것이다. 같은 개수로 파티셔닝한다고 설명하면 될 것 같다. 이렇게 하고, 혹시 빈 칸이 있다면 fill_with 로 채워준다. 기본값은 nil이므로 nil로 채워지고, false를 전달하면 빈칸을 만들지 않는다. 코드를 보자.

  1.         # Iterates over the array in groups of size +number+, padding any remaining
            # slots with +fill_with+ unless it is +false+.
            #
            #   %w(1 2 3 4 5 6 7).in_groups_of(3) {|g| p g}
            #   ["1", "2", "3"]
            #   ["4", "5", "6"]
            #   ["7", nil, nil]
            #
            #   %w(1 2 3).in_groups_of(2, '&nbsp;') {|g| p g}
            #   ["1", "2"]
            #   ["3", "&nbsp;"]
            #
            #   %w(1 2 3).in_groups_of(2, false) {|g| p g}
            #   ["1", "2"]
            #   ["3"]
            def in_groups_of(number, fill_with = nil, &block)
              if fill_with == false
                collection = self
              else
                # size % number gives how many extra we have;
                # subtracting from number gives how many to add;
                # modulo number ensures we don't add group of just fill.
                padding = (number - size % number) % number
                collection = dup.concat([fill_with] * padding)
              end

              if block_given?
                collection.each_slice(number, &block)
              else
                returning [] do |groups|
                  collection.each_slice(number) { |group| groups << group }
                end
              end
            end

사실 array의 each_slice가 유사한 일을 한다. 여기서도 보면 알 수 있겠지만 fill_with가 false일 경우는 단순히 collection의, 즉 자기 자신의 each_slice 를 실행한다. each_slice는 padding을 뺀 모든 기능을 한다. 알고보면, in_groups_of는 each_slice 가 하는 일에 padding을 넣는 것만 더 하는 것이다. 흠. dup.과 concat은 잘 안 쓰는 메서드다. 게다가 약간 로직이 들어가는 padding을 만드는 부분을 위해 세 줄이나 주석을 달았다. 흠!

다음은 split(value = nil, &block) 이다. value 혹은 block이 주어짐에 따라 문자열 대신 배열을 split한다!

코드는 inject를 활용하였다. 좀 복잡하지만 한번 보자.

  1.         # Divides the array into one or more subarrays based on a delimiting +value+
            # or the result of an optional block.
            #
            #   [1, 2, 3, 4, 5].split(3)                # => [[1, 2], [4, 5]]
            #   (1..10).to_a.split { |i| i % 3 == 0 }   # => [[1, 2], [4, 5], [7, 8], [10]]
            def split(value = nil, &block)
              block ||= Proc.new { |e| e == value }

              inject([[]]) do |results, element|
                if block.call(element)
                  results << []
                else
                  results.last << element
                end

                results
              end
            end

첫 줄에서 만든 proc 혹은 블럭으로 넘어온 객체를 block.call(element) 해서 그 값이 참이면 split을 해야 하는 것이니 [[]] 에 []을 추가. 그러면 [[], []] 가 되고, 만약 block.call이 false가 되면 .last에 << element를 한다. 아항.

ActiveSupport::CoreExtensions::Array::RandomAccess

RandomAccess 에는 rand 메서드만 덜렁 하나 있다. 배열에서 아무 값이나 하나 임의로 추출하는 것이다. 특별한 것은 없음.

느낀점-_-

주석이 하는 일은 정말 대단하다. 전에 김기사님에게 이렇게 말했던 적이 있다. "코드가 무슨 일을 하는지 알고 보니 정말 잘 읽히더라." 처음엔 주석을 잘 써라라고 가르친다(입문서). 그 다음엔, 코드가 주석을 대신하라고 가르친다(리팩토링 등). 그 후, 내가 요즘 느끼는 것은 바로.. 코드는 구현을 잘 설명할 수 있도록 하고, 구현 외의 부분은 주석으로 설명해라.이다.

말처럼 쉽지는 않겠지만 노력하다 보면 또 내공이 올라가겠지.^^ 프로그래밍 누가 쉽다고 했던가.

이 글은 스프링노트에서 작성되었습니다.

Posted by 나이누

코드를 읽어서 블로그에 포스팅을 한다 - 가 부담이 되서 코드를 안 읽고있다. -_-
그래서 그냥 코드만 읽고 대충대충 기록만 남기고 넘어가야 할 것 같다.

전에 말했던 대로 abc 순으로 읽어가고 있다.
그래서 오늘은 base64, basic_object, buffered_logger를 넘어
cache와, cache/ 안에 있는 여러 store들을 읽어보았다.

액티브서포트의 Cache 모듈은 Store 클래스를 가지고 있다. cache/ 안의 여러 파일들은 주로 Store를 재구현하는 코드들이다.

  • DRbStore < MemoryStore < Store
  • CompressedMemCacheStore < MemCacheStore < Store
  • FileStore < Store

Store

Store는 거의 여러 Store들의 공통 코드들만을 정의하고 있다. read나 write도 log 남기는 것 외에 하는 일이 없다. 이 Store를 상속받는 것들을 하나씩 보자.

MemoryStore < Store

MemoryStore는 아주 전형적인 루비 상속 구조를 취하는 클래스다. 잠시만 보자면 -_-

memory_store.rb
  1. module ActiveSupport
      module Cache
        class MemoryStore < Store
          def initialize
            @data = {}
          end

          def read(name, options = nil)
            super
            @data[name]
          end

          def write(name, value, options = nil)
            super
            @data[name] = value
          end

          def delete(name, options = nil)
            super
            @data.delete(name)
          end

          def delete_matched(matcher, options = nil)
            super
            @data.delete_if { |k,v| k =~ matcher }
          end

          def exist?(name,options = nil)
            super
            @data.has_key?(name)
          end

          def clear
            @data.clear
          end
        end
      end
    end

대략 한 줄 한 줄이 자바 코드로 1:1 매핑이 될 것 같은 느낌이 드는 코드다. (delete_if 부분만 빼고?)
전형적 루비 상속은 이렇게 하면 된다는 것을 보여준다.

drb_store.rb
  1. require 'drb'

    module ActiveSupport
      module Cache
        class DRbStore < MemoryStore #:nodoc:
          attr_reader :address

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

별거 없다. druby 주소를 지정할 수 있는 address가 추가됐다. 오우.. 심플그자체. 코드는 작은데 많은 일을 한다.

mem_cache_store.rb

아마 우리가 자주 이야기하는 그 멤캐시를 쓰는 스토어인 것 같다. MemCacheStore의 생성자를 보면 잘 알 수 있다.

  1.       def initialize(*addresses)
            addresses = addresses.flatten
            options = addresses.extract_options!
            addresses = ["localhost"] if addresses.empty?
            @addresses = addresses
            @data = MemCache.new(addresses, options)
          end

MemCache의 api를 이용해서 캐시한다. read는 단순히 MemCache 객체에 get 메세지를 보낸다.
write는 옵션에 따라 add 혹은 set 메세지를 보낸다.

  1.       def read(key, options = nil)
            super
            @data.get(key, raw?(options))
          rescue MemCache::MemCacheError => e
            logger.error("MemCacheError (#{e}): #{e.message}")
            nil
          end
  2.  
  3.   # Set key = value. Pass :unless_exist => true if you don't
      # want to update the cache if the key is already set.
      def write(key, value, options = nil)
        super
        method = options && options[:unless_exist] ? :add : :set
        response = @data.send(method, key, value, expires_in(options), raw?(options))
        response == Response::STORED
      rescue MemCache::MemCacheError => e
        logger.error("MemCacheError (#{e}): #{e.message}")
        false
      end

다른 메서드들도 대략 유사함.

compressed_mem_cache_store.rb

memcache를 사용하지만, 더 압축하고 싶을 때 쓰라고 만들어 둔 스토어다.
read와 write만 재구현했다. 쓸 때 압축하고, 읽을 때 압축을 푸는 것이다. 그뿐.

file_store.rb

마지막으로 FileStore다.

파일 스토어는 코드를 보면 알 수 있겠지만, cache_path를 지정하면 그 디렉토리를 캐시 디렉토리로 사용하고, 캐시 키 이름에 .cache를 붙여 파일로 바로 저장한다. 파일 이름을 만드는 로직은 private으로 선언돼 있다.

  1.         def real_file_path(name)
              '%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')]
            end

 

Store 다시

  cache.rb 안에 있는 Store를 다시 봤다. threadsafe! 란 메서드가 눈에 들어왔다.

  1.       def threadsafe!
            @mutex = Mutex.new
            self.class.send :include, ThreadSafety
            self
          end

이 메서드를 호출하면 self 객체의 메타클래스에 ThreadSafety 가 포함? 된다라고 하면 될려나.
여튼 쉽게 이해하기로, 여러 Store 객체 중 이 객체만 ThreadSafety 모듈을 상속받게 됐다고 생각하면 되겠다.
그럼 ThreadSafety 모듈은 무슨 일을 하는가?

  1.     module ThreadSafety #:nodoc:
          def read(key, options = nil) #:nodoc:
            @mutex.synchronize { super }
          end

          def write(key, value, options = nil) #:nodoc:
            @mutex.synchronize { super }
          end

          def delete(key, options = nil) #:nodoc:
            @mutex.synchronize { super }
          end

          def delete_matched(matcher, options = nil) #:nodoc:
            @mutex.synchronize { super }
          end
        end

미리 정해둔 @mutex 객체를 사용해 크리티컬 섹션으로의 접근을 제어한다. 그 안에서는 단순히 super를 호출하여 원래 호출되어야 하는 메서드가 자연스럽게 호출된다.

머리속이 복잡하다.
ThreadSafety에서 super를 호출하면 어떤 클래스의 메서드가 호출되는 것인가? -_-;
ThreadSafety는 Store의 하위 클래스처럼 취급되는 것 같다. 하지만 동적으로 include 되기 때문에 예를들어 MemCacheStore 객체에게 threadsafe! 메세지를 보냈다면 그 객체를 구성하는 메타클래스로 올라가는 길에 ThreadSafety 모듈이 추가된다. 객체 이름이 store 라고 했을 때

  1. store < MemCacheStore

이 상태에서, threadsafe! 메세지를 보내면

  1. store < ThreadSafety < MemCacheStore

이렇게 되는 것 같다. 아.......그렇구나.
하지만 읽기만 할 뿐, 실제로 짜는 것은 아직 수련이 부족한것 같다. 하긴 그래서 이렇게 코드를 읽고 있는 것이기도 하징.

이 글은 스프링노트에서 작성되었습니다.

Posted by 나이누

요즘은 코코아, 루비, 그리고 jquery에 꽂혀 있다.
꽂혀만 있고 공부는 잘 안한다. ...

업무 중에 시간이 좀 남아서 일과 직접적으로 연관되는 jquery와 루비가 공부 후보로 올라왔는데
낮에 jquery를 처음 써보면서 공부를 약간 했었기 땜에 남는 저녁시간은 레일스를 보기로 했다.

액티브서포트부터 시작해서 내키는 데까지 코드를 쭉 훑어보기로 했다.
눈으로만 훑으면 나중에 다 까먹으니까 간단히 정리만 해 두기로 했다.

abc 순으로 제일 위에 있는 b*.rb 들을 봤다. a로 시작하는 파일은 없었다.

lib/buffered_logger.rb

  1. module ActiveSupport
      # Inspired by the buffered logger idea by Ezra
      class BufferedLogger
        module Severity
          DEBUG   = 0
          INFO    = 1
          WARN    = 2
          ERROR   = 3
          FATAL   = 4
          UNKNOWN = 5
        end
        include Severity
  2. ...

class 안에 module 선언해서 enum처럼? 혹은 C의 define처럼? 활용한다. 오오.

 

  1. module ActiveSupport
      # Inspired by the buffered logger idea by Ezra
      class BufferedLogger
  2. ...
  3.     for severity in Severity.constants
          class_eval <<-EOT, __FILE__, __LINE__
            def #{severity.downcase}(message = nil, progname = nil, &block)
              add(#{severity}, message, progname, &block)
            end

            def #{severity.downcase}?
              #{severity} >= @level
            end
          EOT
        end
  4. ...

메서드 선언을 class_eval로 하는 코드.

logger.warn, logger.info, logger.warn? logger.info? 등의 메서드를 정의한다. 심플한 코드라 나중에 쉽게 참조하기 좋을듯.

 

  1.     # Set the auto-flush period. Set to true to flush after every log message,
        # to an integer to flush every N messages, or to false, nil, or zero to
        # never auto-flush. If you turn auto-flushing off, be sure to regularly
        # flush the log yourself -- it will eat up memory until you do.
        def auto_flushing=(period)
          @auto_flushing =
            case period
            when true;                1
            when false, nil, 0;       MAX_BUFFER_SIZE
            when Integer;             period
            else raise ArgumentError, "Unrecognized auto_flushing period: #{period.inspect}"
            end
        end

auto_flushing= 은 period를 인자로 받는 것 같지만 boolean 값도 받아서 처리한다.
메서드는 한가지 일을 해야 하지만, 한 종류의 인자를 받을 필요는 없다. 하튼 자기 할 일만 잘하면 된다. 개발자 헛갈리지 않게.

 

이같은 코드는 생성자에도 존재하는데 바로 @log를 생성하는 부분.

  1.     def initialize(log, level = DEBUG)
          @level         = level
          @buffer        = []
          @auto_flushing = 1
          @no_block = false

          if log.respond_to?(:write)
            @log = log
          elsif File.exist?(log)
            @log = open(log, (File::WRONLY | File::APPEND))
            @log.sync = true
          else
            FileUtils.mkdir_p(File.dirname(log))
            @log = open(log, (File::WRONLY | File::APPEND | File::CREAT))
            @log.sync = true
            @log.write("# Logfile created on %s" % [Time.now.to_s])
          end
        end

write가 있으면 그냥 쓴다. 왜냐면 우린 @log.write만 쓸 꺼니까..
그렇지 않으면 파일 이름으로 간주하고, 찾아본다. 그래도 없으면 디렉토리+파일 이름으로 간주하고 디렉토리도 만들고 찾는다.
파일이 없는 경우 이 로그 파일은 새 것이므로 맨 윗줄에 주석까지 달아주는 센스를 보여준다.

후에 write외에 다른 메서드를 쓸 일이 생기는데... 바로 close할 때다.

  1.     def close
          flush
          @log.close if @log.respond_to?(:close)
          @log = nil
        end

@log가 IO 객체? 라면 아마 close를 모두 가지고 있을 것이다. 위위 코드에서 두번째와 세번째 경우는 모두 close를 가지고 있을 것이다.
하지만 직접 구현한 객체라면? 이 객체는 write는 있는데 close는 없다면?

그런 경우에 위 close 메서드의 두번째 줄 같은 코드가 필요하다.
duck type을 이용하여 @log를 생성하여 쓰고... close는 있으면 하고 없으면 말고.
아.. 아름답다. (라고 세뇌한다.-_-)

 

오늘은 비도 오고 하니까 이만

 

References

이 글은 스프링노트에서 작성되었습니다.

Posted by 나이누