Shim Won

January 10, 2015 7:14 am

번역 ActiveRecord 4.2의 형변환

지난 달 레일스 4.2가 릴리즈 되었습니다. 제 글들을 구독하고 계셨다면, 한방에 3.2에서 4.2로 업그레이드 하는 방법도 다룬 적이 있습니다. 이는 팬스 밖에서 구경하는 사람들에게 우리가 사랑하는 프레임워크의 업그레이드가 얼마나 쉬운지를 강변하고 있습니다. 하지만 종종, 버전 변경은 우리가 전혀 볼 수 없는 곳의 구현이 바뀝니다. 예를 들어 AdequateRecord Pro™에서 아론 페터슨이 한 모든 일은 API의 밖에서 보기엔 변화가 없는 성능 최적화입니다. 당신이 소스를 읽지 않는 한, 그 훌륭한 변경의 대부분은 눈치채지 못하고 지나가고 이는 무척 애석한 일이 될 겁니다. 왜냐하면 그중 일부는 우리의 삶을 편하게 해주거든요.^1

오늘은 ActiveRecord SQL 서버 어뎁터를 최신에 맞게 갱신하면서 발견한 새로운 멋진 기능의 일부를 여러분께 공유하고 싶습니다. 특히 어떻게 ActiveRecord가 값을 형 변환하는가에 대해서 이야기하겠습니다. 레일스 4.2이전엔, 모든 형 변환은 ActiveRecord::ConnectionAdapters::Column 객체의 value_to_date같은 클래스 메소드 안에서 행해졌습니다. 션 그리핀이 이 복잡한 프로세스에 대해 훌륭히 설명한 적이 있죠. 미리 말해두지만, 저 글은 자질구레하고 읽기에 지루합니다.

이 프로세스는 제가 기억하는 한 계속 주변에 있었습니다. 이것은 정말 힘들게 데이터베이스로 들어오고 나오는 값을 변환하는 잘 추상화된 OO 코드를 썻습니다. 새 ActiveRecord::Type 네임스페이스가 나오면서 모든게 변했습니다. 이 네임스페이스에 있는 모든 객체는 굉장히 명백하고 잘 문서화된 인터페이스를 가진 그냥 루비 객체 ^2 입니다. 베이스 클래스는 ActiveRecord::Type::Value이고 아래는 주석을 조금 제거한 버전의 코드입니다. 슬쩍 한 번 읽어보세요.

module ActiveRecord
  module Type
    class Value
      def type_cast_from_database(value)
        type_cast(value)
      end
      def type_cast_from_user(value)
        type_cast(value)
      end
      def type_cast_for_database(value)
        value
      end
      def type_cast_for_schema(value)
        value.inspect
      end
      def changed?(old_value, new_value, _new_value_before_type_cast)
        old_value != new_value
      end
      def changed_in_place?(*)
        false
      end
      private
      def type_cast(value)
        cast_value(value) unless value.nil?
      end
      def cast_value(value)
        value
      end
    end
  end
end

보셨나요? 굉장하죠. 드디어 아래에 있는 모든 항목을 커버하는 객체를 보고 있습니다.

  • 생 DB 값으로 변환
  • 유저입력을 DB 입력에 대비한 값으로 변환
  • 스키마 덤프에 있는 기본 값으로 변환
  • ActiveRecord::ConnectionAdapters::Column의 코드가 늘지 않도록 함
  • 기타등등등등…

많은 데이터베이스 커낵션 젬들이 여전히 생 문자열로 모든 값을 반환하고 있습니다. 그들만의 type_cast_from_database 구현하려면 Value의 서브 클래스를 정의하면 됩니다. 예를 들어, 여기 Integer 객체의 기본 동작이 있습니다. 정말 간단하죠?

def type_cast_from_database(value)
  return if value.nil?
  value.to_i
end

레일스 코어팀이 한 일 중에 이 장점을 더욱 좋게 만든 것은 데이터베이스에 저장하기 전에 속성을 할당할 때 루비의 값들을 체크할 수 있게 해준 것입니다. 이런 일은 이제 SQL 형에서 파싱한 limit 속성을 사용해 Integer클래스 안에서 합니다. 아래의 코드가 그 클래스의 눈에 띄는 부분입니다.

module ActiveRecord
  module Type
    class Integer < Value
      def initialize(*)
        super
        @range = min_value...max_value
      end
      private
      def cast_value(value)
        case value
        when true then 1
        when false then 0
        else
          result = value.to_i rescue nil
          ensure_in_range(result) if result
          result
        end
      end
      def ensure_in_range(value)
        unless range.cover?(value)
          raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || 4}"
        end
      end
      def max_value
        limit = self.limit || 4
        1 << (limit * 8 - 1) # 8 bits per byte with one bit for sign
      end
      def min_value
        -max_value
      end
    end
  end
end

Integer 값 객체에서 사용할 모든 형은 이제 값이 허용가능한 데이터베이스의 범위안에 있는지 체크하게 됩니다. 제가 말할 수 있는 범위에서는, 레일스 코어에 있는 Integer 객체가 이 일을 했습니다만, Decimal이나 다른 값도 구현해야 했었죠. 어떻게 SQL 서버의 smallint(2) SQL 형이 동작하는지 보세요.

@obj.small_int_value = -32_768
@obj.small_int_value = -32_769  # => RangeError!
@obj.small_int_value = 32_767
@obj.small_int_value = 32_768   # => RangeError!

저 객체들을 사용해 우리가 할 수 있는 일은 훨씬 더 많이 있습니다. PostgreSQL 어댑터는 벌써 JSON 데이터 타입을 변환하고 있습니다. 심지어 SQL 서버가 XML 데이터 형을 위한 Nokogiri 객체를 반환하게 하는 것도 봤습니다. 무궁무진합니다. 코어 Value 객체는 SQL 서버 어뎁터가 다른 커낵션 모드에 대한 보호를 구현할 수 있게 합니다. TinyTDS 커낵션은 모든 DB 값을 적절한 루비 기본형(primitive)에 매핑합니다. 소중한 시간을 낭비하지 않도록, 모든 레일스 형변환을 한 곳을 모이도록 할 수 있습니다.

이 객체들은 위대한 전진이고 DB객체를 확장할 수 있도록 모든 젬들에게 가능성을 열어 주었습니다. ActiveRecord를 더 좋고 더 빠르고 더 쓰기쉽게 해준 션 그리핀과 그 밖에 분들께 감사드립니다.

Resources

1: 삶이라곤해도 코딩이야기입니다. ..: 그냥 루비 객체 PORO (Plain old ruby object)