본문으로 건너뛰기
김신건의 로그

[Python] pickle, shelve, marshal: 객체 직렬화

· 수정 · 📖 약 2분 · 816자/단어 #python #pickle #shelve #serialization #stdlib
python pickle, pickle.dumps, shelve, marshal, 직렬화, serialization

정의

picklePython 객체를 바이트로 직렬화하는 표준 모듈. dict, list, 사용자 클래스 등 거의 모든 객체를 디스크/네트워크로 옮길 수 있다.

보안 경고: pickle은 신뢰할 수 없는 입력을 절대 로드하지 마라. __reduce__ 메서드를 통해 임의 코드 실행이 가능 (RCE).

기본 API

import pickle

data = {"name": "Alice", "age": 30, "tags": [1, 2, 3]}

# 바이트로 직렬화
b = pickle.dumps(data)
print(type(b), len(b))

# 역직렬화
restored = pickle.loads(b)
print(restored)

# 파일에 직접
with open("data.pkl", "wb") as f:
    pickle.dump(data, f)

with open("data.pkl", "rb") as f:
    restored = pickle.load(f)

json과 달리 binary, Python 전용.

지원 객체

  • 기본 타입: int, float, str, bytes, bool, None
  • 컨테이너: list, tuple, dict, set, frozenset
  • 사용자 클래스 (모듈 경로 + 속성 사전)
  • function, class 자체 (이름 참조)

불가:

  • 람다, 로컬 함수 (__qualname__로 찾을 수 없음)
  • 파일 핸들, 소켓, DB 커넥션
  • 일부 제너레이터, 컨텍스트 매니저

프로토콜 버전

pickle.HIGHEST_PROTOCOL    # 5 (Python 3.8+)
pickle.DEFAULT_PROTOCOL    # 4 또는 5

pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)

높은 protocol = 작고 빠름. 다른 Python 버전과 호환 필요하면 protocol 명시.

  • protocol 5 (3.8+): out-of-band buffer (NumPy 등 zero-copy)
  • protocol 4 (3.4+): 큰 객체 지원
  • protocol 2 (2.3+): 신클래스 지원

사용자 클래스

import pickle

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

u = User("Alice", 30)
b = pickle.dumps(u)

# 역직렬화 시 클래스 import 가능해야 함
u2 = pickle.loads(b)
print(u2.name, u2.age)

pickle은 인스턴스 데이터(__dict__)와 클래스의 이름(module.Classname)을 저장. 역직렬화 시 import 후 __new__로 인스턴스 만들고 __dict__ 복원.

클래스가 이동/이름 변경되면 unpickle 실패.

reduce: 커스텀 직렬화

특별한 객체를 직렬화하려면.

class FileWrapper:
    def __init__(self, path):
        self.path = path
        self.f = open(path)

    def __reduce__(self):
        return (FileWrapper, (self.path,))    # 재생성 인자

__reduce__(callable, args) 또는 더 긴 튜플 반환.

이게 보안 위험의 핵심: 임의 callable + 인자를 실행하라고 지시할 수 있음.

보안: 절대 신뢰할 수 없는 pickle 로드 금지

# 공격자가 만든 pickle
class Exploit:
    def __reduce__(self):
        import os
        return (os.system, ("rm -rf /",))

evil = pickle.dumps(Exploit())

# 서버에서 함부로
pickle.loads(evil)    # 시스템 삭제!

규칙:

  • 외부에서 받은 pickle 절대 로드 X
  • 신뢰 경계 내부에서만 (내부 캐시, 같은 프로세스 IPC)
  • 외부 통신은 JSON, MessagePack, Protocol Buffers 등 안전한 포맷

json vs pickle

jsonpickle
보안안전위험
크기작음
속도보통빠름
호환성모든 언어Python 전용
지원 타입dict/list/기본형거의 모든 객체
가독성OX (binary)
상황선택
다른 시스템과 통신json
Python 캐시, 임시 저장pickle
사용자가 업로드 가능json (절대 pickle X)
성능 + 풍부한 타입pickle

shelve: dict 같은 영속 저장소

내부적으로 pickle + dbm.

import shelve

with shelve.open("mydata.db") as db:
    db["user1"] = {"name": "Alice", "age": 30}
    db["users"] = [1, 2, 3]

with shelve.open("mydata.db") as db:
    print(db["user1"])
    for k in db:
        print(k, db[k])

dict 인터페이스로 디스크 영속화. 키는 string만, 값은 pickle 가능한 모든 것.

함정: 가변 객체 변경이 반영 안 됨.

with shelve.open("db") as db:
    db["list"] = [1, 2]
    db["list"].append(3)    # 반영 X (pickle된 사본을 변경한 것뿐)

    # writeback=True 또는 명시 재할당
    lst = db["list"]
    lst.append(3)
    db["list"] = lst         # 재할당으로 다시 pickle
shelve.open("db", writeback=True)    # 메모리 캐시 + 자동 sync (메모리 부담)

작은 데이터엔 편하지만 동시성·트랜잭션 없음. 대안: sqlite3.

marshal

CPython 내부 포맷 (.pyc 파일에 사용). 사용자 코드는 거의 안 씀.

import marshal
data = marshal.dumps({"a": 1})
print(marshal.loads(data))

장점: pickle보다 빠를 수 있음. 단점: 매우 제한적 타입 지원, Python 버전 호환 없음, 디버깅 어려움. 거의 사용 안 함.

안전한 대안

MessagePack

import msgpack    # pip install msgpack

b = msgpack.packb({"name": "Alice", "age": 30})
restored = msgpack.unpackb(b)

json보다 작고 빠름. Python 외 언어와 호환. 안전.

Protocol Buffers

pip install protobuf

타입 스키마 정의 → 강타입, 다국어 호환. 대규모 시스템에 적합.

Apache Arrow / Parquet

큰 DataFrame, 분석 데이터.

pickle 안전 모드: 화이트리스트

import pickle
import io

class SafeUnpickler(pickle.Unpickler):
    safe_modules = {"builtins": {"list", "dict", "set"}}

    def find_class(self, module, name):
        if module in self.safe_modules and name in self.safe_modules[module]:
            return super().find_class(module, name)
        raise pickle.UnpicklingError(f"forbidden: {module}.{name}")

def safe_loads(data):
    return SafeUnpickler(io.BytesIO(data)).load()

완벽한 안전은 아님. 신뢰할 수 없는 데이터엔 여전히 다른 포맷 권장.

자주 보는 패턴

multiprocessing 인자 전달

multiprocessing은 내부적으로 pickle을 사용. 람다, 로컬 함수, 일부 객체 전달 시 PicklingError 발생.

# WRONG
def main():
    fn = lambda x: x * 2
    pool.map(fn, data)    # 람다는 pickle 불가

# CORRECT
def double(x): return x * 2

def main():
    pool.map(double, data)

캐시 저장

import pickle
from pathlib import Path

def cached(path, fn):
    p = Path(path)
    if p.exists():
        with p.open("rb") as f:
            return pickle.load(f)
    result = fn()
    with p.open("wb") as f:
        pickle.dump(result, f)
    return result

data = cached("cache.pkl", lambda: expensive_compute())

빠른 prototype. 프로덕션엔 더 견고한 캐시 (Redis 등).

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기