Algorithm

[크래프톤정글] 파이썬의 메모리 관리

하루이2222 2024. 9. 20. 11:23

파이썬에서 리스트와 문자열의 메모리 사용을 설명하면서, 파이썬의 동적 타이핑(dynamic typing)과 객체의 동등성 판단에서 메모리 주소를 객체의 식별자로 사용하는 방식에 대해서도 추가 설명해보겠다.

1. 파이썬의 메모리 관리

파이썬은 객체 지향 언어로, 모든 데이터가 객체로 표현된다. 각 객체는 메모리에 저장되며, 파이썬의 메모리 관리 시스템이 이를 관리한다. 파이썬은 가비지 컬렉션(garbage collection)을 통해 더 이상 참조되지 않는 객체를 자동으로 메모리에서 해제한다.

  • 레퍼런스 카운팅(Reference Counting): 파이썬은 기본적으로 레퍼런스 카운팅을 사용해 객체의 메모리를 관리한다. 객체가 생성될 때마다 그 객체를 참조하는 변수가 몇 개인지를 세고, 더 이상 참조되지 않으면 그 객체의 메모리가 해제된다.
  • 가비지 컬렉션(Garbage Collection): 레퍼런스 카운팅 외에도, 순환 참조(circular reference)와 같은 복잡한 상황을 처리하기 위해 가비지 컬렉터가 주기적으로 메모리를 정리한다.

2. 파이썬의 동적 타이핑(Dynamic Typing)

파이썬은 동적 타이핑(dynamic typing)을 사용하는 언어다. 이는 변수를 선언할 때 자료형을 명시할 필요가 없으며, 변수에 할당되는 값에 따라 자료형이 결정된다는 의미다.

  • 동적 타이핑: 변수의 타입은 런타임에 결정된다. 예를 들어, 같은 변수에 정수, 문자열, 리스트 등을 할당할 수 있으며, 그때마다 변수의 타입이 변경된다.

    a = 10        # a는 현재 int 타입
    a = "hello"   # a는 이제 str 타입
    a = [1, 2, 3] # a는 list 타입으로 변경
  • 유연성: 동적 타이핑은 코드의 유연성을 높이지만, 타입 오류를 런타임에만 발견할 수 있다는 단점이 있다. 이로 인해 코드의 안정성을 유지하기 위해 타입 힌팅(type hinting)이 도입되기도 했다.

3. 객체의 동등성 판단과 메모리 주소

파이썬에서 객체의 동등성(equality)과 동일성(identity)을 판단하는 방식은 중요하다. 이는 객체가 메모리에 어떻게 저장되고, 어떻게 비교되는지에 영향을 미친다.

  • 동등성(Equality): == 연산자는 두 객체의 값이 같은지를 비교한다. 이는 객체가 가리키는 실제 데이터(내용)가 같은지를 판단하는 것이다.

    a = [1, 2, 3]
    b = [1, 2, 3]
    print(a == b)  # True, 리스트 a와 b의 내용이 동일
  • 동일성(Identity): is 연산자는 두 객체가 동일한 메모리 주소를 가리키는지를 비교한다. 즉, 두 객체가 실제로 동일한 객체인지를 판단하는 것이다.

    a = [1, 2, 3]
    b = a
    print(a is b)  # True, a와 b는 동일한 객체를 가리킴
    
    c = [1, 2, 3]
    print(a is c)  # False, c는 a와 동일한 값을 가지지만 다른 객체임
  • 메모리 주소: 파이썬에서 객체의 식별자(identity)는 메모리 주소를 기반으로 한다. id() 함수는 객체의 고유한 식별자를 반환하며, 이는 해당 객체가 메모리에 저장된 위치를 나타낸다.

    a = "hello"
    b = "hello"
    print(id(a))  # a의 메모리 주소 출력
    print(id(b))  # b의 메모리 주소 출력
    print(a is b) # True, 문자열 인터닝으로 인해 a와 b는 동일한 객체를 참조
    • 인터닝(Interning): 파이썬은 짧고 자주 사용되는 문자열을 재사용하기 위해 인터닝(interning) 기법을 사용한다. 동일한 문자열 리터럴이 여러 번 사용될 경우, 같은 메모리 주소를 참조하게 된다. 이는 메모리 사용을 최적화하기 위한 방법이다.

4. 문자열의 메모리 사용

4.1 문자열의 불변성

파이썬에서 문자열(str)은 불변(immutable) 객체이다. 이는 문자열이 생성된 후에는 그 내용을 변경할 수 없음을 의미한다. 문자열을 수정하는 작업은 실제로는 새로운 문자열 객체를 생성하는 것이다.

s1 = "hello"
s2 = s1 + " world"
# 이때 'hello'와 'hello world'는 각각 별도의 메모리 공간에 저장됨

4.2 문자열의 메모리 할당

  • 문자열 객체의 구조: 파이썬에서 문자열 객체는 다음과 같은 구조를 가진다.

    • 크기(size): 문자열의 길이(문자 개수)를 저장하는 필드.
    • 해시(hash): 문자열의 해시값을 저장하는 필드(해시 테이블에서 빠른 검색을 위해 사용).
    • 문자 데이터: 실제 문자열 데이터를 저장하는 공간.
  • 슬라이싱(slicing): 문자열 슬라이싱은 새로운 문자열 객체를 생성한다. 슬라이싱 결과는 원본 문자열의 일부를 참조하는 것이 아니라, 새로운 문자열로 메모리에 저장된다.

    s = "hello world"
    sub_s = s[0:5]  # 'hello'라는 새로운 문자열 객체가 생성됨
  • 메모리 최적화: 불변성으로 인해, 문자열을 계속 수정하면 메모리 사용량이 증가할 수 있다. 특히, 큰 문자열을 자주 변경하는 경우, 메모리 사용과 성능 문제가 발생할 수 있다. 이 경우, 문자열 연결 대신 리스트에 문자열을 모은 후 ''.join() 메서드를 사용하는 것이 더 효율적이다.

    # 비효율적인 방법
    result = ""
    for s in list_of_strings:
        result += s  # 매번 새로운 문자열 객체를 생성
    
    # 효율적인 방법
    result = ''.join(list_of_strings)  # 한 번에 문자열을 생성

5. 리스트의 메모리 사용

5.1 리스트의 가변성

리스트(list)는 가변(mutable) 객체로, 생성된 후에도 요소를 추가, 삭제, 변경할 수 있다. 리스트의 가변성은 메모리 관리에 있어 문자열과는 다른 특징을 나타낸다.

lst = [1, 2, 3]
lst.append(4)  # 기존 리스트 객체에 요소가 추가됨 (새로운 객체 생성 아님)

5.2 리스트의 메모리 할당

  • 리스트 객체의 구조: 리스트는 다음과 같은 구조로 되어 있다.

    • 크기(size): 리스트가 포함하는 요소의 개수를 저장.
    • 용량(capacity): 리스트가 실제로 할당한 메모리 크기(저장 가능한 요소의 개수).
    • 포인터 배열: 리스트의 각 요소가 저장된 메모리 주소를 가리키는 포인터 배열. 리스트의 요소들은 연속된 메모리 공간에 저장되지 않으며, 포인터를 통해 접근한다.
  • 메모리 확장: 리스트는 가변이므로, 요소가 추가될 때마다 메모리 공간이 확장된다. 리스트의 크기가 용량을 초과하면, 파이썬은 리스트의 용량을 자동으로 증가시키고 기존 요소들을 새로운 메모리 공간으로 복사한다. 이 과정은 자주 발생할 경우 비효율적일 수 있다.

    import sys
    
    lst = []
    print(sys.getsizeof(lst))  # 초기 용량의 메모리 크기 출력
    
    lst.append(1)
    print(sys.getsizeof(lst))  # 요소 추가 후 메모리 크기 출력

    리스트는 요소를 추가할 때마다 크기를 조금씩 확장하며, 확장 과정에서 기존 데이터가 복사되어 새로운 메모리 블록에 할당된다. 이로 인해, 리스트의 크기가 커질수록 확장 비용이 발생한다.

  • 슬라이싱과 복사: 리스트 슬라이싱은 새로운 리스트 객체를 생성한다. 원본 리스트의 일부를 복사하여 새로운 리스트로 저장하기 때문에, 슬라이싱은 메모리를 추가로 사용한다.

    lst = [1, 2, 3, 4, 5]
    sub_lst = lst[1:3]  # [2, 3]이라는 새로운 리스트 객체가 생성됨
  • 메모리 낭비: 리스트는 동적

    배열(dynamic array)로 구현되어 있어, 요소를 추가할 때마다 메모리 할당과 복사가 발생한다. 리스트가 매우 크거나, 자주 확장되는 경우, 메모리 낭비가 발생할 수 있다.

6. 메모리 사용 최적화

문자열 최적화:

  • str.join() 사용: 문자열을 반복적으로 결합할 때는 + 연산자 대신 ''.join() 메서드를 사용해 메모리 사용을 줄인다.

  • 인터닝 활용: 짧고 자주 사용되는 문자열은 파이썬이 자동으로 인터닝 처리하지만, 필요한 경우 수동으로 sys.intern() 함수를 사용해 메모리 효율을 높일 수 있다.

    import sys
    a = sys.intern("some string")
    b = sys.intern("some string")
    print(a is b)  # True

리스트 최적화:

  • 리스트 크기 예측: 요소의 수를 미리 알고 있다면, 초기 리스트 크기를 설정해 메모리 재할당을 최소화할 수 있다.

  • 리스트 컴프리헨션: 리스트 컴프리헨션을 사용해 리스트를 생성하면, 메모리 사용을 줄일 수 있다.

    squares = [x*x for x in range(10)]
  • 슬라이싱 대체: 슬라이싱을 자주 사용하는 경우, 슬라이스 객체(slice object)를 활용하거나 인덱싱을 통해 메모리 사용을 줄인다.

결론

  • 파이썬의 동적 타이핑은 코드의 유연성을 높이지만, 메모리 관리와 객체 동등성 판단에 있어 신중한 접근이 필요하다.
  • 문자열은 불변성이기 때문에, 수정할 때마다 새로운 객체를 생성하므로 메모리 사용량이 증가할 수 있다. 파이썬은 이를 최적화하기 위해 인터닝 등을 사용하지만, 큰 문자열을 자주 수정하는 작업에서는 주의가 필요하다.
  • 리스트는 가변성이므로, 크기가 동적으로 변할 수 있으며, 메모리 확장이 필요할 때마다 기존 요소들을 복사해야 한다. 이로 인해, 메모리와 성능에 영향을 미칠 수 있다.
  • 객체의 동등성은 메모리 주소를 기반으로 판단되며, 동일성을 비교할 때는 is 연산자를 사용하여 메모리 주소를 직접 비교한다. 동등성 판단은 메모리 사용과 객체 관리에 있어 중요한 개념이다.