언어를 바라보는 관점

프로그래밍 언어를 바라보는 관점은 다양하다. '쉬운 언어 vs 어려운 언어'로 시작해서 '좋은 언어 vs 나쁜 언어'를 거쳐서 'Write in C'로 끝맺음을 하는 경우도 있다. 수준 높은 분들은 '함수형 vs 객체지향'과 같은 높은 수준의 난해한 주제를 가지고 토론을 하기도 한다. 프로그래밍 언어를 다루는 교재는 '명령형 언어 vs 함수형 언어', '절차지향 vs 객체지향' 등의 관점을 제시하기도 한다.

관리할 수 있는가?

나는 프로그래밍 언어를 바라보는 관점 중 하나로 '관리형 언어 vs 비 관리형 언어'로 구분한다. 관리형 언어는 메모리를 개발자가 직접 관리하는 언어는 C/C++이 있다. C++을 사용했던 나의 리즈(Leeds) 시절엔 MFC를 주로 사용하였고, MSDN에 나와있는 메모리 관리를 기반으로 개발을 하였다. 메모리 관리 때문에 스트레스가 이만저만이 아니였지만, Visual Studio의 디버거 덕분에 숨은 쉴 수 있었다. 세월이 흘러 JavaSpring으로 먹고 살아가는 동안 메모리는 배포 후에 문제가 발생할 때 들여다보는 것으로 전락했다. 그토록 소중하게 다뤘던 것에서 이제는 많으면 좋지만, 그렇다고 신경쓸 필요가 없어진 것이다. 요즘은 주로 Python/JavaScript를 주로 사용하고 있다보니 메모리에 대한 개념이 없다. 요즘은 변수와 객체의 라이프 사이클이나 스코프(범위)를 더 중요하게 생각하고 있다. malloc(0)를 하면 어떻게 될까?를 가지고 고민하고 친구들고 어떻게 동작해야 될지 나름의 방법을 논의하던 시절은 나에게서 사라졌다.

세고, 삭제한다.

Python은 객체의 할당과 해제를 자동으로 진행한다. 즉, 개발자가 메모리를 직접적으로 관리하지 않는다. Python은 C로 만들어졌음에도 불구하고 사용자가 메모리를 직접적으로 메모리를 할당하거나 해제할 필요가 없도록 설계되었다. 이 말을 쉽게 풀어보면, C언어를 기반으로 만들어진 인터프리터가 메모리를 직접 관리하기 위해선 별도의 방법이 마련되어 있어야 한다. 심지어 이 방법은 편리해야 한다. 해당 방법이 어렵거나 사용이 난해할 경우 당연하게도 개발자에게 외면 받는다. 따라서 손쉽게 사용할 수 있는 메모리 관리 기법이 필요하다. Python은 메모리를 두 가지 기법으로 관리한다. 하나는 레퍼런스 카운팅(reference couning)이며, 다른 하나는 가비지 컬렉터(garbage collector)이다.

서울 지하철 2호선은 순환선이고, 우리 코드의 순환도 그와 같다(?)

초기 Python은 레퍼런스 카운팅만 사용했다. 레퍼런스 카운팅은 특정 객체를 참조하는 다른 객체들을 계산하여 특정 객체의 '레퍼런스 카운트'가 0이 되면 메모리에서 객체를 삭제한다. 해당 방법은 매우 간단한 만큼 직관적이지만 주의할 내용이 많다.

대표적인 것이 'reference cycle'로 알려진 것으로 특정 객체에 접근할 수 있는 방법이 없음에도 불구하고, '레퍼런스 카운트'가 0보다 큰 경우가 있다. 내가 만든 코드는 'pytest`를 곧 잘 통과하긴 했지만, 배포 후에 런타임에 코드가 무너지는 걸 보면서 역시 난 버그 팩토리인가? 하는 고민을 가끔하곤 했다. 여러분도 이런 코드를 만들지 않기 위해서 아래 코드를 구경해보자.

# 자기참조
l = []
l.append(l)
del l

# 상호참조
f1 = f()
f2 = f()
f1.x = b
f2.x = a
del f1
del f2

레퍼런스 카운트는 sys 모듈을 통해서 확인할 수 있는 방법을 제공한다. 대표적인 사용법은 아래와 같다(Q: 레퍼런스 카운트는 몇개로 출력될까?).

import sys
str = 'hello'
sys.getrefcount(str)

레퍼런스 카운트를 확인하는 방법을 제공하지만 막상 레퍼런스 카운트를 확인할 수 있는 방법이 없는 경우도 있다. 이런저런 어른들의 사정 때문에 Python은 카운트 레퍼런스에서 발생하는 문제를 해결하는 방법이 필요했다. 그리고 그 문제는 현재 Python에서 '가비지 컬렉터'로 해결했다.

가비지 컬렉터의 등장

Python에서 사용하는 가비지 컬렉터는 흔히 'generational garbage collector(세대별 가비지 컬렉터)'라 한다. 일단 이 GC를 이해하기 위해선 몇가지 전제가 필요하다. 첫번째는 generation에 대한 이해다. 가비지 컬렉터는 메모리의 모든 객체를 추적해야 한다. 당연하게 이러한 작업은 런타임의 실행 속도에 영향을 준다. 따라서 금방 사라질 객체와 지속될 객체를 구분할 수 있어야 한다. 따라서 Python에서 만들어진 모든 새로운 객체는 1세대 가비지 컬렉터에서 시작한다. 객체가 살아남으면, 두 번째 세대로 올라간다. 가비지 컬렉터는 총 3세대로 구성된다. 이런 세대별 가비지 컬렉터는 Java도 사용하고, C#도 사용한다. 세대별 가비지 컬렉터에 대해서 더 궁금하다면 C# 문서를 읽어보면 좋을 듯 싶다.
두번째는 어떤 기준으로 객체를 이동하는지에 관한 것으로 threshold(임계) 값을 사용한다. 각 세대별 가비지 컬렉터엔 임계값만큼의 객체가 존재하며, 임계값을 초과하면 객체를 검사한다. 이 때 살아남은 객체는 이전 세대로 옮겨진다. 가비지 컬렉터는 내부적으로 generation과 threshold로 관리한다. 가비지 컬렉터에 처음 수집된 것일 수록 더 많이 검사하도록 설계되어 있다. 좀 더 자세한 사항은 이 기사를 참고하자, GC에 관한 가장 쉽고 훌륭한 기사라 할 수 있다.

아무것도 하지 말자

Python 레퍼런스 카운팅과 가비지 컬렉터에 대해서 알아보았다. 우리는 Python의 가비지 컬렉터가 성능에 영향을 줄 것임을 직감적으로 알 수 있다. 하지만 우리는 가비지 컬렉터 동작을 어떤 것도 수정하지 않도록 해야한다. 가비지 컬렉터에 대해서 이런 저런 많은 이야기가 있지만, 메모리를 수동으로 관리하거나 가비지 컬렉터를 수정하는 것은 많은 위험이 따른다. 근래에 Instagram에서 Django 사용성을 높이기 위해서 GC 관련 작업을 진행한 기사를 많이들 참고하지만, 해당 사항은 매우 독특한 경우라서 일반화 하기에 곤란하다.

메모리를 관리하지 않기로 했다면 VM에 전적으로 의존해야 한다. 제대로 제어할 수 없는 것을 몇가지 옵션만으로 제어하고자 하는 욕심은 메모리를 관리하는 관점에서 올바른 선택이 아니다. 가비지 컬렉터에 대해서 알아보았으니, 걱정말고 Python에 맡기자.

다음은?

우리들 마음속에서 메모리를 자유롭게 해주었다면, 이제 우리는 '객체'에 집중해야 한다. 당연하게도 Python은 모든 것이 객체로 구성되어 있다. 그렇다면 도대체 이 '객체'란 무엇을 뜻하는 것일까?