들어가며
배포 파이프라인을 구현하는 과정에서 Java 계열과 node 계열 프로그래밍 언어들이 동작이 미묘하게 다르다는 것을 느꼈는데 (컴파일러와 인터프리터의 차이점으로 인한 것이었다) 관련 내용에 대해서 트러블슈팅하는 과정에서 얻은 인사이트를 요약하여 컴파일러와 인터프리터의 차이에 대해서 포스팅하려고 한다.
또한 전공 수업을 들으면서 C언어의 복잡한 컴파일 과정을 살펴보면서 생긴 개인적인 호기심을 해결하기 위해 조사하며, 함께 자바스크립트의 동작 원리도 좀 살펴보고 개인적인 호기심을 통해 조사한 내용을 읽기 쉽게 최대한 집약해 두었다. 차례차례 빌드, 컴파일, 인터프리터, 하이브리드에 대해서 알아보려고 한다.
빌드(Build)
먼저 빌드(Build)라는 것은 단어의 뜻에서 알 수 있듯이 뭔가를 짓는다는 것이다. 빌드는 소스 코드 파일을 컴퓨터나 휴대폰에서 실행할 수 있는 독립 소프트웨어 가공물로 변환하는 과정이나 그 결과물을 일컫는다고 한다. ( 위키피디아 )
우리가 소스 코드를 실행시키면 일반적으로 그 소스 코드 자체를 그대로 실행시키는 것이 아니다. 그 코드를 빌드함으로써 생기는 결과물을 실행하게 되는데, 이러한 빌드를 도와주는 것이 컴파일러와 인터프리터다. 컴파일러와 인터프리터에 의해 컴퓨터가 이해할 수 있는 수준으로 바뀌고, 이후 컴퓨터가 빌드된 코드를 실행시키는 것이다. 즉, 프로그래밍에서 빌드라고 하면 실행 가능한 파일로 만드는 과정을 의미한다.
앞서 말한 컴퓨터가 이해할 수 있는 수준은 어셈블리어(Assembly Language)라고 한다. 위키피디아에서 어셈블리어에 대한 설명을 참고해보자
In Computer Programming, assembly language often referred to simply as assembly and commonly abbreviated as ASM or asm, is any low-level programming language with a very strong correspondence between the instructions in the language and the architecturs's machine code instructions. - wikipedia
어셈블리어는 기계어와 1대 1로 대응되는 관계이다. 기계어는 이름만 들어도 알 수 있겠지만 정말 CPU의 언어이다. 실제로 0과 1로 이루어져 있는데, 예를 들면 다음과 같다.
10110000 01100001
이는 x86 계열 CPU의 기계어 명령이고, 이것을 어셈블리어로 옮겨 쓰면 다음과 같다.
mov al, 061h
어셈블리어도 굉장히 복잡한 언어지만, 그래도 기계어를 생각하면 나은 것 같다. 기계어의 문제는, CPU의 언어이기 때문에 CPU가 바뀔 때마다 기계어가 바뀌고, 이러한 기계어와 1대 1로 매칭되는 어셈블리어 또한 바뀐다는 것이다. 프로그래밍 언어가 매번 바뀐다니, 가슴 아픈 일이 아닐 수 없다.
이러한 문제점을 해결하기 위해서 컴파일(Compile)이라는 방법이 나온다. 좀 더 인간에게 가까운 수준이고 (어셈블리어는 너무 어렵고 저급 언어이다) 좀 더 통일된 언어체계가 필요하다. 따라서 C 언어와 같은 언어로 소스 코드를 작성하고, 이를 컴파일하여 어셈블리어로 빌드하는 방법을 사용하기 시작했다. 자, 이제 본격적으로 컴파일러 언어와 인터프리터 언어의 차이점에 대해서 알아보자.
컴파일(Compile)
컴파일은 소스코드 전체를 기계어로 번역하는 것이다.
이렇게 한 번에 어셈블리어로 번역되는 언어들은 C, C++, Go 언어들이 있는데, 대표적인 C 언어의 컴파일 과정을 사진으로 알아보자.
해당 과정을 살펴보면 .c 소스가 전처리 과정에 의해서 .i 소스로 바뀌고, 이후 C Compiler가 어셈블리어로 바꾸고, Assmembler가 다시 기계어로 바꾸게 된다. 각각의 과정에 대해 상술된 참고 자료는 밑에 남기도록 한다.
중요한 포인트는 C를 컴퓨터가 실행시키기 위해서 어셈블리어로 바뀌고, 또 기계어로 바뀐다는 점이다. 이렇게 컴파일 과정은 이루어진다. 다음으로 장단점을 요약하면 다음과 같다.
장점
- 좋은 퍼포먼스 : 미리 컴파일된 기계어 코드를 실행하므로 퍼포먼스가 좋은 편이다.
- 에러 검출 용이 : 컴파일러는 코드를 컴파일하는 과정에서 에러를 감지할 수 있다.
단점
- 개발 시간 증가 : 컴파일러 언어는 코드를 작성하고 컴파일하는 데 시간이 걸린다. 코드 수정 후에도 다시 컴파일해야 하기 때문에 개발 과정이 번거로울 수 있다.
- 플랫폼 종속적 : 컴파일 언어로 작성된 프로그램은 특정 플랫폼에 종속적이기 때문에 이식성이 떨어진다. ( window에서 gcc를 통해 c언어를 컴파일하면 a.exe 파일, mac에서는 a.out 파일이 나온다 )
- 큰 용량 : 컴파일된 실행 파일의 크기가 크고, 컴파일 과정에서 메모리 사용량을 많이 사용한다.
인터프리트(Interpret)
인터프리트는 소스코드를 한 줄씩 번역하면서 실행하는 것이다.
인터프리터 언어로는 대표적으로 Javascript나 Python, Ruby 등이 있다. 사실 정확히 꼭 집어서 100% 인터프리터 언어라고 보기 어렵긴 하다. 중간에 컴파일하는 과정이 없는 것은 아니기 때문인데, 이는 Javascript의 실행 과정을 보면서 설명하도록 하겠다.
자바스크립트(Javascript)란? 간단하게 객체 기반의 스크립트 프로그래밍 언어이며, 웹 브라우저 내에서 사용되는 프로그래밍 언어라고 보면 된다. 자바스크립트를 실행시키는 것을 도와주는 것을 V8 엔진이라고 부른다.
V8 엔진은 구글이 주도하여 C++로 작성된 고성능의 자바스크립트 및 웹 엔진이다. 따라서 Google Chrome에 내장되어 있다. 또한 HTML&CSS와 함께 웹에서 실행하는 것이 아닌, 자바스크립트 자체만을 실행시키게 도와주는 node.js 또한 V8 엔진을 통해서 자바스크립트를 실행하고 있다.
이러한 V8 엔진을 통한 자바스크립트의 실행은 다음과 같은 원리로 이루어진다.
C 언어의 컴파일 과정과 마찬가지로 개별적인 과정(Parser, Syntax Tree 등)이 동작하는 원리는 복잡하다. 간단하게 전체적인 과정을 보면 자바스크립트는 자바스크립트의 바이트코드로 먼저 컴파일된 후, 인터프리터를 통해서 한 줄씩 번역하면서 실행되게 됩니다. 앞서 100% 인터프리터 언어라고 부르기 어려운 이유는 컴파일하는 과정이 포함되어 있기 때문인데, 여기서는 인터프리터가 바이트코드를 기계어로 번역해 주면서 바로바로 실행해 주게 된다.
이렇게 인터프리터 언어는 컴퓨터 자체가 이해할 수 있는 수준이 아닌 바이트코드 수준으로 약간의 컴파일을 하기 때문에, 인터프리터를 도와주는 것이 필요하다. 실제로 Pycharm에서 파이썬을 실행시키려고 하면 먼저 인터프리터 설정을 해야 한다. 인터프리트 언어의 장단점은 아래와 같다.
장점
- 빠른 개발 : 인터프리터 언어는 소스 코드를 직접 실행하기 때문에 코드를 빠르게 수정하고 테스트할 수 있다.
- 좋은 이식성 : 일반적으로 플랫폼에 독립적이다. ( 단, 플랫폼에 맞는 인터프리터를 설치해야 함 )
- 동적 타입 시스템 : 대부분의 인터프리트 언어는 동적 타입 시스템을 사용하기 때문에 유연하고 간편하게 코드를 작성할 수 있다.
단점
- 낮은 성능 : 일반적으로 컴파일러 언어에 비해 퍼포먼스가 떨어진다.
- 에러 발견 지연 : 인터프리터는 컴파일 과정이 없기 때문에, 에러를 런타임에서 확인하게 된다.
하이브리드(Hybrid)
하이브리드 타입은 컴파일 방식과 인터프리트 방식을 합친 방법이다.
대표적인 하이브리드 언어는 Java가 있다. 다른 프로그래밍 언어를 사용하다가 자바를 사용하게 되었을 때 자바의 독자적인 프로그램 실행 체계에 조금 놀랐는데, 다음과 같은 원리로 자바는 동작한다.
Java가 여러 개의 CPU에서 같은 코드가 실행되기 위해서는 C/C++의 프로그램의 실행 구조와는 다른 방식이 필요하다. C/C++가 특정 CPU의 기계어 코드를 직접 생성하면, 이 기계어 코드가 메모리에 적재되어 바로 실행된다. 따라서 C/C++는 CPU가 달라지면 컴파일러가 달라져야 한다.
그러나 Java는 같은 코드를 사용하여 다른 CPU에서 실행되도록 하기 위해 직접 CPU의 기계어 코드를 생성해서는 안된다. 그 대신 자바는 바이트코드(Java bytecode)라는 것을 생성하고, 이것을 자바 가상 머신(JVM, Java Virtual Machine)이 해석하여 실행하는 구조다. JVM이 인터프리터가 되어 코드 해석 방식을 실행함으로써, 같은 바이트코드를 가지고 여러 가지의 CPU에서 실행이 가능해진다. JVM은 CPU와 직접적 관계가 있으므로 이것은 CPU의 기계어 코드로 이루어진다.
자바 소스코드는 *.java의 확장자를 가지게 된다. 이는 당연하게도 그 자체로 CPU가 인식할 수 없기 때문에 빌드 과정을 통해서 소스 코드를 실행시킬 수 있는 상태로 만들어야 한다.
자바 언어를 사용해 본 사람은 알겠지만, JDK(Java Developer Kit)를 설치해야 한다. JDK 안에 있는 javac.exe가 .java 확장자 파일을 .class 확장자 파일로 컴파일해주게 된다. 이렇게 생긴 .class 확장자 파일은 자바의 바이트 코드가 된다.
이렇게 생긴 .class 파일은 JVM이 인식할 수 있게 되는데, 위 사진과 같은 구조로 되어 있다. 이러한 구조를 가지고 있기 때문에 자바 언어를 하이브리드 언어라고 부르며, 앞서 컴파일 언어가 가졌던 플랫폼의 종속적이라는 단점을 극복할 수 있게 되었다.
마치며
이렇게 컴파일러와 인터프리터 언어에 대해서 다루어보았고, 두 개의 장점을 혼합한 자바에 대해서도 알아보았다. 개인적으로 자바를 별로 안 좋아하긴 하지만, 대체 왜 많은 곳에서 쓰이게 되는지 이해가 가긴 한다..
소프트웨어 기술이 대부분 그렇듯이 장단점이 있어, 상황에 따라 적절한 기술이 있는데 필자는 인터프리터 언어를 좀 더 좋아한다. 자바와 같은 언어가 굉장히 훌륭한 퍼포먼스를 보여주기는 하지만, 무겁기 때문에 개인적인 프로젝트를 자바로 하면 서버가 뻗는 상황도 종종 있다. ( AWS freetier에서 제공하는 t2.micro로 스프링부트를 돌렸을 때에는 뻗어버렸다 )
이러한 이유로 빠른 개발을 할 수 있는 자바스크립트를 좋아하는 편이긴 하나 사람의 취향이 있듯이 자바를 더 좋아하는 사람도 많은 것 같다.
꽤 예전 내용이긴 하지만 프로그래밍 언어의 퍼포먼스를 비교한 사진을 첨부한다. 또한 아래의 레퍼런스에 각각 언어의 빌드 과정이 자세히 나와있기 때문에 관심이 있다면 직접 언어의 빌드 과정을 살펴보는 것을 추천한다.
References
- 위키피디아 : 어셈블리어, C, V8, 소프트웨어 빌드, 자바
- Stranger's LAB : 프로그래밍 언어와 빌드 과정
- Evans Library : V8 엔진은 어떻게 내 코드를 실행하는 걸까?
- 알짜배기 프로그래머 : 알기 쉽게 정리한 JAVA의 컴파일 과정 및 JVM 메모리 구조, JVM GC
- attractivechaos : programming language benchmark