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_dump는 Marshal 문자열을 더 작게 만들 수도 있어요.
Security considerations
설계상 Marshal.load는 Ruby 프로세스에 로드된 거의 모든 클래스를 역직렬화할 수 있어요. 많은 경우, 신뢰할 수 없는 소스에서 Marshal 데이터를 로드하면 원격 코드 실행으로 이어질 수 있어요.
그래서 Marshal.load는 범용 직렬화 형식으로는 적합하지 않아요. 사용자 입력이나 신뢰할 수 없는 데이터를 절대 언마샬링하지 마세요.
신뢰할 수 없는 데이터를 역직렬화해야 한다면, JSON이나 String, Array, Hash 같은 단순한 '원시(primitive)' 타입만 로드할 수 있는 다른 직렬화 형식을 사용하세요. 사용자 입력이 역직렬화할 임의의 타입을 지정하도록 두면 안 돼요.
marshal_dump and marshal_load
객체를 덤프할 때 marshal_dump 메서드가 호출돼요. marshal_dump는 marshal_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를 사용해요.
객체를 덤프할 때 인스턴스 메서드 _dump가 Integer와 함께 호출되는데, 이 값은 덤프할 객체의 최대 깊이를 나타내요(-1은 깊이 검사를 비활성화하라는 뜻이에요). _dump는 객체를 재구성하는 데 필요한 정보를 담은 String을 돌려줘야 해요.
클래스 메서드 _load는 String을 받아 같은 클래스의 객체를 돌려줘야 해요.
예시:
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는 문자열을 출력하므로, 복잡한 객체라면 _dump가 Marshal 문자열을 돌려주고 _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 객체로 변환한 결과를 돌려줘요(관련된 하위 객체를 함께 가질 수도 있어요). source는 IO의 인스턴스이거나 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모듈에서 확인하세요.