
우리는 "개발을 한다, 개발을 배운다"는 것을 프로그래밍 언어를 배운다는 것과 동일시하기도 한다.
하지만 프로그래밍 언어란, 프로그래머가 컴퓨터에 명령을 내리는 도구일 뿐.
프로그래머가 코드를 작성할 때는 어떤 일이 일어나며, 우리는 어떤 과정을 거치면서 개발을 하고 있는 걸까?
- 프로그래머가 프로그래밍 언어로 코드를 작성하면,
- 컴파일러는 코드를 기반으로 실행 파일을 생성(build)한다.
- 이 실행 파일을 실행한다.
프로그래밍 언어는 매우 다양한데, 컴파일러는 프로그래밍 언어를 어떻게 인식하여 실행 파일을 만들까? 실행 파일은 어떻게 실행될 수 있을까?
이것이 이 책이 던진 질문이며, 우리가 알아가게 될 내용이다.
프로그래밍 언어의 탄생
CPU
: CPU는 똑똑한 바보?
간단한 스위치들의 조합으로 복잡한 불 논리(boolean logic)를 표현할 수 있다는 사실을 기반으로 만들어졌다.
CPU는 간단한 개폐만 이해할 수 있으며 컴퓨터는 이를 숫자 0과 1로 표현한다.
이를 이용하여 CPU가 할 수 있는 일은
- 데이터를 한 곳에서 다른 곳으로 옮기고,
- 간단히 연산을 한 후
- 다시 그 데이터를 또 다른 곳으로 옮기기
이렇게 매우 간단하고 쉬운 작업만 할 수 있을 뿐이지만 이러한 이유로 CPU는 다른 어떤 것과도 비교할 수 없는 장점을 지니는데, 바로 엄청나게 빠르다는 것이다. 이 엄청나게 빠른 작업 속도는 간단한 작업밖에 할 수 없다는 단점을 상쇄한다.
프로그래머(인간)와 컴퓨터(CPU)는 프로그래밍 언어를 통해 소통할 수 있다.
현재의 프로그래밍 언어는 인간의 언어에 가깝지만, 최초의 의사소통에서는 프로그래머가 CPU의 언어로 말해야 했다. 프로그래머는 천공 카드를 이용하고, 직접 0과 1로 구성된 명령어를 작성하여 컴퓨터 작업을 제어했다. 이것이 바로 최초의 코드(code)이고, 소스(source)인 것이다.


마치 점과 선으로만 소통하는 모스 부호처럼, 이처럼 초기의 프로그래머들은 컴퓨터가 알아들을 수 있는 언어로 직접 소통하였다.

그리고 그 후,,,
프로그래머는 인간의 언어로 컴퓨터와 소통하고 싶어하였고, CPU는 가산 명령어, 점프 명령어 등 겨우 몇 가지 명령어만으로 실행되고 있다는 사실을 발견했다. 이에 0과 1로 구성했던 코드를 인간이 읽고 이해할 수 있는 단어로 대응시키게 된다.
어셈블리어(assembly language)
: 기계어와 일대일 대응시켜 인간이 인식할 수 있는 단어로 변환한 프로그래밍의 저급 언어

(어셈블리어로 시스템 프로그래밍을 꽤 하는 것 같다. 다양한 환경에서 저수준 프로그래밍을 할 수 있는 능력..?)
아직 어셈블리어는 우리가 일상적으로 사용하는 언어 형식과는 많은 차이가 있으며 여전히 저수준 언어(low-level language)이다.
저수준 언어 : 기계어, 어셈블리어
고수준 언어 : C, JAVA, Python
저수준 언어로 어떠한 상황을 코드로 표현한다면, 세부 사항을 구체적으로 모두 나열해야 한다.
"물 한잔 가져다 줘"
↓
물의 위치를 찾는다, 몸의 방향을 튼다, 오른쪽 다리를 내딛는다, 멈춘다, 왼쪽 다리를 내딛는다, ... , 물컵을 오른손으로 든다, ... , 물이 가득 차지 않았다면, 기다린다, 물이 가득 찼다면, 수도꼭지를 잠근다, 어쩌구 저쩌구,,,,,
고급 프로그래밍 언어의 등장
CPU는 실제로 단순한 표현의 작업만 가능하며 인간의 추상적 언어를 변환할 수 없어 저수준 계층의 세부 사항과 고수준 계층의 추상화 사이의 간극을 자동으로 좁힐 수 있는 방법을 찾아야 했다.
대부분의 코드가 단도직입적인 문장이며, 저수준 언어의 세부 사항이 규칙 또는 패턴으로 가득하다는 것을 발견하고 고급 프로그래밍 언어가 시작된다.
- 특정 작업을 수행하는 단도직입적인 명령어 = 문(statement)
- 개별적인 세부 사항만 차이가 있다 = 매개변수 (parameter)
≫ 이를 매개변수를 제외하고 하나의 코드로 묶어 함수가 탄생한다.
- 특정 상황에 따라 어떤 명령어를 실행할 지 선택 = 만약 ... 라면 ...하고, 그렇지 않다면 ... 한다.(if - else)
- 일정한 명령문을 반복 = ... 동안 ... 한다. (while)
≫ 조건문, 순환문 등 여러 가지 패턴이 있다.
그렇다면 이런 문장들이 계속 이어진다면? 함수 안에 또 다른 함수가, 조건문 안에 순환문이, 또 다른 조건문이 끝없이 이어질 수 있으며 또 그 안에서 함수가 호출될 수 있는데, 이것은 상당히 복잡해 보이지만 결국 수학의 재귀수열처럼 단계 안에 단계가 중첩되는 '재귀'라는 개념으로 설명될 수 있다.
이 재귀의 개념은 아래의 간결한 문장 몇 가지로 표현할 수 있다. 이 문장들을 우리는 구문(syntax)이라고 한다.
if: if expr statement else statement
for: while expr statement
statement: if | for | statement
이제 구문에 따라 코드를 작성할 수 있게 되었지만, 이 구문은 결국 인간의 언어로 표현된 문자열에 불과하다. 이 재귀 구문으로 표현된 문자열을 컴퓨터가 인식할 수 있도록 재귀 구문에 따라 작성된 코드를 트리(tree) 구조로 표현할 수 있다.
이것이 구문 트리(syntax tree) 이다.

컴파일러와 인터프리터
이 구문 트리를 번역하는 방식에 따라 컴파일러 언어와 인터프리터 언어로 구분할 수 있다.
컴파일러(compiler)
: 고급 프로그래밍 언어를 기계가 이해할 수 있는 저급 언어로 번역시키는 프로그램
: 고수준 → 저수준
: 정적 프로그램(변수 타입 변경 X)
컴퓨터는 프로그래밍 언어를 처리할 때 구문 정의에 따라 트리 형태로 코드를 구성하여 맨 끝의 리프 노드(leap node)의 표현이 매우 간단해지기 때문에 기계어로 번역이 가능해진다. 이렇게 리프 노드 → 부모 노드 차례로 번역하는 방식으로 전체 구문을 기계어로 번역할 수 있는데, 이 작업을 담당하는 프로그램이 컴파일러(compiler)이다.
프로그래머는 인간의 언어로 코드를 작성하고, 컴파일러는 구문 트리 방식을 이용해 이를 CPU가 이해할 수 있는 기계 명령어로 번역한다.
※ 해당 언어: C, C++, Java 등
(Java는 컴파일 과정은 컴파일 언어의 특징을 가지지만 이후 실행 과정에서는 인터프리터 언어의 특징을 가지고 있어 하이브리드 언어라고도 불린다.)
- 컴파일러 언어의 장점
1. 성능이 우수
2. 컴파일 시 오류 검출하여 런타임 전에 일부 오류를 미리 방지
3. 코드의 로직이 직접 노출되지 않음
- 컴파일러 언어의 단점
1. 컴파일 시간이 길다.
2. 메모리 사용량이 크다.(실행 파일 저장)
3. 제한된 이식성(형식이 다른 CPU는 각각의 고유한 언어가 있어, 하나의 코드를 각기 다른 플랫폼에서 실행할 수 없다.)
인터프리터(interpreter)
: 프로그래밍 언어의 소스코드(고급언어)를 바로 실행하는 컴퓨터 프로그램 또는 환경
: 고수준 → 중간수준
: 동적 프로그램(변수 타입 변경 O)
하나의 코드로 어디서나 그 코드를 실행할 수 있도록 표준 명령어 집합을 정의해서 각 CPU의 기계 명령어에 상응하는 시뮬레이션 프로그램을 만들게 되는데, 이것을 인터프리터(interpreter)라고 한다.
인터프리터 언어는 컴파일러 언어의 단점(컴파일 시간, 제한된 이식성)을 보완하기 위해 만든 언어로, 성능은 조금 떨어진다.
소스코드(고급 언어)를 한 줄씩 바로 바이트 언어(중간 언어)로 번역한 후 실행하는데, 이 중간 언어를 실행하기 위한 소프트웨어나 하드웨어 기반의 가상적 실행 환경이 바로 가상 머신(virtual machine)이다. 보통 가상 머신과 인터프리터를 비슷한 개념으로 통칭한다.
※ 해당 언어: Python, 루비 등
- 인터프리터 언어의 장점
1. 플랫폼 독립성(높은 이식성)
2. 빠른 개발 속도
※ JVM은?
JIT(Just in time) compile: 소스 코드를 흐름에 맞게 읽어가며 중간 언어로 번역 -> 번역 된 중간 언어를 메모리에 적재 -> 실행
JVM은 JIT compiler를 이용하여 자바 바이트 코드로 먼저 컴파일 한 뒤 Interpreter를 이용하여 읽어 들이기 때문에 Interpreter 실행 속도를 개선하였고, 또한 Interpreter 방식으로 기계어 번역을 하기 때문에 디버깅이 가능하다. 또한 JIT Compiler는 한 번 컴파일 된 실행파일을 캐싱하여 저장하기 때문에 자주 호출되는 경우 캐시를 가져와 유리하게 성능을 가져갈 수도 있다.
- 인터프리터 언어의 단점
1. 실행 속도가 상대적으로 느림
2. 코드 보안성이 취약
3. 전체적인 프로그램 최적화가 어려움(실행하며 한 줄씩 동적으로 해석하기 때문)
이렇게 프로그래밍 언어가 탄생하게 되었다.
세상의 모든 프로그래밍 언어는 특정 구문에 따라 작성된다.
컴파일러는 언어 구문에 따라 코드 구문을 분석하여 구문 트리로 만들고, 이 구문 트리를 C/C++언어처럼 기계 명령어로 번역하여 CPU로 직접 넘기거나 Java처럼 바이트 코드로 변환한 후 가상 머신으로 넘겨 실행한다.
경우에 따라서는 직접 저수준 계층의 세부 사항을 제어할 수 있어야 하는데, 이러한 운영 체제 중 일부분은 어셈블리어로 작성되기도 한다.
'CS > 밑바닥' 카테고리의 다른 글
| 동기와 비동기, 블로킹과 논블로킹 (0) | 2024.06.26 |
|---|---|
| 콜백 함수와 비동기 프로그래밍 (0) | 2024.06.26 |
| 코루틴(Coroutine) - 스레드보다 가벼운 멀티 태스킹 (0) | 2024.06.18 |
| 프로그램이 실행될 때 - 운영 체제, 프로세스, 스레드 (0) | 2024.06.18 |
| (~ing)프로그래머가 코드를 작성할 때 일어나는 일 (2) - 컴파일러와 링커 (1) | 2024.06.15 |