Marshal

Marshal

Marshal 라이브러리는 Ruby 객체들을 바이트 스트림으로 변환해서, 현재 실행 중인 스크립트 밖에 저장할 수 있게 해줘요. 이 데이터는 나중에 다시 읽어서 원래 객체를 복원할 수 있어요.

출처: Ruby 3.3 API

본문

마샬링된 데이터에는 객체 정보와 함께 주 버전(major)과 부 버전(minor) 번호가 저장돼요. 일반적인 사용에서는 주 버전 번호가 같고 부 버전 번호가 같거나 낮은 데이터만 로드할 수 있어요. Ruby의 "verbose" 플래그(보통 -d, -v, -w 또는 --verbose로 설정)가 켜져 있으면, major·minor 번호가 정확히 일치해야 해요. Marshal 버전 관리는 Ruby의 버전 번호와는 독립적이에요. 마샬링된 데이터의 처음 두 바이트를 읽으면 버전을 알아낼 수 있어요.

str = Marshal.dump("thing")
RUBY_VERSION   #=> "1.9.0"
str[0].ord     #=> 4
str[1].ord     #=> 8

어떤 객체들은 덤프할 수 없어요. 덤프할 객체에 binding, 프로시저나 메서드 객체, IO 클래스의 인스턴스, 싱글턴 객체가 포함되어 있으면 TypeError가 발생해요.

클래스에 특별한 직렬화 요구가 있거나(예: 특정 형식으로 직렬화하고 싶을 때), 그렇지 않으면 직렬화할 수 없는 객체를 담고 있다면, 직접 직렬화 전략을 구현할 수 있어요.

이를 위한 두 가지 방법이 있는데, 객체에 marshal_dump/marshal_load를 정의하거나 _dump/_load를 정의하면 돼요. 둘 다 정의되어 있으면 marshal_dump_dump보다 우선해요. marshal_dumpMarshal 문자열을 더 작게 만들 수도 있어요.

Security considerations

설계상 Marshal.load는 Ruby 프로세스에 로드된 거의 모든 클래스를 역직렬화할 수 있어요. 많은 경우, 신뢰할 수 없는 소스에서 Marshal 데이터를 로드하면 원격 코드 실행으로 이어질 수 있어요.

그래서 Marshal.load는 범용 직렬화 형식으로는 적합하지 않아요. 사용자 입력이나 신뢰할 수 없는 데이터를 절대 언마샬링하지 마세요.

신뢰할 수 없는 데이터를 역직렬화해야 한다면, JSON이나 String, Array, Hash 같은 단순한 '원시(primitive)' 타입만 로드할 수 있는 다른 직렬화 형식을 사용하세요. 사용자 입력이 역직렬화할 임의의 타입을 지정하도록 두면 안 돼요.

marshal_dump and marshal_load

객체를 덤프할 때 marshal_dump 메서드가 호출돼요. marshal_dumpmarshal_load가 객체를 재구성하는 데 필요한 정보를 담은 결과를 돌려줘야 해요. 그 결과는 어떤 객체든 될 수 있어요.

marshal_dump로 덤프된 객체를 로드할 때는, 먼저 객체가 할당된 다음 marshal_dump의 결과로 marshal_load가 호출돼요. marshal_load는 그 정보로 객체를 다시 만들어야 해요.

예시:

class MyObj
  def initialize name, version, data
    @name    = name
    @version = version
    @data    = data
  end

  def marshal_dump
    [@name, @version]
  end

  def marshal_load array
    @name, @version = array
  end
end

_dump and _load

복원할 객체를 직접 할당해야 하는 경우에는 _dump_load를 사용해요.

객체를 덤프할 때 인스턴스 메서드 _dumpInteger와 함께 호출되는데, 이 값은 덤프할 객체의 최대 깊이를 나타내요(-1은 깊이 검사를 비활성화하라는 뜻이에요). _dump는 객체를 재구성하는 데 필요한 정보를 담은 String을 돌려줘야 해요.

클래스 메서드 _loadString을 받아 같은 클래스의 객체를 돌려줘야 해요.

예시:

class MyObj
  def initialize name, version, data
    @name    = name
    @version = version
    @data    = data
  end

  def _dump level
    [@name, @version].join ':'
  end

  def self._load args
    new(*args.split(':'))
  end
end

Marshal.dump는 문자열을 출력하므로, 복잡한 객체라면 _dumpMarshal 문자열을 돌려주고 _load에서 Marshal.load로 다시 로드할 수도 있어요.

Constants

MAJOR_VERSION

major version

MINOR_VERSION

minor version

Public Class Methods

dump( obj [, anIO] , limit=-1 ) → anIO

obj와 그 모든 하위 객체를 직렬화해요. anIO가 지정되면 직렬화된 데이터가 그곳에 쓰여지고, 그렇지 않으면 String으로 돌려줘요. limit이 지정되면 하위 객체 탐색이 그 깊이로 제한돼요. limit이 음수면 깊이 검사를 수행하지 않아요.

class Klass
  def initialize(str)
    @str = str
  end
  def say_hello
    @str
  end
end

(출력 없음)

o = Klass.new("hello\n")
data = Marshal.dump(o)
obj = Marshal.load(data)
obj.say_hello  #=> "hello\n"

Marshal은 다음 객체들을 덤프할 수 없어요.

  • 익명의 Class/Module.
  • 시스템과 관련된 객체 (예: Dir, File::Stat, IO, File, Socket 등).
  • MatchData, Method, UnboundMethod, Proc, Thread, ThreadGroup, Continuation의 인스턴스.
  • 싱글턴 메서드를 정의하는 객체.

load(source, proc = nil, freeze: false) → obj

source의 직렬화된 데이터를 Ruby 객체로 변환한 결과를 돌려줘요(관련된 하위 객체를 함께 가질 수도 있어요). sourceIO의 인스턴스이거나 to_str에 응답하는 객체일 수 있어요. proc이 지정되면, 각 객체가 역직렬화되는 동안 proc에 전달돼요.

이 메서드에 신뢰할 수 없는 데이터(사용자 입력 포함)를 절대 넘기지 마세요. 자세한 내용은 개요를 참고하세요.

freeze: true 인자가 전달되면, 역직렬화된 객체가 깊게 동결(deeply frozen)돼요. 동결된 문자열 중복 제거 덕분에 메모리 사용이 더 효율적일 수 있어요.

serialized = Marshal.dump(['value1', 'value2', 'value1', 'value2'])

deserialized = Marshal.load(serialized)
deserialized.map(&:frozen?)
# => [false, false, false, false]
deserialized.map(&:object_id)
# => [1023900, 1023920, 1023940, 1023960] -- 4 different objects

deserialized = Marshal.load(serialized, freeze: true)
deserialized.map(&:frozen?)
# => [true, true, true, true]
deserialized.map(&:object_id)
# => [1039360, 1039380, 1039360, 1039380] -- only 2 different objects, object_ids repeating

restore로도 별칭돼 있어요.

restore(source, proc = nil, freeze: false) → obj

load의 별칭이에요.

더 알아보기

  • 객체 직렬화의 또 다른 방법으로 JSON, YAML 문서를 함께 보세요.
  • to_json을 통한 직렬화는 JSON 모듈에서 확인하세요.