Q: 우리가 idle에 작성한 소스코드는 컴퓨터가 바로 알아먹을 수 있을까?
우리가 작성하는 코드에 사용되는 언어는 **“고급 언어”**이다.
**고급 언어**의 목적은 개발자가 읽고 쓰기 쉽게 되어있음.
고급 언어로 이루어진 코드는 컴퓨터가 이를 실행할 수 있도록 변환되어야함.
컴퓨터가 읽고 쓸 수 있도록 된 언어가 “저급 언어” 즉 저급 언어로 변환해야 컴퓨터가 이를 실행할 수 있음.
**저급 언어**는 기계어와 어셈블리어로 구분됨
- 기계어 : 이진수로 표현된 언어
- 어셈블리어 : 외계어 같은 기계어를 사람들이 읽기 편한 형태로 번역한 저급 언어
A : 추상화 수준이 다르다.
어셈블리어는 기계어에 대해 1대1로 번역된 것이어서 기계어의 행동을 대변한다.
반면 고급언어의 for문 와 같은 형태는 java에서의, 파이썬에서의 표현 형태가 다르지만 반복한다는 논리는 같다.
그러므로 기계어:고급언어는 1:N 구조이다.
또한 고급언어는 기계어로의 변환 과정에서 단순한 코드임에도 수많은 기계어가 나타날 수 있다.
예를 들면 단순히 덧셈하는 작업이지만 기계어로는 CPU를 가동할 목적의 여러 기계어 명령어가 나올 수 있는데
기계어를 해석하기 쉽게 만든 것이 어셈블리어이고
기계어 모음을 고급언어까지 가게 되면 결국 a = b + 3와 같은 간단한 작업까지 추상화될 수 있는 것이다.
- 고급 언어 : 출금한다.
- 기계어 :
0101011 1101010 10110101… - 어셈블리어 : 잔금조회, 잔금 - 100, 저장 ….
고급언어가 저급언어로 바뀌는 방식은 간단하게는
“컴파일”, “인터프리트” 방식 두 가지로 나뉜다.
- 컴파일 언어는 소스코드가 컴파일러를 거쳐 전체 소스 코드를 저급언어로 바꾸고, 이 저급언어를 실행한다.
- 인터프리터 언어는 소스코드가 "한줄 씩" 인터프리터를 거쳐 저급언어로 바꾸며 동시에 실행한다.
즉,
- 컴파일 언어는 한 뭉텅이를 통째로 번역하여 저급언어로 실행
- 인터프리터 언어는 한줄 한줄 번역 -> 실행
하지만 모든 언어는 컴파일 or 인터프리터로 명확하게 쪼개지지 않는다.
자바의 변환 방식이 이를 대표적으로 방증한다.
자바의 컴파일러는 javac로 JDK에 포함되어 있는 컴파일러이다.
하지만 이 컴파일러는 기계어로의 컴파일러가 아니다. 더 알아보자.
우리가 적은 소스코드는 javac가 바이트코드로 컴파일하여 1차 번역된 바이트코드가 담긴 .class 파일을 생성한다.
(다시 말하지만 이 바이트 코드는 기계어가 아니다!)
이 바이트코드는 JVM이 읽을 수 있는 중간 단계 언어이며
최종적으로는 이 바이트코드를 JVM이 기계어로 변환(2차 번역) 하여 실행하는 구조이다.
소스 코드를 JVM이 사용가능하게끔 1차적으로 바이트 코드로 변환하는 과정은 컴파일 과정이라고 볼 수 있다.
하지만 JVM이 바이트코드를 기계어로 번역하여 실행하는 것은
인터프리트 방식과 컴파일방식을 혼용하여 기계어로 변환하고 실행시킨다.
실행 중 자주 쓰이는 메서드나 루프는 JVM이 컴파일해서 빠른 실행을 의도한다.
JVM이 바이트코드를 인터프리트 방식으로 실행하는 것이 아니라
컴파일하여 실행하는 것을 JIT(Just-In-Time) 컴파일이라 부른다.
실제로 바이트코드 -> 기계어 변환 및 실행에서
인터프리팅 방식이 빠른 경우,
JIT 컴파일이 빠른 경우가 있기 때문이다.
예를 들어, 1만번 반복하는 for문에 대한 소스코드를 생각해보자.
for문 소스코드는 동일한 동작이 1만번 반복된다.
실제로 1만번 반복되기까지 컴퓨터는 for문 안의 로직이 이렇게 많이 반복될 것이라는 것을 알지 못한다.
예를 들어(이해를 위한 예시),
JVM은 동일한 코드가 1500번쯤 돌아갔을 때 고반복을 인지하고 이 코드를 컴파일 해놓는다.
컴파일 해놓게 되면 인터프리터 과정이 아니라서,
컴파일에 필요한 비용만 지불한다면 기계어만 실행시키면 되기에 속도가 빨라진다.
그러면 처음부터 컴파일 해놓으면 되지 않는가?
컴파일 과정은 시간적, 메모리 용량적으로 비용 소모가 크다.
그렇기에 이 트레이드오프를 계산해서,
이득이 더 커지는 시점에 컴파일 방식을 사용하는 것이 최적화 관점에서 좋다.
이를 판단하는 것이 JVM의 HotSpot 엔진이다.
그렇다!
JVM이 바이트 코드를 실행시키는 것(JIT/인터프리팅) 은 런타임 환경이다.
추가 의문
“소스코드 -> 바이트코드 -> 기계어” 과정을 미리 모두 컴파일 해놓고
준비된 기계어로만 빠르게 실행하면 되지 않나?
여러 문제가 있지만,
모두 컴파일 해놓고 사용한다면 대표적으로 리플렉션을 활용할 수 없게 된다.
리플렉션은 구축되어있는 코드를 활용해야 하는데
미리 컴파일하고 이전 코드를 버리게 되면,
이러한 동적인 구조를 구축할 수 없다.
흔히 코드를 작성하다보면 컴파일 에러에 해당하는 것은
IDE가 빨간줄을 그어준다.
“컴파일 에러는 좋은 에러”라고 배웠다.
실행 도중 터지는 에러가 아니고, IDE가 알려주기 때문에
소스코드 실행 이전 쉽게 수정할 수 있다.
최신 IDE는 코드를 실행하여 컴파일 로직이 작동하기 이전에
IDE가 가진 실시간 분석기로 하여금
문법 / 타입 / 스코프 등을 체크하여 문제를 곧바로 알려준다.
이를 javac가 도와주는구나 생각할 수 있지만,
이는 IDE의 단독 기능이다.
그래서 실제로는 IDE가 잡지 못했는데,
소스코드 실행 후 컴파일 과정에서 에러가 발생하는 경우도 존재한다.
IDE가 잡지 못하는 컴파일 에러 또한
여전히 불행 중 다행의 카테고리에 들어가는 좋은 에러이다.
실행 도중 터지는 문제가 아니기 때문이다.
기계어 실행 관점으로만 본다면 컴파일 언어가 빠르다.
- 컴파일 언어는 컴파일러로 하여금 실행 이전 모든 번역을 완료함
- 인터프리터 언어 또한 정상작동 시 모든 코드를 번역하지만
항상 코드 실행을 위해 한줄 한줄 변환 및 실행을 거치게 됨
컴파일 언어는 실행 이전 선 변환 완료 → 기계어 실행만 수행하기에
빠르게 실행시킬 수 있다.