2 분 소요

1. 파이썬은 동적 타입 언어

파이썬은 동적 타입 언어(Dynamically Typed Language)이다: 정적 타입 언어인 C나 자바와 달리 원시 타입인 string, boolean, int 등을 선언할 필요가 없다. 하지만 선언을 하지 않은 만큼 컴퓨터는 할 일이 늘어난다. 각 속성에 접근해 타입을 알아봐야 할 필요가 있기 때문이다. 그리고 정말 동적이기 때문에 파이썬을 최적화하기가 어렵다.

C나 자바 같은 언어들은 기본적으로 원시 타입(Primitive Type)을 제공 한다. c 언어는 동일한 정수형이라도 크기나 부호에 따라 매우 다양한 원시 타입을 제공한다.

print-image

원시 타입은 메모리에 타입 크기만큼의 공간을 할당하고 그 공간을 값으로 채워넣는다.

원시 타입이 정수형의 덧셈 연산을 하는 경우 메모리에서 값을 꺼내 한번 연산하면 된다:

// C code
int a = 1;
int b = 2;
int c = a + b;

C 덧셈

  1. 1을 a에 할당
  2. 2을 b에 할당
  3. binary_add<int, int>(a,b) 호출
  4. 결과를 c에 할당

print-image

하지만 파이썬에는 원시 타입이 없고 모든 것이 객체다. 그래서 값을 꺼내는 데만 해도 PyObject_HEAD에서 타입코드를 찾는 등 여러 단계의 부가 작업이 필요하다.

# Python Code
a = 1
b = 2
c = a + b

Python 덧셈

  1. a에 1을 할당
  2. a->PyObject_HEAD->typecode 정수 설정

  3. a->val = 1 설정

  4. b에 2를 할당

  5. b->PyObject_HEAD->typecode 정수 설정

  6. b->val = 2 설정

  7. binary_add(a,b) 호출

  8. a->PyObject_HEAD 에서 typecode 찾기

  9. a는 정수형; 값 a->val

  10. b->PyObject_HEAD 에서 typecode 찾기

  11. b는 정수형; 값 b->val

  12. binary_add<int, int>(a->val, b->val) 호출

  13. 정수형 결과값 result

  14. 파이썬 개체 c 생성

  15. c->PyObject_HEAD->typecode 정수 설정

  16. c->val에 result 설정

2. 인터프리터(Interpret)

컴파일을 하는 C나 Java와 다르게 Python은 인터프리터로 소스코드를 한줄씩 읽어서 바로 실행한다. 인터프리터가 한줄씩 읽어 실행하기 때문에 컴파일 언어에 비해 속도가 느려진다.

3. GIL(Global Interpreter Lock)

GIL은 파이썬 객체에 대한 다중 접근을 보호하기 위한 Mutex로서 여러 쓰레드가 병렬적으로 실행하지 못하도록 하는 것이다. 따라서 파이썬 인터프리터가 한 번에 하나의 쓰레드만 처리할수 있다.

GIL을 채택한 이유

Python에서 모든 것은 객체(Object)이다. 그리고 각 객체는 참조 횟수(Reference Count)를 저장하기 위한 필드를 갖고 있다. 참조 횟수란 그 객체를 가리키는 참조가 몇 개 존재하는지를 나타내는 것으로, Python에서의 GC(Garbage Collection)는 이러한 참조 횟수가 0이 되면 해당 객체를 메모리에서 삭제시키는 메커니즘으로 동작하고 있다.

참조 횟수에 기반하여 GC를 진행하는 Python의 특성상, 여러 개의 쓰레드가 Python 인터프리터를 동시에 실행하면 Race Condition이 발생할 수 있기 때문이다.

Race Condition이란, 하나의 값에 여러 쓰레드가 동시에 접근함으로써 값이 올바르지 않게 읽히거나 쓰일 수 있는 상태를 말한다. 이러한 상황을 보고 Thread-safe 하지 않다고 표현하기도 한다.

즉, 여러 쓰레드가 Python 인터프리터를 동시에 실행할 수 있게 두면 각 객체의 참조 횟수가 올바르게 관리되지 못할 수도 있고, 이로 인해 GC가 제대로 동작하지 않을 수도 있다는 말이다. 물론 이러한 Race Condition은 뮤텍스(Mutex)를 이용하면 예방할 수 있다.

문제는, Python에서 모든 것은 객체이고, 객체는 모두 참조 횟수를 가진다. 따라서 GC의 올바른 동작을 보장하려면 결국 모든 객체에 대해 뮤텍스를 걸어줘야 한다는 말이 된다.

결국 Python은 마음 편한 전략을 택하였다. 애초에 한 쓰레드가 Python 인터프리터를 실행하고 있을 때는 다른 쓰레드들이 Python 인터프리터를 실행하지 못하도록 막는 것이다. 이를 보고 “인터프리터를 잠갔다”라고 표현한다. 즉, Python 코드를 한 줄씩 읽어서 실행하는 행위가 동시에 일어날 수 없게 하는 것이다. 그러면 모든 객체의 참조 횟수에 대한 Race Condition을 고민할 필요도 없어진다. 뮤텍스를 일일이 걸어줄 필요도 없어지는 것이다. 이것의 GIL의 존재 이유이다.

댓글남기기