빌드와 컴파일: 컴파일러와 인터프리터의 차이

기계는 영어를 모른다

학교에서 처음 C언어를 배울 때 가장 이해가 안 갔던 것이 있다. ‘왜 내 코드는 바로 실행되지 않고, 빌드(Build)와 컴파일이라는 귀찮은 과정을 거쳐야 할까?’

파이썬(Python)은 코드 짜고 엔터 치면 바로 돌아가는데, 자바(Java)나 C는 꼭 시간을 잡아먹는 단계가 필요했다. 학부 시절엔 그저 ‘언어마다 문법이 다르니까’ 하고 넘어갔다. 시험에 나오면 ‘C는 컴파일 언어, 파이썬은 인터프리터 언어’라고 답만 맞추면 그만이었다.

하지만 실무에서 대용량 트래픽을 처리하는 백엔드 서버를 다루다 보니, 이 단순한 차이가 시스템의 성능과 배포 속도를 결정짓는 거대한 장벽임을 깨닫게 되었다. 내가 작성한 우아한 코드는, 사실 CPU라는 무식하고 성실한 작업자에게는 그저 알 수 없는 외계어일 뿐이었다.

우리가 쓰는 언어와 기계가 쓰는 언어 사이에는 반드시 ‘번역’이 필요하다.

‘빌드(Build)’가 도대체 뭐길래?

그렇다면 빌드(Build)란 정확히 무엇일까? 단순히 “저장”하는 것과는 무엇이 다를까?

우리가 작성한 소스 코드(.java, .c)는 사실 텍스트 파일에 불과하다. 메모장으로 열어도 읽을 수 있는 ‘글’이다. 하지만 컴퓨터(CPU)는 글을 읽을 줄 모른다. 오직 전기가 통하냐(1), 안 통하냐(0)만 알 수 있다.

빌드는 우리가 쓴 ‘텍스트 파일’을 컴퓨터가 실행할 수 있는 ‘실행 파일(.exe, .class, .jar)’로 변환하는 종합적인 포장 과정이다.

  1. 전처리 (Preprocessing): 주석을 지우고, 필요한 라이브러리 코드를 가져와 붙인다.
  2. 컴파일 (Compilation): 영어로 된 코드를 기계어(또는 바이트코드)로 번역한다.
  3. 링킹 (Linking): 여러 개의 번역된 파일들을 하나로 묶어 최종 실행 파일을 만든다.

즉, 소스 코드가 ‘요리 레시피(종이)’라면, 빌드는 그 레시피대로 재료를 다듬고 볶아서 ‘완성된 요리(음식)’를 내놓는 과정이다. 레시피(코드)를 아무리 수정해도, 다시 요리(빌드)하지 않으면 식탁(서버)에는 어제 만든 식은 음식이 그대로 올라가 있는 것이다.

디지털 물류 센터의 작업 지시서

이 복잡한 과정을 이해하기 위해, 컴퓨터 내부를 거대한 디지털 물류 센터에 비유해보자.

여기서 CPU는 엄청나게 손이 빠르지만, 융통성은 제로인 작업자다. 이 작업자는 오직 기계어(0과 1)라는 작업 지시서만 읽을 수 있다.

우리가 자바나 파이썬으로 코드를 짜는 행위는, 작업자에게 줄 ‘업무 매뉴얼’을 만드는 과정이다. 그런데 문제는 우리가 이 매뉴얼을 영어(프로그래밍 언어)로 쓴다는 점이다. 작업자는 영어를 모른다. 그래서 우리에겐 번역가가 필요하다. 이 번역 방식에 따라 언어의 운명이 갈린다.

1. 컴파일러(Compiler): 미리 번역해두는 전문 번역가

  • 대표 언어: C, C++, Java(하이브리드), Go, Rust
  • 방식: 책 한 권을 통째로 번역해서 출판해두는 것과 같다. 작업 시작 전에 모든 코드를 기계어로 바꿔서 실행 파일(.exe, .class)을 만든다.
  • 장점: 미리 번역해뒀으니 작업자(CPU)가 읽는 속도가 매우 빠르다. 실행 전에 문법 오류를 다 잡아낸다.
  • 단점: 매뉴얼을 한 줄만 고쳐도 책 전체를 다시 인쇄(Re-build)해야 한다.

2. 인터프리터(Interpreter): 실시간 동시 통역사

  • 대표 언어: Python, JavaScript, Ruby
  • 방식: 작업자가 일하는 옆에 붙어서, 한 문장씩 읽어주고 바로 통역한다.
  • 장점: 매뉴얼을 고치면 바로 반영된다. 별도의 번역 파일이 필요 없다.
  • 단점: 통역하는 시간이 걸리므로 작업 속도가 느리다. 실행해보기 전까진 뒤쪽에 오타가 있는지 모른다.
미리 번역할 것인가(속도), 그때그때 통역할 것인가(유연성)의 차이다.

[Code Verification] 기계어의 민낯 확인하기

말로만 들어선 체감이 안 된다. 파이썬이 정말로 한 줄씩 번역하고 있는지, 기계가 보는 코드는 어떻게 생겼는지 눈으로 확인해 보자.

파이썬에는 dis(Disassembler)라는 모듈이 있다. 이것을 쓰면 파이썬 코드가 실행될 때 내부적으로 어떤 명령어로 쪼개지는지 볼 수 있다.

import dis

def my_function():
    a = 10
    b = 20
    print(a + b)

# 파이썬 코드가 어떤 기계어(바이트코드)로 변환되는지 확인
print("--- Python Bytecode Verification ---")
dis.dis(my_function)

이 코드를 실행하면 아래와 같은 외계어들이 출력된다.

  5           0 LOAD_CONST               1 (10)
              2 STORE_FAST               0 (a)

  6           4 LOAD_CONST               2 (20)
              6 STORE_FAST               1 (b)

  7           8 LOAD_GLOBAL              0 (print)
             10 LOAD_FAST                0 (a)
             12 LOAD_FAST                1 (b)
             14 BINARY_ADD
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE

분석:

  1. 우리가 쓴 a = 10LOAD_CONST (상수 10을 가져와라)와 STORE_FAST (빠른 저장소 a에 넣어라)라는 두 개의 명령으로 쪼개졌다.
  2. print(a + b)를 하기 위해 BINARY_ADD (이진 덧셈)와 CALL_FUNCTION (함수 호출)이 수행된다.
  3. 파이썬 인터프리터는 런타임에 이 명령어들을 하나씩 읽어가며 C언어로 짜인 내부 로직을 돌린다. 즉, CPU에게 직접 명령하는 게 아니라, CPU와 대화하는 ‘가상 머신(VM)’에게 명령을 내리는 것이다. 이것이 파이썬이 C보다 느린 이유다.

반면, C언어는 컴파일하면 MOV EAX, 10 (EAX 레지스터에 10을 넣어라) 같은 실제 CPU 어셈블리어로 직역된다. 중간 관리자가 없으니 빠를 수밖에 없다.

실무에서의 Trade-off: 무엇을 선택할까?

학부 때는 그저 편한 게 최고였다. 컴파일 에러가 나는 C언어보다는, 대충 짜도 돌아가는 파이썬이나 자바스크립트가 좋았다. 하지만 실무에서는 안정성생산성 사이에서 치열한 고민(Trade-off)을 해야 한다.

1. 런타임 에러의 공포 (인터프리터의 약점) 파이썬으로 서버를 짰을 때 가장 무서운 건, 오타 하나 때문에 새벽 3시에 서버가 멈추는 것이다. 인터프리터는 실행해보기 전까지는(그 코드가 실행되는 시점까지는) 에러를 모른다. 반면 자바(컴파일 언어)는 코드를 빌드할 때 “여기 오타 났는데요?” 하고 알려준다. 배포 전에 실수를 잡아주는 이 엄격함이 대규모 프로젝트에선 구원자가 된다.

2. 빌드 시간의 지루함 (컴파일러의 약점) 반면 자바 프로젝트가 커지면 코드 한 줄 고치고 확인하는 데 몇 분씩 걸리기도 한다(Gradle Build…). 빠른 수정과 배포가 생명인 스타트업 초기 단계나, 데이터 분석처럼 결과를 바로 봐야 하는 작업에 파이썬이 압도적으로 많이 쓰이는 이유다.

엄격한 선생님(컴파일러)은 안전하지만 답답하고, 친절한 친구(인터프리터)는 편하지만 가끔 실수를 방치한다.

마치며: 이론은 무기가 된다

우리는 이제 ‘빌드와 컴파일’의 과정, 그리고 언어마다 코드를 번역하는 방식이 다르다는 것을 알았다. 이론적으로는 내 프로젝트 성격에 맞춰 최고의 언어를 고르는 것이 정답일 것이다.

하지만 현실은 그렇게 친절하지 않았다. 내가 입사한 회사는 이미 정해진 기술 스택이 있었고, 나는 자바 개발자로 들어왔지만 파이썬과 자바스크립트를, 심지어 안드로이드 XML까지 만져야 했다.

이 혼란스러운 ‘잡식 개발’의 현장에서 나를 구원한 것은 역설적이게도 오늘 배운 이 ‘기초 이론’들이었다. 언어의 껍데기는 달라도 그 안에서 돌아가는 원리는 같기 때문이다.

다음 시간에는 내가 어떻게 ‘하나의 언어(Java)를 깊게 파서 다른 언어들을 공략했는지’, 그리고 왜 프레임워크의 노예가 되지 않으려면 기초가 중요한지에 대한 실전 경험담을 이야기해 보겠다.

댓글 남기기