Marshal

Marshal

Marshal은 Ruby 객체들을 바이트 스트림으로 변환해 주는 모듈이에요. 변환된 데이터를 파일이나 네트워크에 저장해 두었다가, 나중에 다시 읽어 원래 객체를 복원할 수 있죠.

출처: Ruby 4.0 API

본문

객체를 파일로 저장했다가 다시 읽고 싶을 때 Marshal을 써요. dump로 직렬화하고 load로 다시 복원하는 구조예요.

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

마샬된 데이터에는 객체 정보와 함께 메이저/마이너 버전 번호가 저장돼요. 일반적으로 같은 메이저 버전이면서 자신과 같거나 낮은 마이너 버전으로 쓴 데이터만 불러올 수 있어요. Ruby의 -d-v, -w, --verbose 같은 verbose 플래그를 켜면 마이너까지 정확히 맞아야 해요. Marshal의 버전 관리는 Ruby 자체 버전과는 독립적이에요. 마샬 데이터의 첫 두 바이트를 읽으면 버전을 알 수 있어요.

아무 객체나 다 덤프할 수 있는 건 아니에요. binding, 프로시저·메서드 객체, IO 인스턴스, 싱글톤 객체가 섞여 있으면 TypeError가 나요.

클래스에 특별한 직렬화 요구가 있거나, 기본 방식으로는 직렬화할 수 없는 객체를 담고 있다면 직렬화 전략을 직접 구현할 수 있어요. 두 가지 방식이 있는데, 객체가 marshal_dump/marshal_load를 정의하거나 _dump/_load를 정의하면 돼요. 둘 다 정의돼 있으면 marshal_dump_dump보다 우선해요. marshal_dump 쪽이 Marshal 문자열을 더 작게 만들 수 있기도 해요.

보안 고려 사항

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

주의: 신뢰할 수 없는 데이터를 Marshal.load에 넘기지 마세요.

그래서 Marshal.load는 범용 직렬화 포맷으로는 부적합하고, 사용자가 입력한 데이터나 신뢰할 수 없는 데이터는 절대 언마샬하면 안 돼요. 신뢰할 수 없는 데이터를 역직렬화해야 한다면, String, Array, Hash 같은 단순 "기본" 타입만 다룰 수 있는 JSON이나 다른 포맷을 쓰세요. 사용자 입력이 역직렬화할 타입을 임의로 지정하지 못하게 해야 해요.

marshal_dump와 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와 _load

복원할 객체를 스스로 할당해야 할 때는 _dump_load를 써요.

객체를 덤프할 때 인스턴스 메서드 _dump가 호출되는데, 인자로 덤프할 객체의 최대 깊이를 나타내는 정수(-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 — 마샬 데이터 포맷의 메이저 버전.
  • MINOR_VERSION — 마샬 데이터 포맷의 마이너 버전.

클래스 메서드

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

obj와 그 하위 객체들을 모두 직렬화해요. anIO를 지정하면 직렬화된 데이터를 그 IO에 쓰고, 아니면 String으로 돌려줘요. limit을 지정하면 하위 객체 탐색 깊이를 그 값만큼 제한해요. limit이 음수면 깊이 검사를 하지 않아요.

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를 넘기면 역직렬화된 객체가 깊게 동결(frozen)돼요. 동결된 문자열은 중복 제거(deduplication) 덕분에 메모리 사용이 더 효율적일 수 있어요.

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

같은 문자열 값이 freeze: true로 로드되면 같은 객체로 합쳐져서 object_id가 반복되는 걸 볼 수 있어요.

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

load의 별칭이에요.

보안 주의: 신뢰할 수 없는 데이터를 이 메서드에 넘기지 마세요. 개요의 보안 고려 사항을 확인하세요.