크리스 래트너의 LLVM이 어떻게 언어와 도구들의 모듈형 컴파일러 플랫폼이 되었는지—최적화, 향상된 진단, 빠른 빌드를 가능하게 한 과정을 알아봅니다.

LLVM은 많은 컴파일러와 개발 도구들이 공유하는 "엔진 룸"이라고 생각하면 가장 쉽습니다.
C, Swift, Rust 같은 언어로 코드를 쓸 때, 그 코드를 CPU가 실행할 수 있는 명령어로 바꿔줄 무언가가 필요합니다. 전통적인 컴파일러는 파이프라인의 모든 부분을 자체적으로 구현하곤 했습니다. LLVM은 다른 접근을 취합니다. 최적화, 분석, 여러 종류의 프로세서를 위한 머신 코드 생성 같은 어렵고 비용이 많이 드는 작업을 처리하는 재사용 가능한 고품질 코어를 제공합니다.
LLVM은 대부분의 경우 "직접 사용하는 단일 컴파일러"라기보다는 컴파일러 인프라입니다. 언어 팀은 문법과 의미론, 개발자용 기능에 집중하고 무거운 작업을 LLVM에 맡길 수 있도록 구성 요소를 조합해 툴체인을 만들 수 있습니다.
이 공유 기반 덕분에 현대 언어들은 수십 년간 축적된 컴파일러 기술을 재발명하지 않고도 빠르고 안전한 툴체인을 제공할 수 있습니다.
LLVM은 일상적인 개발 경험 속에 다음과 같은 방식으로 나타납니다:
이 글은 크리스 래트너가 시작한 아이디어—LLVM의 구조, 왜 중간 레이어가 중요한지, 최적화와 다중 플랫폼 지원을 어떻게 가능하게 하는지—를 안내하는 투어입니다. 교과서 수준의 이론보다는 직관과 실무적 영향에 초점을 맞춥니다.
크리스 래트너는 2000년대 초 대학원생 시절 실용적인 불만에서 출발해 LLVM을 만들었습니다. 당시 컴파일러 기술은 강력했지만 재사용하기 어려웠습니다. 새 언어를 만들거나 더 나은 최적화를 하거나 새로운 CPU를 지원하려면 각 컴파일러의 tightly coupled한 부분을 손봐야 했습니다.
당시 많은 컴파일러는 언어를 이해하는 부분, 최적화 부분, 머신 코드를 생성하는 부분이 깊이 뒤얽혀 있는 하나의 거대한 기계처럼 설계되어 있었습니다. 이는 원래 목적에는 효과적이었지만 확장하거나 적응하기엔 비용이 컸습니다.
래트너의 목표는 "하나의 언어용 컴파일러"가 아니라 여러 언어와 도구를 구동할 수 있는 공유된 기반을 제공하는 것이었습니다—여러 팀이 동일한 복잡한 부분을 계속해서 다시 쓰지 않도록 하는 것이었습니다. 파이프라인의 중간을 표준화할 수 있다면 모서리(프론트엔드와 UX)에서 더 빠르게 혁신할 수 있다는 베팅이었습니다.
핵심 전환은 컴파일을 명확한 경계를 가진 분리 가능한 빌딩 블록 집합으로 보는 것이었습니다. 모듈형 세계에서는:
지금은 당연해 보이지만 당시 많은 생산용 컴파일러의 진화 방식과는 대조적이었습니다.
LLVM은 초기에 오픈소스로 공개되었고, 이는 중요했습니다. 공유 인프라는 여러 그룹이 신뢰하고 검사하고 확장할 수 있어야만 작동합니다. 시간이 지나면서 대학, 기업, 독립 기여자들이 타깃 추가, 코너 케이스 수정, 성능 향상, 새로운 도구 구축 등을 통해 프로젝트를 형성했습니다.
이 커뮤니티 측면은 단순한 호의가 아니었습니다—핵심을 널리 유용하게 만들어야 함께 유지할 가치가 생긴다는 설계 요소였습니다.
LLVM의 핵심 아이디어는 간단합니다: 컴파일러를 세 부분으로 나누어 많은 언어가 가장 어려운 작업을 공유하게 하자는 것입니다.
프론트엔드는 특정 프로그래밍 언어를 이해합니다. 소스 코드를 읽고 규칙(문법과 타입)을 확인한 뒤 구조화된 표현으로 바꿉니다.
요점은 프론트엔드는 모든 CPU 세부사항을 알 필요가 없다는 것입니다. 그들의 임무는 함수, 루프, 변수 같은 언어적 개념을 더 보편적인 형태로 번역하는 것입니다.
전통적으로 컴파일러를 만들려면 같은 작업을 반복해야 했습니다:
LLVM은 이를 다음으로 줄입니다:
그 "공유된 형태"가 바로 LLVM의 핵심입니다: 최적화와 분석이 존재하는 공통 파이프라인입니다. 중간층의 개선(더 나은 최적화나 디버깅 정보)은 여러 언어에 동시에 이익을 줄 수 있습니다.
백엔드는 공유된 표현을 받아 특정 프로세서용 출력(예: x86, ARM)을 만듭니다. 레지스터, 호출 규약, 명령어 선택 같은 세부사항이 중요한 부분입니다.
컴파일을 여행 경로로 생각해보면:
결과는 모듈형 툴체인입니다: 언어는 개념을 명확히 표현하는 데 집중하고, LLVM의 공유 코어는 그 개념들을 여러 플랫폼에서 효율적으로 실행되도록 만듭니다.
LLVM IR(중간 표현)은 프로그래밍 언어와 CPU가 실행하는 머신 코드 사이에 놓인 "공통 언어"입니다.
컴파일러 프론트엔드(예: C/C++용 Clang)는 소스 코드를 이 공유된 형태로 번역합니다. 그런 다음 LLVM의 최적화기와 코드 생성기가 IR을 대상으로 작업하고, 최종적으로 백엔드가 IR을 특정 타깃(x86, ARM 등)의 명령어로 바꿉니다.
LLVM IR을 다리로 생각해보면:
이 때문에 사람들은 LLVM을 "컴파일러 인프라"라고 부릅니다. IR은 그 인프라를 재사용 가능하게 하는 공통 계약입니다.
코드가 LLVM IR로 표현되면 대부분의 최적화 패스는 그 코드가 원래 C++ 템플릿에서 시작했는지, Rust 이터레이터에서 시작했는지 알 필요가 없습니다. 그들은 대체로 다음과 같은 보편적 아이디어에 관심을 둡니다:
따라서 언어 팀은 자체 최적화 스택을 직접 구축(및 유지)할 필요 없이 프론트엔드—파싱, 타입 검사, 언어 고유 규칙—에 집중한 뒤 LLVM에 무거운 작업을 맡길 수 있습니다.
LLVM IR은 머신 코드에 깔끔하게 매핑될 만큼 저수준이지만 분석하기에 구조화되어 있습니다. 기본 연산(add, compare, load/store), 명확한 제어 흐름(분기), 강타입 값들로 구성된 깔끔한 어셈블리 언어에 가깝습니다. 인간이 직접 쓰기보다는 컴파일러를 위해 설계된 표현입니다.
사람들이 "컴파일러 최적화"를 들으면 신비한 기법을 떠올리기 쉽습니다. LLVM에서 대부분의 최적화는 안전한 기계적 재작성으로 이해하는 편이 더 정확합니다—프로그램의 의미를 보존하면서 더 빠르게(또는 더 작게) 동작하도록 바꾸는 변환들입니다.
LLVM은 코드를(LLVM IR로) 받아 작은 개선을 반복적으로 적용합니다. 초안을 다듬는 것과 비슷합니다:
3 * 4 → 12) 런타임 비용을 줄입니다.이러한 변경은 보수적으로 이루어집니다. 패스는 재작업이 프로그램 의미를 바꾸지 않는다고 증명할 수 있을 때만 변환을 수행합니다.
프로그램이 개념적으로 다음을 한다면:
…LLVM은 이를 "설정은 한 번만 한다", "결과를 재사용한다", "죽은 분기를 삭제한다"로 바꾸려 합니다. 마법이라기보다 집안 정리에 가깝습니다.
최적화는 무료가 아닙니다: 더 많은 분석과 패스를 돌리면 보통 컴파일이 느려집니다, 반면 최종 프로그램은 더 빨라질 수 있습니다. 그래서 툴체인은 "약간 최적화"와 "공격적으로 최적화" 같은 수준을 제공합니다.
프로파일은 여기서 도움이 됩니다. **프로파일 기반 최적화(PGO)**를 사용하면 프로그램을 실행해서 실제 사용 데이터를 수집한 뒤 재컴파일해 LLVM이 실제로 중요한 경로에 더 많은 노력을 기울이게 할 수 있습니다—이렇게 하면 트레이드오프를 더 예측 가능하게 만들 수 있습니다.
컴파일러는 두 가지 다른 임무를 수행합니다. 첫째, 소스 코드를 이해해야 합니다. 둘째, 특정 CPU가 실행할 수 있는 머신 코드를 생성해야 합니다. LLVM 백엔드는 두 번째 작업에 집중합니다.
LLVM IR을 "범용 레시피"로 생각하면, 백엔드는 그 레시피를 특정 프로세서 계열의 정확한 명령어로 바꿉니다—데스크톱/서버용 x86-64, 휴대기기 및 최신 노트북용 ARM64, 혹은 WebAssembly 같은 특수 타깃 등입니다.
구체적으로 백엔드는 다음을 담당합니다:
공유 코어가 없다면 모든 언어는 지원하려는 모든 CPU마다 이 모든 것을 다시 구현해야 합니다—막대한 작업량이고 지속적인 유지보수 부담입니다.
LLVM은 이를 뒤집습니다: 프론트엔드(예: Clang)는 한 번 LLVM IR을 생성하고, 백엔드는 타깃별 "라스트 마일"을 처리합니다. 새로운 CPU 지원을 추가하려면 보통 하나의 백엔드를 쓰거나 확장하면 되지, 존재하는 모든 컴파일러를 다시 쓰지 않아도 됩니다.
Windows/macOS/Linux에서, x86과 ARM에서, 혹은 브라우저에서 실행해야 하는 프로젝트에 대해 LLVM의 백엔드 모델은 실용적 이점입니다. 하나의 코드베이스와 대체로 하나의 빌드 파이프라인을 유지한 채 다른 백엔드를 선택하거나 크로스 컴파일링을 통해 타깃을 바꿀 수 있습니다.
이러한 이식성 때문에 LLVM은 광범위하게 사용됩니다: 단지 속도 때문만이 아니라 플랫폼별로 반복되는 컴파일러 작업을 피해 팀의 속도를 높여주기 때문입니다.
Clang은 C, C++, Objective-C용 프론트엔드로 LLVM에 플러그인됩니다. LLVM이 최적화하고 머신 코드를 생성할 수 있는 공유 엔진이라면, Clang은 소스 파일을 읽고 언어 규칙을 이해해 LLVM이 작업할 수 있는 형태로 바꿔주는 부분입니다.
많은 개발자는 논문을 읽고 LLVM을 발견한 것이 아니라, 컴파일러를 바꿨을 때 피드백이 확연히 좋아진 경험을 통해 LLVM을 처음 알게 됩니다.
Clang의 진단은 더 읽기 쉽고 구체적이라는 평을 받습니다. 모호한 오류 대신 문제를 일으킨 정확한 토큰을 가리키고, 관련 행을 보여주며 무엇을 기대했는지 설명해 주는 경우가 많습니다. 일상적인 작업에서 이 점은 중요합니다. "컴파일-수정-반복" 루프가 덜 고통스러워지기 때문입니다.
Clang은 또한 libclang과 광범위한 Clang 도구 생태계를 통해 깔끔하고 문서화된 인터페이스를 제공합니다. 이것은 에디터, IDE, 기타 개발 도구들이 C/C++ 파서를 재발명하지 않고도 깊은 언어 이해를 통합하기 쉽게 만들었습니다.
도구가 코드를 안정적으로 파싱하고 분석할 수 있게 되면 텍스트 편집을 넘는 기능들이 가능해집니다:
이 때문에 Clang은 종종 LLVM의 첫 접점으로 여겨집니다: 실용적인 개발자 경험 개선이 출발하는 곳입니다. LLVM IR이나 백엔드를 전혀 의식하지 않아도 에디터의 자동완성, 정적 검사, 빌드 오류가 더 똑똑하고 처리하기 쉬워진 혜택을 받습니다.
LLVM이 언어 팀에게 매력적인 이유는 단순합니다: 언어 자체에 집중하도록 해주고, 완전한 최적화 컴파일러를 재발명하는 데 수년을 소비하지 않도록 해줍니다.
새 언어를 만드는 일은 이미 파싱, 타입 검사, 진단, 패키지 툴링, 문서화, 커뮤니티 지원 등을 포함합니다. 여기에 제품급 최적화기와 코드 생성기, 플랫폼 지원을 처음부터 만들어야 한다면 출시가 지연됩니다—때로는 수년 단위로요.
LLVM은 레지스터 할당, 명령어 선택, 성숙한 최적화 패스, 일반 CPU용 타깃을 제공하는 준비된 컴파일 코어를 제공합니다. 팀은 자신의 언어를 LLVM IR로 내리는 프론트엔드를 연결하고 기존 파이프라인을 이용해 macOS, Linux, Windows용 네이티브 코드를 생성할 수 있습니다.
LLVM의 최적화기와 백엔드는 장기간의 엔지니어링과 실전 테스트의 결과물입니다. 이는 LLVM을 채택한 언어에 대해 초기부터 강력한 기본 성능을 제공하며, LLVM 자체가 향상되면 그 이득을 곧바로 누릴 수 있습니다.
이 때문에 잘 알려진 여러 언어가 LLVM을 기반으로 삼았습니다:
LLVM 선택은 트레이드오프입니다. 어떤 언어는 아주 작은 바이너리, 초고속 컴파일, 또는 툴체인 전체에 대한 완전한 제어를 우선시합니다. 다른 경우 이미 확립된 컴파일러(예: GCC 기반 생태계)가 있거나 더 단순한 백엔드를 선호하기도 합니다.
LLVM은 강력한 기본값이기 때문에 인기 있는 것이지, 유일한 정답은 아닙니다.
"Just-in-time"(JIT) 컴파일은 쉽게 말해 실행하면서 컴파일하는 것입니다. 모든 코드를 미리 번역해 최종 실행 파일로 만들지 않고, 실제로 필요할 때 해당 코드 조각을 그때그때 컴파일합니다—종종 정확한 타입과 데이터 크기 같은 런타임 정보를 이용해 더 나은 선택을 하기도 합니다.
모든 것을 미리 컴파일할 필요가 없기 때문에 JIT 시스템은 인터랙티브 작업에서 빠른 피드백을 제공합니다. 코드 조각을 작성하거나 생성하고 즉시 실행하면 시스템은 지금 당장 필요한 부분만 컴파일합니다. 같은 코드가 반복 실행되면 JIT은 컴파일된 결과를 캐시하거나 "핫" 섹션을 더 공격적으로 재컴파일할 수 있습니다.
JIT은 동적이거나 인터랙티브한 워크로드에 특히 강합니다:
LLVM이 모든 프로그램을 마법처럼 더 빠르게 만들지는 않으며, 자체로 완전한 JIT은 아닙니다. 대신 LLVM은 툴킷을 제공합니다: 잘 정의된 IR, 많은 최적화 패스, 다양한 CPU 용 코드 생성을 제공합니다. 프로젝트는 이러한 빌딩 블록 위에 JIT 엔진을 구축해 시작 시간, 최고 성능, 복잡성 사이에서 적절한 균형을 선택할 수 있습니다.
LLVM 기반 툴체인은 매우 빠른 코드를 만들어낼 수 있지만 "빠름"은 단일하고 불변의 특성이 아닙니다. 같은 소스라도 컴파일러 버전, 타깃 CPU, 최적화 설정, 그리고 컴파일러가 프로그램에 대해 가정하는 내용에 따라 달라집니다.
두 컴파일러가 동일한 소스(C/C++이나 Rust, Swift 등)를 읽어도 생성되는 머신 코드는 눈에 띄게 다를 수 있습니다. 일부는 의도적입니다: 각 컴파일러는 고유한 최적화 패스, 휴리스틱, 기본 설정을 가집니다. 같은 LLVM이라도 Clang 15와 Clang 18은 인라인 결정, 루프 벡터화, 명령어 스케줄링에서 다르게 동작할 수 있습니다.
또한 언어의 정의되지 않은 동작이나 명시되지 않은 동작에 의한 차이도 있습니다. 프로그램이 표준에서 보장하지 않는 것(예: C의 부호 있는 정수 오버플로)을 실수로 의존하면, 서로 다른 컴파일러나 플래그가 결과를 변경할 수 있습니다.
사람들은 종종 컴파일이 결정적이길 기대합니다: 같은 입력이면 같은 출력. 실제로는 거의 비슷하지만 항상 동일한 바이너리가 나오지는 않습니다. 빌드 경로, 타임스탬프, 링크 순서, 프로파일 기반 데이터, LTO 선택 등이 최종 결과물에 영향을 줄 수 있습니다.
더 실무적인 구분은 디버그 대 릴리스입니다. 디버그 빌드는 스텝 바이 스텝 디버깅과 읽기 쉬운 스택 트레이스를 유지하려고 많은 최적화를 비활성화합니다. 릴리스 빌드는 코드 재배열, 인라이닝, 변수 제거 같은 공격적 변환을 활성화해 성능을 높이지만 디버깅은 더 어렵게 만들 수 있습니다.
성능 문제는 측정 문제로 다루세요:
-O2 vs -O3, LTO 활성화/비활성화, -march 선택 등).작은 플래그 변경이 성능을 좌우할 수 있습니다. 가장 안전한 워크플로는 가설을 세우고 측정하며, 사용자 실제 사용 환경에 가까운 벤치마크를 유지하는 것입니다.
LLVM은 종종 컴파일러 툴킷으로 묘사되지만 많은 개발자가 그 영향을 컴파일 주변의 도구들—분석기, 디버거, 빌드 중 켤 수 있는 안전 검사—을 통해 느낍니다.
LLVM은 잘 정의된 중간 표현(IR)과 패스 파이프라인을 노출하므로 성능 외의 목적을 위해 코드를 검사하거나 재작성하는 추가 단계를 만드는 것이 자연스럽습니다. 어떤 패스는 프로파일링을 위한 카운터를 삽입하거나 의심스러운 메모리 연산에 표시를 하거나 커버리지 데이터를 수집할 수 있습니다.
중요한 점은 이러한 기능들을 각 언어 팀이 같은 배관을 다시 만들지 않고 통합할 수 있다는 것입니다.
Clang과 LLVM은 런타임에서 일반적인 버그 클래스를 감지하기 위해 프로그램을 계측하는 런타임 "sanitizer" 계열을 대중화했습니다—경계 밖 메모리 접근, use-after-free, 데이터 레이스, 정의되지 않은 동작 패턴 등이 그 예입니다. 이들은 마법 같은 보호막이 아니며 보통 프로그램을 느리게 하므로 CI와 사전 릴리스 테스트에서 주로 사용됩니다. 하지만 트리거될 때 정확한 소스 위치와 읽기 쉬운 설명을 제공해 간헐적 크래시를 추적하는 데 매우 유용합니다.
도구 품질은 커뮤니케이션과도 관련이 있습니다. 명확한 경고, 실행 가능한 오류 메시지, 일관된 디버그 정보는 신규 개발자의 "미스테리 요인"을 줄입니다. 툴체인이 무슨 일이 일어났는지와 어떻게 고칠지를 설명하면 개발자는 컴파일러 특성을 외우는 데 시간을 쓰지 않고 코드베이스를 배우는 데 집중할 수 있습니다.
LLVM 자체가 완벽한 진단이나 안전을 보장하진 않지만, 많은 프로젝트에서 이러한 개발자 중심 도구를 실용적으로 구축하고 유지·공유할 수 있는 공통 기반을 제공합니다.
LLVM은 "자체 컴파일러와 툴링을 만드는 키트"로 생각하는 것이 좋습니다. 이 유연성 때문에 많은 현대 툴체인의 동력이 되지만, 모든 프로젝트에 맞는 정답은 아닙니다.
LLVM은 심각한 컴파일러 공학을 재사용하고 싶을 때 빛을 발합니다.
작고 제한된 임베디드 시스템처럼 빌드 크기, 메모리, 컴파일 시간이 엄격히 제한되는 경우 LLVM은 무거울 수 있습니다.
또한 매우 특화된 파이프라인으로 일반 목적 최적화를 원치 않거나 언어가 고정된 DSL에 가깝고 직접 머신 코드로 매핑하는 게 더 간단한 경우에는 부적합할 수 있습니다.
다음 세 가지 질문을 던져보세요:
대부분에 대해 "예"라면 LLVM은 보통 실용적인 선택입니다. 주로 하나의 좁은 문제를 해결하는 가장 작고 단순한 컴파일러가 목표라면 더 가벼운 접근이 유리할 수 있습니다.
대부분 팀은 LLVM을 "도입"하는 프로젝트를 원하지 않습니다. 그들이 원하는 것은 결과입니다: 크로스 플랫폼 빌드, 빠른 바이너리, 좋은 진단, 신뢰할 수 있는 도구.
이 때문에 Koder.ai 같은 플랫폼이 흥미롭습니다. 워크플로가 높은 수준의 자동화(기획, 스캐폴딩 생성, 빠른 반복)에 의해 점점 더 구동될 때도, LLVM/Clang과 같은 현대 컴파일러 인프라는 도구체인 아래에서 최적화, 진단, 이식성 같은 보이지 않는 작업을 수행합니다. Koder.ai의 채팅 기반 "vibe-coding" 접근은 제품을 더 빠르게 내보내는 데 집중하고, 해당 환경에서 적용 가능한 경우 LLVM/Clang 등은 배경에서 묵묵히 최적화와 진단을 담당합니다.