파이썬 프로그램의 메모리는 어떻게 관리될까? 🤔

codebeez 글의 의역본입니다.


---

많은 사람들이 애용하는 파이썬은 아이디어를 손쉽게 코드로 구현해낼 수 있는 개발 언어입니다. 대부분 그 과정에서 파이썬의 비효율성을 대면하게 되는데 그 중 하나가 메모리 관리입니다. 정적 타입 개발 언어와 비교하자면 파이썬의 메모리 관리는 비효율적입니다. 그리고 이를 해결하기 위해 온라인에서 도움을 요청하면 “그냥 Rust로 다시 쓰셈 ㅇㅇ”과 같은 답변을 쉽게 볼 수 있습니다. 당연하게도, 이런 조언이 틀린 것은 아니지만 실용적이지 않습니다. 대부분 파이썬과 기타 라이브러리로 해당 문제를 해결해보려고 할 겁니다.

파이썬에서 객체가 어떻게 할당되는지, 저장되는지, 삭제되는지 - 대략적인 메모리 관리를 이해한다면 효율적인 코드를 짜는데 도움이 될 것 입니다.


메모리 모델

메모리 모델의 기본은 3가지 개념으로 설명할 수 있습니다: 스택, 힙, 포인터.


스택

스택은 프로그램에 할당되는 메모리의 한 영역을 차지하고 있으며 앱의 실행 순서를 담고 있습니다. 앱에서 함수가 호출되면 스택 제일 위쪽에 스택 프레임이 생성됩니다. 함수가 반환하면 해당 스택 프레임은 스택에서 없어지며 프로그램 제어권을 한층 아래의 스택 프레임으로 넘겨줍니다. 스택의 제일 첫 번째 스택 프레임 함수가 끝나고 반환하면, 스택은 완전히 비게 되고 앱은 종료됩니다.

스택은 지역 변수가 저장되는 위치기도 합니다. 함수가 종료되고 나면 지역 변수가 사라지고, 외부 스코프에서 지역 변수에 접근하지 못하는 이유기도 합니다.


프로그램에서 가장 많은 메모리 영역을 차지하는 곳입니다. 파이썬에서 객체를 생성할 때 힙에 저장 시킵니다. 스택과는 다르게 영역이 구조화되어 있지 않습니다. 객체를 저장하기 위한 공간은 필요할 때 마다 확보됩니다. 힙에 저장되는 객체의 생애주기는 미리 정해지지 않으며 객체는 필요한 경우 삭제되지 않고 계속 힙 영역에 저장되어 있습니다.

프로그램을 실행 시키려면 스택이나 다른 객체에서 특정 객체에 접근이 가능해야 하는데 이 역할을 담당하는 것은 포인터입니다.


포인터

C나 C++을 해본 사람이라면 포인터가 무엇인지 아실 겁니다. 그리고 왜 대부분의 개발 언어들이 포인터란 개념을 추상화 해서 개발자들이 걱정할 필요 없게 하려는지도 이해할 겁니다. 포인터는 메모리 주소입니다. 포인터는 특정 메모리 영역을 가리키고 있으며 해당 주소에는 필요로 하는 데이터가 저장되어 있습니다.


파이썬에서 객체가 생성되면 힙에 저장됩니다. 그리고 해당 객체를 초기화한 지역 스코프 (영역)에 객체를 접근할 수 있는 메모리 주소를 남겨줍니다 (포인터). 즉, 앞서 말한 “스택은 지역 변수가 저장되는 위치기도 합니다” 문장이 완전히 옳다고 보기 어렵습니다. 객체 자체는 힙에 저장되고 해당 스택 프레임에는 객체로 접근 가능한 포인터만 있을 뿐입니다.


이렇게 구현되어 생긴 이점으로는 스택 프레임이 삭제될 때 해당 객체에 대한 포인터만 사라질 뿐, 실제 객체는 그대로 메모리에 남아 있다는 것입니다. 이는 객체의 초기화 이후에 객체에 대한 레퍼런스가 여럿 생길 수 있고 현재 스택 프레임이 삭제되더라도 프로그램 어딘가 아직 해당 객체를 바라보고 있는 포인터가 남아있을 수 있기 때문입니다. 파이썬에서 이런 경우 어떻게 객체를 다루는지 가비지 콜렉션이란 개념을 통해 알 수 있습니다.


포인터의 크기는 파이썬이 돌아가는 컴퓨터의 시스템 구조에 따라 다를 수 있고 한 단어 크기만큼의 크기가 필요합니다. 64-bit 시스템에서는 8 바이트입니다.


파이썬의 메모리 관리

C 스타일 배열 vs 파이썬 리스트

C에서 배열 타입은 사실 배열의 첫 아이템을 가리키는 포인터입니다. 배열의 크기 같은 부가 정보가 제공되지 않습니다. 이런 데이터 타입을 지원하려면, 배열 내에 아이템들이 물리적으로 메모리 영역에 순차적으로 위치해야 합니다. 그리고 배열에 저장되는 아이템의 타입도 알아야 합니다. 이렇게 데이터 구조를 구현하면 비효율적인 메모리 낭비를 줄이고 성능적으로 이득을 볼 수 있습니다.


파이썬의 동적 타입 할당은 위와 같은 효율적인 데이터 구조로 구현하기 어렵습니다. 몇 몇 케이스를 제외하고, 파이썬에서 객체는 포인터로 서로를 간접적으로 가리키고 있습니다. 이는 파이썬의 콜랙션인 list, set, tuple에도 해당됩니다.


파이썬의 거의 모든 데이터 유형에 포인터로 서로를 가리킬 수 있게 한 방식은 파이썬이 이토록 유연한 이유이기도 합니다. 예를 들면, 파이썬의 list, tuple 은 서로 다른 유형의 데이터를 저장할 수 있습니다. 하지만, 이는 메모리 관점에서 정적 타입 개발 언어 보다 성능이 떨어지는 주된 요인이기도 합니다. 파이썬의 표준 라이브러리 중 array를 사용하면 좀 더 효율적인 배열을 생성할 수 있지만, 대부분 현업에서 numpy 같은 써드 파티 라이브러리를 사용합니다.


레퍼런스 카운팅과 가비지 콜랙터

이후 기술된 내용은 CPython을 사용하는 파이썬에만 해당되는 내용입니다. 파이썬은 여러 구현 방식이 있고 CPython 외에 PyPy 같은 것을 선택한다면 레퍼런스 카운팅에 의존하는 가비지 콜랙터 (이하 GC)를 사용하지 않습니다.


파이썬은 레퍼런스 카운팅접근성을 사용하여 메모리 관리를 합니다. 레퍼런스 카운팅은 객체가 변수에 얼마나 할당 되었는지 와 해당 할당이 풀렸는지 세는 수입니다. 레퍼런스 카운트가 0인 객체는 더 이상 사용하는 곳이 없다는 뜻이며 메모리에서 삭제해도 되는 데이터입니다. 또한, 접근성이란 개념도 사용하는데, 이는 스택에서 해당 메모리로 접근할 수 있는 경로가 있는지 확인 하는 것입니다. 접근성을 확인하는 주된 이유는 레퍼런스 카운팅 만으로 삭제 할 수 없는 객체를 식별하고 삭제하기 위함입니다.


CPython의 GC는 generational (세대적) 합니다. GC는 각 세대를 개별적으로 검사합니다. 객체가 처음 생성되면 0번째 generation (세대)에 할당됩니다. 한 세대를 검사한 뒤 여전히 유효한 객체들은 다음 세대로 넘어가게 됩니다. 파이썬에는 총 3가지 세대가 있습니다. 세대가 높아질 수록 GC가 해당 세대에 속한 메모리를 관리하는 빈도가 줄어듭니다. 이는 세대가 높을 수록 계속 쓰일 가능성이 높다고 판단하기 때문입니다.


파이썬에서 제한적으로 GC를 수동으로 조작할 수 있습니다. gc 라이브러리를 사용하면 수동으로 gc를 실행시키고 멈출 수 있습니다.


파이썬에서 모든 객체는 GC에 의해서 관리되기 때문에 추가적으로 2가지 속성이 객체에 할당됩니다: 객체 레퍼런스 카운트를 위한 ob_refcnt 와 객체의 클래스를 가리키는 ob_type 입니다. 둘을 합쳐 총 16 바이트가 객체마다 추가적으로 필요합니다. 동일한 int 타입을 정적 개발 언어에서는 4 바이트로 해결 할 수 있다는 것을 감안하면 이는 무시 못할 수준의 크기 입니다.


일반적인 타입

sys 모듈을 사용하면 타입의 메모리 사용량을 측정할 수 있습니다. CPython에서 float의 크기는 24 바이트입니다. 이는 double-precision floating number를 위한 8바이트와 추가적인 16바이트로 이루어져 있음을 알 수 있습니다.

>>> sys.getsizeof(1.0)
24

정수 타입은 좀 더 이상합니다. 4 바이트 정수 타입은 대략 40억 개의 숫자를 표현할 수 있지만, CPython의 int는 제한이 없습니다. 이는 부가적인 메모리 오버헤드가 생긴다는 것을 뜻합니다. 메모리 사용량은 숫자에 비례해서 계속 늘어날 수 있습니다.

>>> sys.getsizeof(0)
28
>>> sys.getsizeof(2**256)
60

파이썬은 문자열을 위해 utf-8 인코딩을 사용합니다. 즉, ascii 문자는 1 바이트로 표현될 수 있지만 다른 문자들은 unicode로 표현하기 위해 필요한 바이트 만큼 메모리를 차지합니다. 문자열도 문자 수에 따라 메모리 사용량이 달라지고 49 바이트의 오버헤드가 있습니다.

>>> sys.getsizeof('abc')
52
>>> sys.getsizeof('🐍')
80 

표준 콜랙션 타입을 살펴보면, list보다 tuple이 좀 더 효율적입니다. set은 유니크성 검사를 위한 빠른 접근을 지원하기 위해 해싱이 구현되어 있어서 훨씬 비효율적입니다. 셋 모두 메모리의 크기는 콜랙션의 크기에 비례합니다. 따라서 큰 배열을 저장함에 있어서 set보다는 list가 더 메모리 관점에서 효율적입니다. set의 비효율성은 dict 타입에도 똑같이 해당됩니다.

my_list = [random.randint(0, 1000) for _ in range(100)]

>>> sys.getsizeof(my_list)
920
>>> sys.getsizeof(tuple(my_list))
840
>>> sys.getsizeof(set(my_list))
8408


클래스/객체

파이썬에서 클래스를 선언할 때, __dict__라는 속성으로 클래스 내에 필드들이 객체 형태로 저장됩니다. 이는 유연성을 더해줍니다. 예를 들면, 클래스가 선언된 뒤에도 객체에 새로운 값을 할당 할 수 있습니다.

class Foo:
    def __init__(self):
        self.bar = 2

foo = Foo()
foo.xyz = 3  # 허용됨

하지만, 이는 객체를 지원하기 위해 더 많은 메모리를 쓴다는 단점이 있습니다.

>>> sys.getsizeof(foo.__dict__)
296

파이썬의 원시 타입에는 이런 특성이 없습니다. 정확히는 __dict__ 라는 것이 없습니다.

int(0).xyz = 3 # AttributeError
object().xyz = 3 # AttributeError
int(0).__dict__  # AttributeError

파이썬에는 slotted class 라는 개념이 있습니다. 클래스 내에 객체에 저장할 필드를 지정해줘서 유연성은 좀 떨어지더라도 메모리 효율을 향상 시킬 수 있습니다.

class Foo:
    __slots__ = ('bar', 'baz')  # 클래스 필드로 저장할 값을 선언

    def __init__(self, bar, baz):
        self.bar = bar
        self.baz = baz

foo = Foo('bar', 'baz')
foo.xyz = 3  # 이제는 AttributeError를 내뱉음

slotted 필드는 __dict__로 접근하는 것보다 더 빠릅니다.

파이썬을 사용하는 개발자 중 대다수는 dataclasses를 사용하는데, 해당 라이브러리에서도 slot을 손쉽게 설정할 수 있습니다.

@dataclass(slots=True)
class Foo:
    bar: str
    baz: str

foo = Foo('bar', 'baz')


결론

파이썬의 메모리 관리를 이해하는 것은 프로그램의 성능을 이해하는데 도움을 주고 파이썬의 유연성이 어떻게 구현되었지는 이해하는데 도움을 줍니다. 기본적인 것이지만 간단한 변경으로 파이썬의 메모리 효율성을 개선할 수 있는 방법에 대해 기술했으며 도움이 되셨길 바랍니다.

마지막으로 현업에서는 “순수” 파이썬을 그대로 사용 하는 곳은 많이 없을 겁니다. 써드 파티 라이브러리에 의존하며 적어도 일부는 정적인 개발 언어로 작성되어 있을 겁니다. 메모리 효율을 극도로 신경 써야 하는 환경이라면 위의 정보만으로 해결하기 힘들 겁니다. 해결하고자 하는 도메인에 맞는 라이브러리를 찾아서 쓰던가 직접 정적 타입 언어로 구현해야 할 수도 있습니다.


---

원글: https://codebeez.nl/blogs/the-memory-footprint-of-your-python-application/

Codebeez

Codebeez

Codebeez

다음 내용이 궁금하다면?

또는

이미 회원이신가요?

2024년 3월 13일 오후 3:37

 • 

저장 49조회 3,037

댓글 0

    함께 읽은 게시물

    해외 취업이 목표라면 지금부터 알아둬야 할 20가지 자료

    1️⃣ 취업 루트 및 경험담 01. 미국 취업 루트: https://careerly.co.kr/comments/51260 02. 캐나다 취업 허가증 수령 경험담: https://careerly.co.kr/comments/56992 03. 미국 이민/해외 취업 준비 중이라면 꼭 알아야 하는 것: https://careerly.co.kr/comments/56991 04. 실리콘 밸리 개발자의 7가지 찰랜지: https://careerly.co.kr/comments/72236 05. 미국 생활하면서 가장 힘들었던 점: https://careerly.co.kr/comments/52097 06. 미국 취업 비자, 4월이 가장 절망적일 수 있는 이유: https://careerly.co.kr/comments/54680 2️⃣ 영어관련 자료 01. 개발자에게 필요한 영어 실력: https://careerly.co.kr/comments/56961 02. 영어 공부팁: https://careerly.co.kr/comments/60926 03. 영어 독해 공부법: https://careerly.co.kr/comments/67571 3️⃣ 면접... 더 보기

     • 

    댓글 2 • 저장 261 • 조회 7,639



    <알쏭달쏭 포폴 속 문제정의.. 그거 어떻게 하는거죠?>

    문제 정의는 UX 포폴의 처음이자 끝입니다.

    ... 더 보기

     • 

    댓글 1 • 저장 5 • 조회 776


    React Testing Library 사용법

    T

    ... 더 보기

     • 

    저장 30 • 조회 2,378


    📰 대학생이 40년만에 해시테이블의 성능 향상을 이뤄냈다고

    ... 더 보기

    Optimal Bounds for Open Addressing Without Reordering

    arXiv.org

    Optimal Bounds for Open Addressing Without Reordering

     • 

    저장 44 • 조회 3,485


    👋 굿바이 Styled Components 🥹

    S

    ... 더 보기

    Thank you - styled-components

    opencollective.com

    Thank you - styled-components

     • 

    댓글 1 • 저장 39 • 조회 4,170