이번 문서의 목표: 이 파일을 다 읽으면 소스 코드가 실행되기까지 컴파일·인터프리트·하이브리드 방식이 각각 무엇을 하는지 순서대로 설명할 수 있고, 정적 링크와 동적 링크의 차이, 언어 설계(정적 타입인가 동적 타입인가)가 왜 특정 구현 방식과 어울리는지 근거를 들어 비교할 수 있다.
왜 구현 방식을 따로 배우는가
01편에서 “언어(language)“와 “구현(implementation)“은 다른 층위라고 정리했다. 언어는 문법과 의미의 약속이고, 구현은 그 약속을 실제 컴퓨터에서 동작하게 만드는 프로그램이다. 예를 들어 자바(Java)라는 언어는 하나지만, 그 언어를 실행하는 구현은 오라클 JDK, OpenJDK 등 여러 개가 있을 수 있다.
문제는 같은 언어라도 구현 방식에 따라 실행 속도, 오류를 발견하는 시점, 배포 방식이 완전히 달라진다는 점이다. 독학사 시험은 “이 언어는 왜 인터프리터 방식이 많이 쓰이는가”, “정적 링크와 동적 링크의 장단점은 무엇인가” 같은 문항을 통해 이 관계를 이해하는지 확인한다.
쉽게 말하면: 구현 방식은 “글로 쓴 요리 레시피(언어)를 실제로 어떤 도구와 순서로 요리해서(구현) 접시에 올리는가”의 문제다.
세 가지 구현 방식: 컴파일·인터프리트·하이브리드
순수 컴파일 방식
컴파일러(compiler)는 소스 코드 전체를 미리 읽어 기계어(machine code) 또는 어셈블리 코드로 통째로 번역해 실행 파일을 만드는 프로그램이다. 이렇게 만들어진 실행 파일은 컴파일 이후에는 원본 소스 코드 없이도 그 자체로 실행된다. C, C++, Fortran 같은 언어가 대표적으로 이 방식을 쓴다.
- 장점: 실행 시점에는 번역 과정이 전혀 없으므로 실행 속도가 빠르다. 컴파일 단계에서 문법 오류·형 오류(type error) 상당수를 미리 잡아낸다.
- 단점: 소스 코드를 고칠 때마다 전체를 다시 컴파일해야 한다(대형 프로젝트일수록 이 시간이 부담스럽다). 컴파일된 실행 파일은 특정 CPU 종류·운영체제에 맞춰져 있어 이식성(portability, 다른 환경으로 옮겨도 그대로 동작하는 성질)이 낮다.
순수 인터프리트 방식
인터프리터(interpreter)는 소스 코드를 한 줄(또는 한 문장) 읽을 때마다 그 자리에서 바로 해석해 실행하는 프로그램이다. 실행 파일을 미리 만들지 않고, 인터프리터 프로그램이 소스 코드를 옆에 끼고 실행 내내 동작한다.
- 장점: 컴파일 단계 없이 코드를 작성하자마자 바로 실행해 볼 수 있어 개발·디버깅 속도가 빠르다. 같은 인터프리터만 있으면 CPU 종류와 무관하게 실행되므로 이식성이 높다.
- 단점: 실행할 때마다 매번 해석 과정을 거치므로 컴파일 방식보다 실행 속도가 느리다. 문법 오류·형 오류는 그 줄이 실제로 실행되기 전까지 드러나지 않는 경우가 많다(런타임에 가서야 발견).
하이브리드(바이트코드) 방식
하이브리드 구현은 컴파일과 인터프리트를 절충한 방식이다. 소스 코드를 특정 CPU의 기계어가 아니라, 가상 머신(virtual machine, VM)이 이해하는 중간 형태인 바이트코드(bytecode)로 먼저 컴파일한다. 그 다음 이 바이트코드를 가상 머신이 한 명령씩 해석하며 실행한다. 자바(JVM 위에서 실행), 파이썬(Python, CPython의 .pyc 바이트코드)이 이 방식을 쓴다.
- 장점: 바이트코드 한 벌만 만들어 두면 가상 머신이 설치된 어떤 CPU·운영체제에서도 그대로 실행할 수 있어 이식성이 매우 높다(“한 번 작성하면 어디서나 실행”이라는 자바의 표어가 이 구조에서 나왔다). 소스 코드를 통째로 기계어로 번역하는 것보다 준비 단계가 가볍다.
- 단점: 가상 머신이 바이트코드를 해석하는 과정이 추가되므로 순수 컴파일 방식보다는 느리다. 다만 실행 중 자주 쓰이는 부분을 실제 기계어로 즉석에서 번역해 두는 JIT(Just-In-Time) 컴파일 기법을 얹어 이 느림을 상당 부분 보완하는 구현이 많다(예: 자바의 HotSpot VM).
세 방식을 표로 정리한다.
| 구분 | 순수 컴파일 | 순수 인터프리트 | 하이브리드(바이트코드) |
|---|---|---|---|
| 번역 시점 | 실행 전 한 번에 | 실행하면서 한 줄씩 | 실행 전 바이트코드로, 이후 VM이 해석 |
| 실행 속도 | 빠름 | 느림 | 중간(JIT로 보완 가능) |
| 오류 발견 시점 | 대부분 컴파일 시점 | 대부분 실행 시점 | 컴파일 시점 일부 + 실행 시점 일부 |
| 이식성 | 낮음(CPU·OS 종속) | 높음(인터프리터만 있으면 됨) | 매우 높음(VM만 있으면 됨) |
| 대표 언어 | C, C++, Fortran | 초기 베이식(BASIC), 셸 스크립트 | 자바, 파이썬 |
자주 틀리는 점: “인터프리터 방식은 컴파일이 아예 없다”고 오해하기 쉽지만, 많은 현대 인터프리터 언어도 내부적으로 소스 코드를 간단한 중간 표현으로 먼저 바꾼 뒤 그것을 해석한다. 시험에서 구분하는 핵심 기준은 “완성된 목적 코드를 CPU가 직접 실행하는가(컴파일), 아니면 매 실행마다 해석기가 개입하는가(인터프리트·하이브리드)“이다.
정적 링크와 동적 링크
컴파일된 프로그램은 대개 표준 입출력 함수처럼 이미 만들어져 있는 라이브러리(library) 코드를 가져다 쓴다. 이 라이브러리 코드를 최종 실행 파일에 언제, 어떻게 결합하느냐에 따라 링크 방식이 나뉜다.
정적 링크 (static linking)
컴파일이 끝난 뒤 링커(linker)가 프로그램이 사용하는 라이브러리 코드를 실행 파일 안에 통째로 복사해 넣는 방식이다. 완성된 실행 파일 하나에 필요한 모든 코드가 다 들어 있다.
- 장점: 실행 파일 하나만 있으면 실행에 필요한 라이브러리가 그 안에 이미 포함되어 있으므로, 다른 컴퓨터로 옮겨도 라이브러리를 따로 설치할 필요가 없다.
- 단점: 여러 프로그램이 같은 라이브러리를 쓰더라도 각자 자기 실행 파일 안에 똑같은 코드를 복사해 가지므로 디스크 공간과 메모리를 낭비한다. 라이브러리에 오류가 발견되어 새 버전이 나와도, 그 라이브러리를 쓰는 프로그램을 전부 다시 컴파일해야 한다.
동적 링크 (dynamic linking)
실행 파일 안에는 “이 라이브러리의 이 함수를 쓴다”는 참조 정보만 남겨 두고, 실제 라이브러리 코드는 프로그램을 실행하는 순간(또는 그 함수를 처음 호출하는 순간) 운영체제가 메모리에 올려 연결하는 방식이다. 이런 라이브러리를 공유 라이브러리(shared library, 윈도우에서는 DLL, 리눅스에서는 .so 파일)라 부른다.
- 장점: 여러 프로그램이 같은 공유 라이브러리를 메모리에 한 벌만 올려놓고 함께 쓸 수 있어 메모리를 절약한다. 라이브러리만 새 버전으로 교체하면 그 라이브러리를 쓰는 프로그램들을 다시 컴파일하지 않아도 개선된 기능을 바로 쓸 수 있다.
- 단점: 실행하려는 컴퓨터에 필요한 공유 라이브러리가 설치되어 있지 않거나 버전이 맞지 않으면 실행에 실패한다(“라이브러리를 찾을 수 없습니다” 오류). 실행 시점에 라이브러리를 찾아 연결하는 과정이 추가되어 정적 링크보다 프로그램을 처음 시작하는 시간이 약간 더 걸릴 수 있다.
| 구분 | 정적 링크 | 동적 링크 |
|---|---|---|
| 라이브러리 코드 위치 | 실행 파일 내부에 복사 | 실행 파일 밖, 실행 시점에 연결 |
| 실행 파일 크기 | 큼 | 작음 |
| 메모리 공유 | 불가(프로그램마다 복사본) | 가능(여러 프로그램이 한 벌 공유) |
| 배포 편의성 | 실행 파일 하나로 충분 | 라이브러리도 함께 설치되어 있어야 함 |
| 라이브러리 갱신 | 재컴파일 필요 | 라이브러리만 교체하면 됨(호환되는 경우) |
비유: 정적 링크는 요리책 레시피마다 필요한 재료 손질법을 처음부터 끝까지 통째로 다시 적어 넣는 것이고, 동적 링크는 “이 손질법은 부록 3번을 보라”고 적어 두고 필요할 때 공용 부록을 펼쳐 보는 것과 같다. 부록(공유 라이브러리)이 없으면 그 레시피는 완성되지 않는다.
런타임 라이브러리
컴파일이나 링크로 다 해결되지 않는, 프로그램이 실행되는 동안(runtime, 런타임) 계속 필요한 서비스도 있다. 이런 서비스를 모아 둔 것이 런타임 라이브러리(runtime library)다. 예를 들어 다음과 같은 일을 한다.
- 자동 메모리 관리를 지원하는 언어라면, 더 이상 쓰이지 않는 메모리를 찾아 회수하는 가비지 컬렉터(garbage collector)를 실행 내내 동작시킨다.
- 배열 인덱스가 범위를 벗어났는지, 0으로 나누기를 시도했는지 같은 실행 시점 오류 검사를 수행한다.
- 15편에서 다룬 활성화 레코드(activation record)를 실제 실행 스택 위에 쌓고 걷어내는 하위프로그램 호출·복귀 절차를 뒷받침한다.
- 하이브리드 구현이라면 가상 머신 자체(바이트코드를 해석하는 프로그램)가 런타임 라이브러리의 핵심을 이룬다.
런타임 라이브러리는 사용자가 직접 호출(call)하는 함수 모음이 아니라, 언어가 약속한 의미(정적/동적 의미, 08편)를 실제로 지켜 내기 위해 구현이 뒤에서 항상 켜 두는 지원 장치라는 점이 핵심이다. 자동 메모리 관리를 언어 명세에서 약속했다면, 그 약속을 실제로 지키는 것은 런타임 라이브러리(가비지 컬렉터)의 몫이다.
언어 설계와 구현 방식의 관계
언어를 설계할 때 정한 성격은 그 언어가 어떤 구현 방식과 잘 맞는지에 영향을 준다. 이 관계를 뒤집어 “왜 이 언어는 이런 방식으로 주로 구현되는가”를 묻는 문항이 자주 나온다.
- 정적 타입 언어(형이 컴파일 시점에 정해지는 언어, 10편)는 컴파일 단계에서 형 검사를 이미 끝낼 수 있으므로 순수 컴파일 방식과 잘 맞는다. C, C++가 그 예다.
- 동적 타입 언어(형이 실행 시점에 정해지는 언어)는 변수의 형이 실행 중에 바뀔 수 있어 컴파일 시점에 모든 검사를 끝내기 어렵다. 그래서 실행하며 검사하는 인터프리트·하이브리드 방식과 잘 맞는다. 파이썬, 자바스크립트(JavaScript)가 그 예다.
- 플랫폼 독립성을 설계 목표로 강하게 내세운 언어는 하이브리드(바이트코드+가상 머신) 방식을 택하는 경향이 크다. 자바가 대표적이다. 특정 CPU의 기계어로 바로 컴파일해 버리면 그 CPU에서만 실행되어, “어디서나 실행”이라는 목표와 어긋나기 때문이다.
- 짧은 스크립트를 빠르게 짜고 즉시 실행 결과를 보고 싶은 용도로 설계된 스크립팅 언어는 컴파일 준비 과정 없이 곧바로 실행되는 인터프리트 방식이 개발 흐름에 잘 맞는다.
자주 틀리는 점: 이 관계는 “그렇게 설계되면 반드시 그 방식으로만 구현해야 한다”는 강제 규칙이 아니라 경향이다. 실제로 자바스크립트도 V8 엔진처럼 JIT 컴파일을 적극적으로 쓰는 구현이 있고, C도 인터프리터로 구현한 사례(예: 교육용 C 인터프리터)가 있다. 시험 문장이 “반드시”, “무조건”으로 단정하면 오히려 옳지 않은 진술일 가능성을 의심해야 한다.
핵심 정리
- 컴파일은 실행 전 전체를 기계어로 번역해 빠르지만 이식성이 낮고, 인터프리트는 한 줄씩 해석해 이식성은 높지만 느리며, 하이브리드(바이트코드+가상 머신)는 둘을 절충해 높은 이식성을 유지하면서 JIT 컴파일로 속도를 보완한다.
- 정적 링크는 라이브러리 코드를 실행 파일에 통째로 넣어 배포는 간단하지만 공간을 낭비하고, 동적 링크는 실행 시점에 공유 라이브러리를 연결해 메모리를 절약하지만 라이브러리 설치 여부에 의존한다.
- 런타임 라이브러리는 가비지 컬렉션, 실행 시점 오류 검사, 활성화 레코드 관리처럼 언어가 약속한 의미를 실행 내내 뒷받침하는 지원 장치다.
- 정적 타입·컴파일, 동적 타입·인터프리트/하이브리드, 플랫폼 독립성·하이브리드라는 경향이 있지만 절대 규칙은 아니다.