프로그래밍 언어와 파이썬
파이썬(Python)은 1980년대 후반 귀도 반 로썸이라는 네덜란드 프로그래머가 고안한 하이레벨 하이브리드 프로그래밍 언어입니다. 이러한 파이썬을 알아보기 위해서, 이번 글에서 프로그래밍 언어는 무엇이고 그러한 프로그래밍 언어에 속하는 다양한 언어 중 파이썬은 어떠한 특징을 가지고 있는지 알아보겠습니다.
- 프로그래밍 언어란?
- 파이썬 언어의 특징들
프로그래밍 언어란?
프로그래밍 언어는 다수의 작성자들이 텍스트 형태로 이뤄진 문서를 통해 컴퓨터에게 특정 작업의 절차를 전달하기 위해서 사용됩니다. 그렇기에 대부분의 프로그래밍 언어는 '지시어(instruction)'로 구성되어 있습니다 [2]. 예로, 한 배달앱 사용자의 핸드폰에서 발생된 요청이 컴퓨터에게 전달되면 그러한 요청을 어떠한 절차로 처리하고 무엇을 응답해주어야 할지, 브라우저 사용자가 + 버튼을 누르면 어떠한 형태로 그러한 이벤트를 처리해야할지에 대한 내용은 모두 프로그래밍 언어로 구성된 '프로그램'에 기술된 내용에 따라 진행됩니다.
그렇기에 (대부분) 다수의 작성자는 프로그래밍 언어로 구성된 프로그램을 컴퓨터가 수행하길 원하는 형태로 수정하고, 작성합니다.
그런데 작성자는 사람이기에 작성자가 작성하는 언어는 사람이 이해할 수 있는 형태이지만, 그러한 원본의 프로그래밍 언어(source code)를 컴퓨터는 이해할 수 없습니다. 그렇기에 그러한 소스코드를 컴퓨터가 이해할 수 있는 형태로 번역해주어야 합니다. 이렇게 번역하는 것을 컴파일(compile)이라고하며, 컴파일러(compiler)가 그러한 작업을 담당해줍니다 [3]. 그렇게 번역된 코드는 실행가능한(executable) 프로그램이 됩니다.
이 부분에서 사람이 컴퓨터가 이해할 수 있는 언어로 직접 작성하면 컴파일할 필요가 없지 않을까요? 물론 그러한 부분도 가능합니다. 하지만 그것은 마치 30년 동안 한국어를 사용한 사람이 완벽하고 빠르게 번역되는 자동번역기를 놔두고 완벽하게 문자체계와 문법, 언어체계가 다른 언어를 사용해 의사를 전달하려는 것과 같이 비효율과 오류를 발생시킬 것입니다.
그렇기에 위 이미지를 좀 더 자세히 살펴보면 다음과 같습니다:
그렇게 컴퓨터가 이해할 수 있어서 실행가능한 프로그램이 컴퓨터에 전달되면, '작업절차서'와 같은 것을 작업자에게 전달한 것과 비슷한 상황이 됩니다. 실제로 '수행'하기 위해서는 작업자가 시간, 필요한 자원 등을 소비하며 진행하도록 지시하여야 합니다.
유사하게 컴퓨터는 실제로 실행하도록 지시를 받으면 다양한 자원(CPU, Memory, 네트워크 대역 등)과 실행가능한 프로그램을 '프로세스'라는 형태로 담아 작업을 수행하게 됩니다. 우리가 컴퓨터를 켠 후 브라우저를 실행하는 것, 다른 애플리케이션을 실행하는 것 등이 이에 해당됩니다.
파이썬은 이러한 '사람이 이해할 수 있는 프로그램'을 작성하는데 사용되는 프로그래밍 언어이기에, 그렇게 작성한 프로그램 역시 위와 같은 형태로 실행되게 됩니다. 그렇다면 파이썬말고도 Machine code, Assembly 언어, Java, C++ 등 다양한 프로그래밍 언어가 있는데 파이썬은 어떻게 다르며 어떠한 특징을 가지고 있는 것일까요?
파이썬 언어의 특징들
어떤 것을 이해하기 위해서는 그것과 상반된 것을 비교하며 극명하게 구분될 수 있습니다. 아래에서도 파이썬의 각 특징과 함께 '상반된 것은 어떠한지'를 기술하여 명확한 대조로 이해를 돕도록 기술하였습니다.
Hybrid(Compiled, Interpreted)로 주로 실행됨
파이썬은 위에서 살펴본 소스코드-컴파일-머신코드-수행 구조에서 인터프리터(interpretor)라는 '파이썬 전용 실시간 번역 수행기'를 추가한 구조로 주로 실행됩니다. 인터프리터는 머신코드로의 컴파일 없이 프로그래밍 또는 스크립팅 언어로 작성된 지시(instruction)를 직접 실행해주는 프로그램입니다.
여기서 짚고 넘어가야할 중요한 부분은 흔히 언어에 대해 Compile 언어 또는 Interpreted 언어라는 표현을 사용하지만, 그러한 부분은 언어의 특성이 아니라 구현의 특성이라는 부분입니다 [4]. 그렇기에 특정 언어의 경우는 Compile 형태로 구현되어 실행되거나, Interpreted 형태로 구현되어 실행될 수 있습니다.
파이썬(CPython)은 주로 아래와 같은 구현으로 실행됩니다. 개발자는 소스코드를 작성하고, 그러한 소스 코드는 op_codes라는 중간 단계의 언어(Intermediate representation [7])로 컴파일됩니다. 이후 op_codes는 파이썬 인터프리터에 전달되어 실행되게 됩니다.
인터프리터 구조는 보통 '중간 단계의 언어'를 머신코드로 변경하기 위해 매 구문이나 함수가 실행할 때마다 수행하는 반면 머신코드로 직접 컴파일하는 컴파일러 구조에서는 처음에 한 번 모든 소스코드를 대상으로 수행합니다. 효율적인 인터프리터에서는 '번역' 작업이 컴파일러와 유사하게 프로그램, 모듈, 함수, 심지어 구문이 실행되는 처음에만 수행되지만, 대부분의 상황에서 컴파일러는 소스코드를 최적화하기 위한 충분한 시간이 주어지기에 인터프리터 기반의 실행보다 빠릅니다.
실제로 파이썬에도 위의 CPython과 다른 형태로 구현되어, 다르게 실행되는 것들이 존재합니다. Jython은 컴파일 시 Java bytecode로 컴파일되어 JVM에서 실행됩니다. IronPython의 경우 컴파일 후 마이크로소프트의 Common Language Runtime에서 실행되게 됩니다 [9].
High-level
하이레벨 프로그래밍 언어는 컴퓨터의 상세한 사항으로부터 멀리 추상화된 프로그래밍 언어입니다. 로우레벨(low-level) 프로그래밍 언어와 대비되어, 하이레벨 언어는 자연어의 요소를 사용하며 더 사용하기 쉽고 컴퓨팅 시스템의 처리를 자동화하여 프로그램 개발 과정을 더 간단하고 이해하기 쉽도록 만들어 줍니다 [8].
예로, 파이썬은 레지스터, 메모리 주소, 콜 스택과 같은 컴퓨터 시스템에 가까운 요소들을 다루는 대신 변수, 배열, 객체와 같은 추상화된 컴퓨터과학의 개념을 다루며 최적의 효율성보다는 사용성에 중심을 두고 있습니다.
Indentation-based
파이썬은 Indentation 기반의 언어입니다. print('Yes')라는 구문은 if라는 제어문에 영향을 받는데, 그 이유는 4개의 스페이스로 indent되어 if라는 제어문 영향권에 존재하기 때문입니다 [10].
if name == 'kaden':
print('Yes')
print('Always')
대비되어 Java라는 프로그래밍 언어는 대괄호를 통해 if의 영향권을 구분하게 됩니다:
if (name.equals('kaden')) {
print('Yes')
}
print('Always')
Object-oriented
객체 지향 언어(Object-oriented Programming Language)가 아닌 언어의 종류로는 절차형 언어(Procedural), 명령형 언어(Imperative)가 존재합니다 [11]. 많이 사용되는 대부분의 프로그래밍 언어가 멀티패러다임으로 객체 지향 언어의 특성과 동시에 절차형, 명령형 언어의 특징도 가지고 있습니다.
객체 지향 언어는 데이터와 코드를 포함하는 "객체"라는 개념에 기반한 프로그래밍 패러다임인데요. 주로 (이후에 배울) class라는 것으로 기반으로 객체를 관리합니다. 절차형 언어와 대비되어 설명을 드리면 아래와 같습니다:
비유를 들자면, 양식코스를 준비하고 서빙하는 과정을 아래와 같이 '절차'를 중심으로 바라보고 구성할 수 있습니다:
반면, 추상화된 객체를 생성하여 객체를 중심으로 '묶어서' 표현하면 아래와 같을 수 있습니다:
절차형이든 객체지향이든 어떠한 흐름은 둘 다 가지고 있습니다. 반면 객체지향은 전체 흐름에 있어서 각 구분별로 '재료 준비', '조리', '서빙'과 같이 추상화된 객체를 생성해 '데이터와 코드'를 포함하고 있는 것을 알 수 있습니다. 이렇게 절차형 언어와 객체지향언어는 코드 작성 시, 중심 관점에 있어 차이를 보이며 그에 따라 다른 스타일의 '문법'을 가지게 됩니다.
Dynamically Typed
비교를 통해 알아보면, 동적타이핑 언어인 파이썬의 변수에 30을 할당하는 코드는 다음과 같습니다:
age = 30
반대로 정적타이핑(Statically typed) 언어인 Java의 변수에 30을 할당하면 다음과 같습니다:
int age = 30;
주목할 차이점은 Java 코드 상의 맨 앞에 존재한는 int입니다. 정적타이핑 언어는 이렇게 어떠한 값을 변수에 할당할 때에 자료형을 명시해주어야 합니다. 반면 동적타이핑 언어인 파이썬은 age라는 변수에 30이 할당되면서 age의 자료형이 결정되게 됩니다.
정적타이핑은 변수의 자료형을 표시하며 컴파일러에게 추가적인 정보를 제공해주기에 컴파일 시점의 최적화나 오류검출을 더 많은 정보를 가지고 수행할 수 있게 됩니다. 하지만 위의 코드에서도 알 수 있듯이 코드의 양이 늘어나고, age에 다시 다른 자료형을 할당하는 것과 같은 수행이 불가능하게 됩니다.
동적타이핑은 그렇기에 정적타이핑과 반대되는 장단점을 가진다고 할 수 있습니다.
Garbage Collected [12]
프로그래밍 언어는 연산을 수행하기 위해 객체를 사용합니다. 객체는 String, int, bool과 같은 변수를 포함합니다. 또한 객체는 list, hash, class와 같은 좀 더 복잡한 자료 구조도 포함합니다.
메모리 상의 객체는 프로그램이 빠르게 데이터에 접근할 수 있도록 해줍니다. 많은 프로그래밍 언어에서, 코드 상의 한개의 변수는 단순히 메모리 상의 객체를 가르키는 포인터입니다. 프로그램에서 변수가 사용되면, 프로세스는 메모리에 있는 해당 변수의 값에 접근하여 연산을 수행하게 됩니다.
초기의 프로그래밍 언어에서는 작성된 코드 상에서 메모리를 직접 관리해주어야 했습니다. 이것은 리스트나 객체를 생성하기 위해 먼저 변수를 위한 메모리를 할당해야만 하는 것을 의미합니다. 그렇한 변수를 모두 사용한 이후에는, 역시 코드 상에서 직접 할당을 거둬들여서 메모리를 다른 곳에서 사용할 수 있도록(free) 해주어야 합니다. 이와 같은 부분에서 아래의 2가지 문제가 발생합니다:
- 메모리 free해주는 것을 까먹을 수 있음. 사용하고 나서 메모리를 '놓아주는' 것을 빼먹는다면, 이것은 메모리 누수(leak)라는 결과로 이어지게 됩니다. 그렇기에 프로그램은 필요하지도 않은 많은 메모리를 사용하며 과다하게 메모리를 소모합니다.
- 메모리를 너무 일찍 free해 줄 수 있음. 이 경우는 아직 사용하고 있는 메모리를 너무 일찍 '놓아주는' 것입니다. 이것은 프로그램이 메모리 상의 값에 접근하려고 할 때, 이미 값이 사라지거나 다른 값으로 대체되어 있기에 프로그램의 장애를 발생시킬 수 있습니다.
이러한 2가지 문제는 빈번하였고 매우 위험하였기에, (비교적) 최근의 프로그래밍 언어들은 자동적으로 메모리 관리를 진행해줍니다.
자동 메모리 관리와 가비지 콜렉션
자동 메모리 관리 시에는 인해 개발자가 작성하는 코드에서 메모리를 관리해주는 것이 아니라, 프로그램 실행을 담당하는 런타임이 메모리 관리를 담당하게 됩니다.
여러 방식이 있으나, 가장 많이 사용되는 것은 '레퍼런스 카운팅' 방식입니다. 런타임은 객체에 대한 레퍼런스를 카운팅하고, 레퍼런스가 0인 객체는 미사용으로 여겨져 메모리에서 지워지게 됩니다.
그렇기에 개발자는 low-level의 메모리 관리를 신경쓰지 않고 개발에 집중할 수 있습니다. 또한, 치명적인 위의 2가지 문제도 피할 수 있게 됩니다.
반면 프로그램은 추가적인 메모리와 컴퓨팅 자원을 소모하여 모든 레퍼런스를 추적해야 합니다. 더욱이, 메모리 상의 필요없는 객체들을 지우기 위해서 많은 프로그래밍 언어들은 "순간적인 멈춤(stop-the-world)"을 가지며 그 시간 동안 가비지 콜렉터(Garbage Collector, 쓰레기 수집기)가 필요없는 객체들을 지우도록 하게 됩니다.
현재는 컴퓨터 하드웨어의 많은 발전으로 단점보다는 장점이 더 우세한 상황이며 Java, Python, Golang과 같은 많은 언어들이 자동 메모리 관리를 사용합니다.
성능이 크리티컬한 장시간 실행되는 애플리케이션에서 몇몇의 언어는 수동으로 메모리 관리를 해주어야 합니다. C++과 macOS와 iOS에 사용되는 Objective-C, Rust가 그렇습니다.
파이썬의 가비지 콜렉션
파이썬의 가비지 콜렉션은 아래 2가지 방법을 통해 구성됩니다:
- 레퍼런스 카운팅
- Generational 가비지 콜렉션
레퍼런스 카운팅
CPython의 주요 가비지 콜렉션 메커니즘은 레퍼런스 카운트를 통해 이뤄집니다. 객체가 생성될 때마다, 파이썬 객체를 이루는 내부 C 객체는 Python type과 레퍼런스 카운트를 가집니다.
기본적으로 객체가 레퍼런스되면 카운트는 1 증가하고, 디-레퍼런스되면 1 감소합니다. 만약 0이면, 객체에 대한 메모리는 할당을 거둡니다(deallocate).
import sys
a = 'my-string'
sys.getrefcount(a)
2
Cyclic 레퍼런스를 감지하지 못하는 결점과 같은 여러 단점이 지적되지만, 레퍼런스가 0이면 바로 메모리를 free할 수 있다는 장점이 있습니다.
Generational 가비지 콜렉션
레퍼런스 카운팅에 더해서 파이썬은 GGC(Generational Garbage Collection)이라고 불리는 방법도 사용합니다.
class MyClass(object):
pass
a = MyClass()
a.obj = a
del a
위와 같이, 인스턴스 a 생성 후 obj 속성에 자기 자신을 넣고 인스턴스 a를 지워보겠습니다. 위와 같은 상태에서, a는 접근이 불가능하지만, 가비지 콜렉터는 a의 레퍼런스 카운트가 0이지 않기 때문에 지울 수가 없습니다. 이러한 문제는 reference cycle이라고 불리며, 레퍼런스 카운팅 방식으로는 해결할 수 없습니다. 바로 이 부분이 GGCr(generational 가비지 콜렉터)의 중점사항입니다.
GGCr은 Generation과 Threshold라는 2가지 주요 개념에 기반합니다.
가비지 콜렉터는 메모리에 존재하는 모든 객체를 추적합니다. 새로운 객체는 가비지 콜렉터의 첫 generation에서 시작하게 됩니다. 파이썬이 지속적으로 가비지 콜렉팅을 진행하며 generation이 증가하고 객체가 콜렉팅을 피하 살아남는다면, 그 객체는 second 그리고 older generation으로 넘어가게 됩니다. 파이썬의 가비지 콜렉터는 총 3개의 generation을 가지며 객체는 살아남으며 generation을 넘어가게 됩니다.
각 generation에서 가비지 콜렉터 모듈은 객체 숫자의 역치값을 가지고 있습니다. 만약 객체들의 수가 역치값을 넘으면, 가비지 콜렉터는 콜렉션 프로세스를 촉발합니다. 그러한 프로세스에서 살아남은 객체는 older generation으로 넘어가게 됩니다.
레퍼런스 카운팅과 달리, 파이썬 프로그램에서 GGCr의 실행 방식을 변경하게 될 수도 있습니다. 콜렉션을 촉발하는 역치값을 바꾸거나 명시적으로 가비지 콜렉션을 실행하거나 아예 가비지 콜렉션이 실행 안되도록 설정할 수 있습니다.
import gc
gc.get_threshold()
(700, 10, 10)
Reference
[1] https://en.wikipedia.org/wiki/Python_(programming_language)
[2] https://en.wikipedia.org/wiki/Programming_language
[3] https://en.wikipedia.org/wiki/Compiler
[4] https://stackoverflow.com/questions/3265357/compiled-vs-interpreted-languages
[5] https://stackoverflow.com/questions/6889747/is-python-interpreted-or-compiled-or-both
[6] https://en.wikipedia.org/wiki/Interpreter_(computing)
[7] https://en.wikipedia.org/wiki/Intermediate_representation
[8] https://en.wikipedia.org/wiki/High-level_programming_language
[9] https://realpython.com/python-memory-management/
[10] https://en.wikipedia.org/wiki/Indentation_(typesetting)#Indentation_in_programming
[11] https://en.wikipedia.org/wiki/Object-oriented_programming
[12] https://stackify.com/python-garbage-collection/