infra
Platform

모듈 맵

[SW Eng] 컴파일·인터프리터·빌드·런타임 — '빌드한다'는 게 뭔가

0 / 38 완료

펼치기
0 / 38 완료0%

PM·SRE를 위한 소프트웨어 엔지니어링 · 02 / 38

[SW Eng] 컴파일·인터프리터·빌드·런타임 — '빌드한다'는 게 뭔가

개발자가 '빌드 깨졌다', 'JVM 위에서 돈다', '인터프리터 언어라 느리다'고 할 때의 의미를 PM·인프라 관점에서 정리합니다

🚨INCIDENT ALERT
HIGH

배포 직전, 개발자가 슬랙에 "빌드 깨졌어요"라고 올립니다. 잠시 뒤 "로컬에선 되는데 CI에서만 안 돼요"가 따라옵니다. PM인 당신은 일정이 밀릴지 가늠해야 하고, 인프라 담당인 당신은 "서버에 뭘 더 깔아야 하나"를 판단해야 합니다. '빌드'가 정확히 무엇을 하는 단계인지, '런타임'과 어떻게 다른지 모르면 이 한 줄짜리 알림 앞에서 아무 판단도 할 수 없습니다.

이번 챕터에서 배울 것
  • 1컴파일 언어와 인터프리터 언어의 차이를 "언제 번역하는가"로 설명할 수 있다
  • 2"빌드"가 소스 코드를 배포 가능한 산출물로 바꾸는 과정임을 설명할 수 있다
  • 3빌드 산출물(JAR/WAR/번들/바이너리)과 런타임 버전을 짝으로 확인할 수 있다
  • 4"로컬은 되는데 CI는 깨진다"의 원인을 환경 차이 관점에서 진단할 수 있다

컴파일 vs 인터프리터 — 언제 번역하는가

💡개념

번역 시점의 차이: 미리 vs 실행하면서

"인터프리터 언어라 느리다", "컴파일 에러가 났다" — 이 말을 이해하려면 코드가 언제 기계가 읽는 형태로 번역되는가를 봐야 합니다.

  • 컴파일 언어(Go, C++, Rust, Java*): 실행 전에 전체 코드를 기계어나 바이트코드로 번역(컴파일)합니다. 번역 중 문법·타입 오류를 미리 잡습니다 → "컴파일 에러". 실행은 빠르지만 빌드 단계가 필요합니다.
  • 인터프리터 언어(Python, Ruby, JavaScript): 실행 하면서 한 줄씩 해석합니다. 빌드 없이 바로 돌릴 수 있지만, 일부 오류는 그 줄에 도달해야 드러납니다.

*Java/Kotlin은 '바이트코드로 컴파일 → JVM이 실행 시 해석/JIT'하는 중간형입니다. 그래서 "컴파일도 하고 JVM 런타임도 필요"합니다.

PM 관점: 컴파일 언어는 빌드 단계가 게이트가 되어 배포 전에 일정 부류의 버그를 차단합니다. 인터프리터 언어는 반복이 빠른 대신 런타임 에러 비중이 높아 테스트가 더 중요해집니다.

컴파일 vs 인터프리터 — 번역 시점의 차이확대

위 그림처럼 컴파일 언어는 실행 전에 전체를 번역해 빌드 단계에서 오류를 차단하고, 인터프리터 언어는 실행 시점에 줄 단위로 해석해 빌드 없이 바로 실행됩니다.

빌드 — 소스를 '배포 가능한 것'으로

💡개념

빌드: 사람이 쓴 코드를 기계가 실행할 산출물로 변환

"빌드 깨졌다"의 빌드는 소스 코드 → 실행/배포 가능한 산출물(artifact) 변환 과정입니다. 보통 이런 단계를 포함합니다.

빌드 파이프라인 — 소스 코드(.java/.ts)에서 의존성 설치(npm·maven), 컴파일/트랜스파일(javac·tsc), 번들/패키징(jar·webpack)을 거쳐 배포 가능한 산출물(JAR/WAR·번들·바이너리)로 변환확대

산출물의 형태는 스택마다 다릅니다:

스택빌드 산출물배포 시 필요한 런타임
Java/Springapp.jar 또는 app.warJVM(JRE/JDK) 동일 버전
프론트엔드(React 등)정적 번들(dist/의 JS/CSS/HTML)정적 웹서버(Nginx) 또는 CDN
Go단일 실행 바이너리(런타임 내장 — 별도 설치 불필요)
Python(대개 빌드 없이 소스 배포)Python 인터프리터 + 패키지

인프라 관점의 핵심: "무엇을 빌드해서(JAR?) 무엇 위에서 실행하는가(JVM 17?)"를 한 쌍으로 확인하면 배포 준비물이 정확해집니다.

위 그림처럼 빌드는 의존성 설치 → 컴파일/트랜스파일 → 번들/패키징 단계를 거쳐 스택마다 다른 형태의 산출물(JAR·번들·바이너리)을 만들고, 각 산출물은 그에 맞는 런타임을 요구합니다.

런타임·빌드 판단 — 버전 짝과 산출물을 어떻게 맞추나
UnsupportedClassVersionError 같은 에러빌드 JDK 버전 > 실행 JRE 버전 — 새 버전으로 컴파일한 산출물을 옛 런타임이 못 읽음. 빌드·실행 런타임 버전을 맞춰라. '컴파일 버전 ≤ 실행 버전'
'로컬은 되는데 서버가 안 됨'런타임 버전 차이가 단골 — 로컬 Node 20, 서버 Node 16. .nvmrc·Dockerfile로 버전을 코드에 고정. '런타임 버전을 명시 고정'
컴파일 언어 vs 인터프리터컴파일(Java·Go)=빌드 산출물+런타임 분리, 인터프리터(Python·JS)=소스+런타임. 배포 단위가 다름(바이너리 vs 소스+의존성). '번역 시점이 배포 형태를 정한다'
빌드를 어디서 할까로컬 빌드 산출물을 직접 배포 금지 — CI에서 빌드(재현 가능·깨끗한 환경). '내 머신에서 빌드한 건 내 머신 의존성이 섞인다'
의존성 설치를 캐시할까빌드 단계에서 의존성(node_modules·.m2)을 캐시 → 빌드 시간 급감. 단 잠금 파일 기반으로 캐시 키. '의존성 캐시가 CI 속도의 핵심'
산출물에 무엇을 포함할까실행에 필요한 것만(멀티스테이지 빌드로 빌드 도구 제외) — 빌드 산출물에 소스·빌드 캐시까지 넣으면 이미지 비대·취약점. '실행에 필요한 최소만 담는다'

빌드 산출물과 런타임 짝 확인 — 직접 보기

1빌드 산출물과 요구 런타임 버전 확인하기

배포 대상 서버(또는 컨테이너)의 런타임 버전과, 빌드 산출물이 요구하는 버전이 맞는지 확인합니다. 불일치는 배포 실패의 단골 원인입니다.

로컬 터미널
# 서버에 설치된 자바 런타임 버전
java -version

# 빌드 산출물(JAR)이 어떤 자바 버전으로 컴파일됐는지(메이저 버전)
# 55=Java11, 61=Java17, 65=Java21
javap -verbose -cp app.jar com.example.Main | grep "major version"

# Node 프로젝트라면: 요구 런타임이 package.json engines에 명시되곤 함
node -v && cat package.json | grep -A2 '"engines"'
OUTPUT
openjdk version "11.0.22"          ← 서버는 Java 11
  major version: 61                ← 그런데 산출물은 Java 17로 빌드됨 → 불일치!
java -version
🔍실행 후 확인할 것
  • java -version의 버전과 산출물 major version을 비교한다 — 산출물 버전이 서버보다 높으면 UnsupportedClassVersionError로 실행 실패. 같거나 서버가 높아야 안전
  • major version 매핑: 55=Java11, 61=Java17, 65=Java21. 산출물 61인데 서버 11(major 55)이면 즉시 런타임 업그레이드 또는 재빌드 필요
  • Node는 package.json의 engines.node 범위를 본다 — 서버 node -v가 그 범위 밖이면 설치 단계나 실행에서 경고/실패
  • 버전이 맞는데도 안 되면 빌드 산출물 자체가 최신인지(오래된 JAR 배포) 확인 — 빌드 시각/커밋 해시를 대조한다

런타임 버전 불일치 — 배포 실패 원인확대

위 그림처럼 빌드 환경(Java 17)과 배포 서버 런타임(Java 11)이 다르면 UnsupportedClassVersionError로 즉시 실패합니다. 컨테이너 베이스 이미지에 버전을 명시해 빌드·런타임을 한 곳에서 고정하는 것이 재발 방지 핵심입니다.

상황: 새 버전을 배포했더니 애플리케이션이 시작조차 못 하고 위 에러로 죽습니다. 개발팀은 "로컬·CI에선 됐다"고 합니다.

원인: 빌드는 Java 17로 했는데 배포 서버의 런타임이 Java 11입니다. JVM은 자신보다 높은 버전으로 컴파일된 바이트코드를 실행할 수 없습니다. 로컬·CI엔 17이 깔려 있어 통과했던 것입니다.

진단:

로컬 터미널
java -version                          # 서버 런타임
javap -verbose -cp app.jar Main | grep "major version"   # 산출물 요구 버전

서버 major(55=11)보다 산출물 major(61=17)가 높으면 확정입니다.

해결: 둘 중 하나로 버전을 맞춥니다. (1) 서버/베이스 이미지의 런타임을 17로 올린다, 또는 (2) 빌드 타깃을 11로 낮춰 재빌드한다. 컨테이너라면 베이스 이미지 태그를 eclipse-temurin:17처럼 명시해 빌드·런타임을 한곳에서 고정하는 것이 재발 방지책입니다.

심화 — 버전이 맞아도 안 도는 이유

💡개념

심화: 아키텍처·libc·JIT — 산출물 호환성의 세 번째 축

런타임 버전을 짝 맞추는 법을 익혔다면, 다음은 버전이 완전히 같은데도 실행이 실패하거나 느려지는 층을 보는 것입니다. 산출물은 런타임 버전 아래의 더 낮은 층 — CPU 아키텍처와 시스템 라이브러리에도 묶여 있습니다.

  • CPU 아키텍처: 'Go는 런타임 내장이라 어디서나 돈다'의 어디서나에는 조건이 있습니다 — 같은 아키텍처. ARM 맥(애플 실리콘)에서 빌드한 바이너리·컨테이너 이미지는 x86_64 서버에서 실행되지 않습니다. 개발 장비는 ARM으로, 서버는 x86과 ARM(Graviton 등) 혼재로 가면서, 아키텍처 불일치는 버전 불일치만큼 흔한 배포 실패가 됐습니다. CI에서 타깃 아키텍처를 지정해 빌드하거나 멀티아키텍처 이미지를 만듭니다.
  • 같은 버전, 다른 libc: 이미지를 줄이려 alpine 베이스를 쓰면 표준 C 라이브러리가 glibc가 아니라 musl입니다. Python 휠·Node 네이티브 모듈처럼 C로 컴파일된 의존성은 glibc 기준으로 배포되는 경우가 많아, 런타임 버전이 똑같아도 alpine에서만 깨질 수 있습니다. '버전이 같은데 왜 안 되지'의 단골 범인이라, 베이스 이미지 교체는 버전 유지라도 검증이 필요한 변경입니다.
  • JIT 워밍업: JVM 앱은 시작 직후엔 바이트코드를 해석 실행하다가, 자주 도는 코드부터 JIT 컴파일해 점점 빨라집니다. 그래서 배포·스케일아웃 직후의 새 인스턴스는 몇 분간 평소보다 느립니다 — 오토스케일이 '느리니까 더 늘리자'로 오판하면 느린 새 인스턴스만 쌓입니다. 배포 직후의 지연 상승은 버그가 아니라 워밍업일 수 있고, 트래픽을 서서히 넘기는 완화가 필요합니다.

정리하면 산출물의 호환성 축은 세 개입니다 — 런타임 버전, CPU 아키텍처, 시스템 라이브러리. 배포 실패를 진단할 때 이 셋을 차례로 확인하면 '로컬은 되는데'의 대부분이 풀립니다.

상황: 급한 수정이라 개발자가 자기 노트북에서 이미지를 빌드해 레지스트리에 올리고 곧장 서버에 배포했습니다. 컨테이너는 뜨자마자 exec /app/server: exec format error 한 줄을 남기고 재시작을 반복합니다. 같은 이미지가 로컬에선 정상 동작합니다.

원인: 노트북이 ARM(애플 실리콘)이라 이미지가 ARM용으로 빌드됐고, 서버는 x86_64입니다. exec format error는 커널이 실행 파일의 기계어 형식 자체를 읽지 못한다는 뜻으로, 코드·런타임 버전 문제가 아니라 아키텍처 불일치의 전형적 신호입니다. '로컬에선 된다'가 성립한 이유는 단순합니다 — 로컬이 곧 빌드한 아키텍처이기 때문입니다.

진단: 서버에서 uname -m으로 서버 아키텍처(x86_64)를 확인하고, docker inspect 이미지명의 Architecture 필드로 이미지 아키텍처(arm64)를 비교합니다. 둘이 다르면 확정입니다.

해결: 서버 아키텍처에 맞춰 다시 빌드합니다 — 급한 불은 docker buildx build --platform linux/amd64처럼 타깃을 명시해 끄고, 근본적으로는 로컬 빌드 배포를 금지하고 CI에서 빌드하는 원칙으로 되돌립니다(CI 러너의 아키텍처가 곧 통제된 타깃이 됩니다). ARM·x86 서버가 혼재된 환경이라면 멀티아키텍처 이미지를 만들어 레지스트리가 아키텍처별로 맞는 이미지를 내려주게 합니다. '어디서 빌드했는가'는 편의 문제가 아니라 산출물의 실행 가능 여부를 결정합니다.

💼
실무 맥락
현업 패턴

인프라 엔지니어로서 컨테이너 베이스 이미지를 고를 때, 이 모듈의 지식이 곧바로 쓰입니다. 개발팀이 "Spring Boot, Java 17"이라고 하면 베이스 이미지를 eclipse-temurin:17-jre로 잡고, 산출물 app.jar를 복사해 java -jar app.jar로 실행하도록 Dockerfile을 구성합니다. 프론트엔드라면 "빌드 산출물은 정적 파일"이므로 Node 런타임이 아니라 Nginx로 서빙합니다 — 이 구분을 못 하면 정적 사이트에 불필요한 Node 서버를 띄우는 낭비가 생깁니다.

다음 모듈에서는 이렇게 빌드·실행되는 코드가 프론트엔드와 백엔드로 어떻게 나뉘고, 화면이 어디서 그려지는지(렌더링)를 다룹니다.


실전 랩으로 손에 익히기: 컴파일·빌드·런타임 실습 — 컴파일·빌드·런타임을 구분하고 '내 컴에선 되는데'를 진단합니다.

관련 모듈로 더 깊이:

지식 확인

퀴즈 — 8문제

Q1

컴파일 언어와 인터프리터 언어의 핵심 차이를 가장 잘 설명한 것은?

Q2

'빌드(build)'가 의미하는 것으로 가장 적절한 것은?

Q3

개발자가 '이건 JVM 위에서 돈다'고 했다. 인프라 담당이 준비해야 할 것은?

Q4

'로컬에선 되는데 빌드 서버(CI)에서만 깨진다'의 가장 흔한 원인은?

Q5

개발자가 'JAR을 Java 17로 빌드했다'고 한다. 인프라가 서버에 준비할 것은?

Q6

빌드 산출물과 런타임의 관계로 옳은 것은?

Q7

[심화] 런타임 버전이 완전히 같은데도 alpine 베이스 이미지에서만 파이썬 휠·Node 네이티브 모듈이 깨진다. 근본 원인은?

Q8

[심화] 컨테이너가 뜨자마자 exec format error를 내며 재시작을 반복한다. 같은 이미지가 어제 노트북에선 잘 돌았다. 원인과 처방은?

0 / 8 답변

🧪 실습으로 확인하기

컴파일·빌드·런타임 — 빌드한다는 게 뭔가

초급

"빌드 깨졌다", "런타임 에러다", "내 컴에선 되는데"라는 한 줄 알림을 정확히 해석한다. 소스에서 빌드 산출물, 실행까지의 단계를 구분하고, 각 용어가 무엇을 가리키는지 식별하며, 환경 차이로 생기는 실패를 어디서부터 봐야 하는지 판단한다.

45📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요