파이썬은 인터프리터언어입니까?

최근에 많이 보게 되는 질문 중 하나가 ‘파이썬은 인터프리터 언어입니까? 컴파일언어입니까?’라는 것이다. 개인적으로 이 질문은 사람을 참 난감하게 하는데, 어떻게 답해야하나에 앞서 아직까지도 이 개념을 이렇게 잘못 가르치는 교재 혹은 과정이 대부분이라는 점 때문이다. 그럼 인터프리터 언어와 컴파일 언어가 무엇인지 알아보고, 과연 파이썬은 인터프리터 언어인지 생각해보자.

참고로, 보통 나는 이 질문에 ‘반만 맞다’고 말하거나 더 이상의 설명이 귀찮은 경우에는 ‘통상 인터프리터 언어라고 합니다.’라고 답한다.

컴파일 언어 (compiled language)

정확하게 말하자면 ‘컴파일 되는 언어’라고 해야 의미가 분명한데, 이 글에서는 줄여서 컴파일 언어라고 하겠다. 컴파일 언어는 “컴파일러에 의해서 구현되는 언어”를 말한다. 컴파일러(compiler)는 ‘컴파일러를 수행하는 프로그램’이라는 뜻인데, 그럼 ‘컴파일’이란 무엇인가?

흔히 컴파일을 C와 같은 언어에서 프로그램 소스코드를 기계어로 번역하는 것을 말하는데, 이것은 매우 좁의 의미의 컴파일이며, 일반적으로 컴파일은 하나의 (프로그래밍) 언어 코드를 다른 언어(혹은 형태)로 변환하는 것을 말한다. 가장 흔이 알려진 컴파일러인 C 컴파일러는 C 소스코드를 기계어(CPU 인스트럭션을 말한다.)로 번역하는 일을 수행하는 프로그램이다. 넓은 의미에서 컴파일러는 보다 다양한 언어에서 찾아볼 수 있다. 예를 들어 TypeScript는 Javascript로 해석되어 실행되는 언어이다. 따라서 TypeScript는 컴파일러를 통해 컴파일되며, 그 결과는 기계어 코드가 아닌 Javascript 코드가 된다.

고전적 의미에서 컴파일 언어는 주로 ‘실행 시간 이전에 기계어 코드로 번역되는 언어’라고들 정의한다. 하지만 당장 위 TypeScript만 보아도 이러한 정의로는 커버할 수 없다는 것을 알 수 있다.

인터프리터 언어 (interpreted language)

전통적인 의미의 인터프리터 언어는 실행전에 기계어로의 컴파일 과정을 거치지 않으며, 소스코드가 해석기(인터프리터)에 의해 직접 해석되어 실행되도록 구현된 언어를 말한다. 소스코드가 의도하는 작업의 실질적인 수행은 이를 해석한 인터프리터에 의해서 수행된다. 따라서 인터프리터는 이 경우에 일종의 가상머신이나 실행환경(runtime environment) 혹은 에뮬레이터 같은 것으로 이해할 수 있다.

하지만 역시나 이 정의를 따르면 Java는 컴파일 언어가 아닌 인터프리터 언어가 되고 만다.

언어의 속성이 아닌 구현체의 디자인

진짜 문제는 컴파일 언어냐 인터프리터 언어냐 하는 것이 언어 자체를 분류하거나 구분짓는 특성이 아니라는 것이다. 이것은 언어 구현의 문제이다. 초창기 프로그래밍 언어들의 역사에서 인터프리터 방식은 언어를 규정하는 특징이라기 보다는 목적과 용도에 따라 실제 작동에 대한 구현의 방식을 정한 것일 뿐이었다.

고전적인 의미의 인터프리터는 한줄씩 명령어를 입력하여 즉각적인 동시에 단계적으로 명령을 실행해 나가는 환경이 필요하여 그러한 방식으로 개발된다. 흔히 명령 프롬프트로 대표되는 cmd.exe나 bash 와 같은 쉘이 인터프리터 언어의 대표적인 형태이다. 파이썬은 인터랙티브 쉘의 형태로 실행되는(REPL이라고 한다.) 모드에서 이처럼 한 줄씩 코드를 입력해서 실행해볼 수 있기 때문에 흔히 인터프리터 언어로 분류되고 있다. 그럼 실제로 파이썬은 어떻게 돌아갈까?

그럼 파이썬은 어떻게 동작하나

어떤 언어가 컴파일러 방식으로 구현되어 있든, 인터프리터 방식으로 구현되어 있든 간에 CPU가 실행할 수 있는 명령은 모두 네이티브 코드인 CPU 인스트럭션 뿐이며, 개개의 CPU 인스트럭션은 아주 간단한 명령들이다. 따라서 어셈블리나 기계어로된 코드가 아닌 이상 대부분의 언어는 번역이 필요하고, 소스코드로부터 출발해서 기계어 코드가 실행되는 과정에서 가장 비용이 많이 드는 구간은 소스코드를 해석하는 과정이다.

따라서 어떤 언어가 프로덕션 레벨에서도 고전적 의미의 인터프리터처럼 매번 소스코드의 매 라인을 해석해서 실행해야 한다면 이 해석작업 때문에 전체 프로그램의 퍼포먼스에서 크게 손해를 보게된다. 이런 문제를 극복하기 위해 파이썬은 소스코드를 바이트로 코드로 컴파일한다음, 이 바이트코드를 해석기가 돌려주는 방식으로 실행한다. 이 때 말하는 바이트코드는 해석기가 사용하는 명령어 세트로 처리된 코드로 가상 머신을 위한 어셈블리 코드 정도로 이해하면 된다.

그러면 이 시점에서 다시 물어보자. 파이썬은 인터프리터 언어일까? 컴파일 언어일까? 이미 우리는 이 질문이 뭔가 잘못되었다는 점을 알고 있다. 앞서서 설명하지 않았던가? 인터프리터/컴파일의 구분은 언어의 특성이 아닌 구현의 문제이다. 표준 파이썬 구현체인 CPython의 인터프리터는 소스코드를 바이트코드로 컴파일 한 후 처리한다. 그럼 앞서 말했던 정의를 따르자면 컴파일 언어에 해당한다고 볼 수 있다. 심지어 pypy같은 파이썬 구현체는 JIT를 탑재하고 있기 때문에 실행 시간에 기계어 코드를 직접 생성하여 돌린다. 인터프리터가 존재한다고 해서 이걸 과연 인터프리터 언어라고 부를 수 있을까?

실존하는 파이썬 컴파일러

컴파일 방식이냐 인터프리터 방식이냐를 구현체로 구분해야 하는 극명한 예를 살펴보자. Cython 이라는 프로젝트가 있다. 이게 뭐냐면 다름 아닌 파이썬 소스코드를 C 언어로 컴파일하는 프로젝트이다. 여기에는 cdef 같은 추가적인 문법이 들어가기도 하는데, 어쨌든 파이썬 소스코드를 C 언어로 바꾼다는 것이다. 그렇다면 우리는 이를 통해 온전한 C 언어 소스를 얻을 수 있고, 그 이후의 과정에서 C컴파일러를 사용한다면 기계어로 된 실행파일을 생성할 수 있다는 이야기다.

누군가는 Cython은 cdef 같은 추가적인 문법을 사용하여 C 함수를 작성할 수 있는 물건이며, Cython전용 코드는 파이썬으로 실행할 수 없기 때문에 Python과 같은 언어로 볼 수 없다는 주장을 할 수 있다. 그렇다면 Nuitka는 어떤가? Nuitka는 순수 파이썬으로 만들어진 파이썬 컴파일러로 순수한 파이썬 코드를 C 코드로 완전하게 컴파일 할 수 있으며, C 컴파일러를 사용해서 실행 파일로 만들 수 있기까지 하다. 이 외에도 목적 언어가 바이트코드나 기계어가 아닌 범용언어가 되는 컴파일러들은 제법 많이 존재한다.

바이트코드? Java는 그럼 어디로 구분될까?

소스코드와 기계어 코드의 중간에 해당하는 바이트 코드를 사용하는 언어로는 대표적으로 Java가 있다. Java는 소스코드를 바이트코드로 컴파일 한 후 Java VM이라고 하는 실행환경에서 돌아간다. 흔히 하나의 소스코드를 수정 없이 (Java VM만 설치되어 있다면) 모든 플랫폼에서 실행할 수 있다는 것을 장점으로 내세운다.

이 것만 봐서는 Java 역시 인터프리터 언어가 아니냐고 말할 수도 있을 것이다. 틀린 말은 아니다. Java는 바이트코드로 컴파일되지만 VM은 인터프리터처럼 돌아갈 수 있다. 심지어 최근의 Java VM 구현은 JIT 컴파일러를 포함하여 일부 구간에서는 최적화를 수행하여 기계어 코드를 생성하여 바로 실행할 수 있기까지 하다.

자바와 자바스크립트는 인도와 인도네시아 만큼이나 다른 언어이지만, 자바 스크립트 역시 바이트코드로 컴파일되어 자바스크립트 VM에서 실행하는 방식으로 돌아간다. 그외에도 많은 언어들이 유연함과 성능을 모두 잡기 위해서 바이트코드를 사용하는 형태로 구현되고 있다. 뿐만 아니라 C언어를 자바스크립트로 컴파일하여 자바스크립트 VM에서 실행할 수 있게 하는 프로젝트도 있다.

참고 : 컴파일 방식에 대한 두 가지 관점

앞에서 컴파일 방식 언어에 대한 설명을 하면서 “실행시간 전에 컴파일을 한다”고 하였는데, 이러한 방식을 AOT(Ahead-Of-Time) 컴파일러라고 한다. 컴파일 언어하면 딱 떠오르는 C나 하스켈의 컴파일러들이 여기에 해당한다. 앞서 언급한 JIT 컴파일러는 프로그램 실행 중에 프로그램 실행 흐름을 관찰하여 더욱 최적화된 기계어 코드를 생성해서 프로그램 실행을 가속화하는 방식이다. Java VM에도 탑재가 되어 있으며, 파이썬으로 만든 파이썬 구현체인 pypy나 Julia와 같은 언어가 JIT 컴파일러를 사용한다. 이런 언어들은 특정한 유형의 문제에 대해서 ‘언어 중에서 성능 최고’라는 C보다도 더 빠른 실행 속도를 보이기도 한다.

결론

어떤 언어 자체를 인터프리터 언어/컴파일 언어로 구분하는 것은 잘못된 접근이다. 이 두 방식은 언어 구현에 있어서의 접근법이며, 기존에 존재하는 언어라도 새 구현체를 만들어서 그 작동 방식을 바꿀 수 있다. 최근 개발되는 언어들과 혹은 아주 오래되었지만 계속해서 발전해나가는 언어들은 표준 구현에서 두 가지 방식에서 장점만을 취하거나, 혹은 필요에 따라서는 두 가지 방식 모두를 포함하는 형태로 개발되고 있다.

그래서 저 질문에 답을 어떻게 해야할까? 일단 표준 파이썬 구현체는 인터프리터 안에 컴파일러를 내장하고 있다. 답은 각자 마음에 드는 걸로 정하시길.