최근에 마인크래프트를 팀원분들과 하게 되었다. 올해 새롭게 출시된 엄청나다던 맥북 m4 pro를 구매했기 때문에 쉐이더 모드라는 마인크래프트의 그래픽을 엄청나게 좋게 만들어주는 모드를 설치하려고 했다. 위 보이는 사진은 쉐이더 모드를 설치하기 위한 인스톨러이다.
근데 이게 웬걸, 찾아보니까 MacOS에서 설치하는 방법이 없었다! 절망감에 휩싸이며 포기하려던 찰나, 쉐이더를 설정하는 파일을 봤더니 그 인스톨러의 확장자가 .jar로 끝나는 것을 봤다. 내가 왜 MacOS에서 설치하는 방법이 없었고, 스스로 도전해보지 않았냐면 ARM64 버전 인스톨러가 없기 때문에 '설마 MacOS는 지원을 안 하는 건가?'라고 생각한 것이었다.
그도 그럴게, 게임이라는 카테고리에 있어서는 MacOS는 거의 외딴 섬 취급이기 때문에 Mac은 지원을 안 하나보다... 이런 생각이었다. 하지만 마인크래프트가 Java 기반이고, .jar 확장자의 인스톨러를 사용하니까, 그냥 java 깔려있으면 되는 건가? 싶어서 해봤더니 맙소사. 정말로 그냥 된다. CPU가 어떤 계열이던지 간에 그냥 자바만 있으면 된다. 그때 문득 스치는 자바의 탄생 철학 'Write Once, Run Anywhere'가 떠올랐고, 자바를 이렇게까지 좋아한 적은 처음이다. 그 동안 자바를 미워했던 게 후회되는 순간이었다.
아무튼 이번 포스팅에서는 인터프리터와 컴파일러의 차이에 대해서 포스팅을 할 예정이다. 일반적으로 어떤 언어는 컴파일러, 어떤 언어는 인터프리터라고 단정을 짓는 것이 아니라, 인터프리터와 컴파일러 언어를 스펙트럼으로서 표현하고자 한다. 이 철학에 앞서 먼저 이전 포스팅을 언급할 수밖에 없다.
- ARM vs AMD(x86) 아키텍처 : https://marsboy.tistory.com/91
ARM vs AMD(x86) 아키텍처
최근에 소프트웨어를 깔면서, 그리고 docker image를 빌드하면서 자꾸 ARM과 AMD을 설정해줘야 하는 부분이 있었고, 가끔씩 뭐가 뭔지 까먹는다. 둘이 비슷하기도 하고, 실수해서 잘못 입력하기도 한
marsboy.tistory.com
일반론적으로는, 빌드된 결과물이 바이너리 파일이면, 이것은 컴파일러 언어다! 라고는 한다. C언어나 Go언어가 그렇다. 하지만 나는 바이너리 파일이 기계어로 바뀌는 과정 까지도 인터프리팅이라고 보고 이야기를 전개하려고 한다. 왜냐하면, CPU 아키텍처에 따라서 바이너리에서 기계어로 바뀌는 과정이 다르기 때문이다.
본론
컴파일 언어와 인터프리터 언어
컴파일 언어는 소스 코드를 실행하기 전에 미리 기계어로 변역(컴파일)하는 언어이다. 컴파일러가 전체 코드를 한 번에 분석하고 최적화해서 실행 파일을 만들어낸다. 대표적으로는 C, C++, Rust, Go 같은 언어가 대표적이다. 실행 속도가 빠르고 런타임 에러를 미리 잡을 수 있지만, 코드를 수정할 때마다 다시 컴파일해야 하는 번거로움이 있다.
인터프리터 언어는 소스 코드를 한 줄씩 읽어가면서 실행하는 언어이다. 대표적으로 Python, Javascript, Ruby 같은 언어들이 이 방식을 사용한다. 코드를 즉시 실행할 수 있어서 개발과 테스트가 편리하지만, 실행할 때마다 코드를 해석해야해서 일반적으로 컴파일 언어보다 느리다.
Java?
자바는 그렇다면 무엇일까? Java는 .jar 확장자로 끝나는 자바 바이트코드라는 수준까지 컴파일된 후에, JVM이 이를 인터프리터 방식으로 실행해준다. 이는 컴파일 언어라고 볼 수 있을까? 아니면 인터프리터 언어라고 볼 수 있을까?
추가적으로, python도 사실 내부적으로는 바이트코드로 컴파일된다. 또한 JIT(Just-In-Time) 컴파일 같은 기술도 있다. .py 확장자로 끝나는 소스 코드를, python이 아니라 pypy라는 런타임으로 실행하면 JIT 방식을 통해서 부분적으로 컴파일을 실행해 주며, 속도가 훨씬 빨라진다.
백준에서 python으로 돌리면 시간초과가 나는 소스 코드를 pypy로 돌리면 가끔씩 통과되는 경우가 있다. 이 외에도 node에 대해서 깊게 설명한 적이 있었는데, node 또한 V8 엔진을 통해서 JIT 방식을 사용한다. 처음에는 인터프리팅하다가 자주 쓰이는 코드는 JIT 컴파일하는 것이다.
바이너리 파일 -> 기계어 실행 과정
인터프리터는 소스 코드를 하나하나씩 읽어서 실행한다. 컴파일러는 말 그대로 실행 가능한 바이너리 파일을 만들어낼 뿐, 실행하지 않는다. 바이너리 파일이 실제로 실행되는 과정을 살펴보면, 이 과정을 인터프리팅이라고 볼 수 있지 않을까? 하는 견해도 받아들일 수 있을 수도 있다.
일단, 바이너리 파일이 실행되는 과정을 알아보자.
1. 로더(Loader)의 역할
- OS가 바이너리 파일을 메모리에 적재
- ELF(Linux), PE(Windows), Mach-O(macOS) 등의 실행 파일 포맷 해석
- 메모리 주소 재배치(relocation), 동적 링킹 수행
2. CPU 레벨에서의 해석
- 바이너리 명령어들이 CPU의 명령어 디코더를 거쳐 마이크로 연산으로 분해
- 예를 들어, ADD EAX, EBX 같은 x86 명령어는: 바이너리 01 D8 (2바이트) 가 된다. 이를 명령어 디코더가 해석해서 ALU에서 실제 덧셈을 수행
3. 마이크로아키텍처 레벨
- 복잡한 CISC 명령어(x86)는 내부적으로 여러 개의 RISC 스타일 마이크로 연산으로 변환
- Intel이나 AMD CPU 내부에서 실시간으로 일어나는 변환
바이너리 파일이 기계어로 변환되는 과정에서 여러 단계의 "해석" 과정이 있다는 걸 알 수 있다. 이 관점에서 보면 C 컴파일러가 만든 바이너리도 결국 OS 로더와 CPU에 의해 "인터프리팅" 된다고 생각할 수 있다.
이렇게 이야기를 꺼내는 이유는 컴파일러 언어, 인터프리터 언어 이렇게 이분법적으로 정의하지 않고, 컴파일과 인터프리팅을 스펙트럼으로 생각하면, Java의 위치와 인터프리터 언어들이 꾸준히 발전하면서 점점 컴파일러 언어와 닮아가려고 하는 것을 이해할 수 있다.
인터프리터와 컴파일러 스펙트럼
컴파일러에 가깝게 딱 붙어있는 언어들도 위의 CPU 레벨의 기계어로 전환되는 과정을 생각하면 약간의 인터프리팅 영역이 있다. 그리고 Python과 같은 언어들도 내부적으로 바이트코드로 변환하는 부분이 있지만, 다른 언어들에 비해서 크지 않다. 따라서 인터프리터에 가깝게 딱 붙어있는 언어들도 마찬가지로 약간의 컴파일 영역이 있다고 볼 수 있다.
왼쪽부터 왜 그 위치에 있는지 대표적인 언어들로 설명하면 아래와 같다.
- Python : .py 확장자를 가진 파이썬 소스 코드를 python3으로 실행하면 소스 코드를 그대로 실행해 준다.
- Javascript : .js 확장자를 가진 자바스크립트 소스 코드를 node로 실행하면 소스 코드를 그대로 실행하지만, 처음부터 적극적인 JIT 컴파일 전략과, 코드 실행 즉시 최적화 등 JIT가 크게 발전했기 때문에 컴파일의 영역이 파이썬보다 크다.
- Java/C# : Java와 C# 모두 플랫폼 독립적인 중간 표현(자바 바이트코드, IL)등으로 바뀌며, JVM 및 CLR이라는 머신 위에서 인터프리팅 된다.
- C/C++ : OS에서 바로 실행이 가능한 바이너리 파일 수준으로 컴파일되며, 나머지는 OS에 따라서 해석되어 기계어를 제어한다.
스펙트럼을 나눠보자면 위와 같이 표현을 할 수 있다.
그렇다면 pypy는 어느 위치 정도에 있을까? 위에 따르면 아마 Python과 Javascript 사이에 있을 것이다. 적극적인 JIT를 사용하면서 점점 오른쪽으로 가게 되며, 성능도 좋아진다. 반대로 왼쪽에 있는 인터프리터 언어일수록 쓰기 편하며, 빠르게 개발할 수 있다. node를 통해서 띄운 서버를 --watch 옵션을 걸어서 소스 코드가 바뀔 때마다 띄울 수 있게 만들면, 순식간에 바뀐 코드로 테스트를 할 수 있다.
이 외에도 Bash는 조금 예외로 맨 왼쪽에 있다. 사실 넣을까 말까, 아니면 Shell 기반 언어인 Perl을 대신 넣을까 했지만, 셸 스크립트나 명령어 해석기도 넓은 의미에서 인터프리터의 한 형태라고 생각해서 추가했다. 터미널에 python 혹은 node라고 입력하면 바로 파이썬과 노드 인터프리터가 실행되는데, 이와 셸이 비슷하다고 판단해서 맨 왼쪽에 추가했다.
마치며
최근에 주변 주니어 개발자에게 빌드가 뭔가부터 시작해서 컴파일과 인터프리터 언어의 차이점에 대해서 설명할 일도 생겼고, 추가적으로 마인크래프트를 하면서 처음으로 자바에 감격을 받아서 포스팅을 해야겠다고 생각하고 빠르게 작성을 해두었다. 개인적으로 프로그래밍 언어를 컴파일러/인터프리터로 이분법적으로 나누는 것보다는, 스펙트럼 관점에서 바라보는 것이 현실을 더 잘 반영한다는 것이 아닌가 하는 생각이다.
마인크래프트 쉐이더 설치 경험에서 느꼈던 Java의 매력도 결국 이 스펙트럼 상에서 적절한 위치를 차지하고 있기 때문이다. 바이트코드로 컴파일되어 성능을 확보하면서도, JVM 위에서 플랫폼 독립성을 유지하는 것 말이다.
앞으로 새로운 언어들이 등장하거나 기존 언어들이 발전할 때도, 이분법적으로 생각하는 것보다 스펙트럼을 생각해 보면 좀 더 깊은 이해가 가능할 것 같다고 생각하여 내용을 정리해 두었다.
'CS > Computer Architecture' 카테고리의 다른 글
ARM vs AMD(x86) 아키텍처 (3) | 2025.06.07 |
---|---|
유니코드(Unicode) 이야기와 UTF-8 (0) | 2025.05.07 |
빌드(build) 그리고 컴파일러와 인터프리터 (1) | 2023.10.15 |