Rust 웹 앱 빌드 가속화
Wenhao Wang
Dev Intern · Leapcell

Rust 웹 개발에서의 지속적인 빌드 오버헤드
Rust의 성능 및 메모리 안전성 평판으로 인해 웹 개발에서 점점 더 매력적인 선택지가 되고 있으며, Axum, Actix-Web, Rocket과 같은 프레임워크가 상당한 인기를 얻고 있습니다. 그러나 컴파일 시간과 관련된 개발자 경험은 종종 병목 현상처럼 느껴질 수 있습니다. 인터프리터 언어 또는 가비지 수집 컴파일 언어조차도 Rust의 강력한 타입 시스템, 빌림 검사기 및 정교한 최적화 프로그램을 사용하면 "cargo build"가 특히 대규모 웹 애플리케이션의 경우 때때로 빙하처럼 느리게 느껴질 수 있습니다. 이러한 지연은 생산성과 창의성에 빠른 피드백이 중요한 내부 개발 루프(IDL)를 방해합니다.
작은 변경 사항을 다시 컴파일하기 위해 몇 분 동안 기다리는 좌절감은 Rust에서의 개발의 즐거움을 빠르게 감소시킬 수 있습니다. 초기 전체 빌드는 허용될 수 있지만, Rust의 뛰어난 캐싱 메커니즘을 사용하더라도 후속 증분 빌드는 여전히 부담이 될 수 있습니다. 이 문서는 Rust 웹 애플리케이션이 종종 느리게 컴파일되는 근본적인 이유와 더 중요하게는 sccache, cargo-watch 및 최신 링커(lld/mold)와 같은 도구를 사용하여 빌드 시간을 대폭 개선하고 개발 민첩성을 되찾을 수 있는 실용적이고 실행 가능한 전략을 살펴봅니다.
컴파일 환경 이해
최적화를 시작하기 전에 Rust 컴파일 속도에 영향을 미치는 주요 개념에 대한 공통된 이해를 확립해 봅시다.
- 컴파일 단위: Rust에서 "crate"(주 애플리케이션 또는 종속성이 있는 라이브러리)는 기본 컴파일 단위입니다. 컴파일러는 각 crate를 개별적으로 처리합니다.
- 증분 컴파일: Rust 컴파일러는 지능적으로 설계되었습니다. 작은 변경을 하면 이전 빌드의 캐시된 아티팩트를 활용하여 영향을 받은 코드 부분만 다시 컴파일하려고 시도합니다. 그러나 "작은" 변경이라도 때로는 캐시의 상당 부분을 무효화하여 상당한 재컴파일을 초래할 수 있습니다.
- 종속성 그래프: 웹 애플리케이션은 일반적으로 수많은 타사 crate(예: 웹 프레임워크, 직렬화 라이브러리, 비동기 런타임)에 종속됩니다. 이러한 각 종속성을 컴파일해야 하며, 특히 첫 번째 빌드 시 컴파일은 총 빌드 시간의 상당 부분을 차지할 수 있습니다. 직접적인 종속성의 변경은 코드 재컴파일을 트리거할 수 있습니다.
- 링커: 모든 오브젝트 파일(
.o파일)이 컴파일된 후 링커의 작업은 이를 실행 파일로 결합하는 것입니다. 이 프로세스는 특히 많은 심볼이 있는 대규모 애플리케이션의 경우 링커가 모든 상호 참조를 확인해야 하므로 놀랍도록 시간이 많이 소요될 수 있습니다. - 디버그 대 릴리스 빌드: 디버그 빌드(
cargo build)는 빠른 컴파일을 우선하고 디버깅 정보를 포함하지만 런타임 성능을 일부 희생합니다. 릴리스 빌드(cargo build --release)는 광범위한 최적화를 수행하여 컴파일 속도가 느려지지만 바이너리는 더 빠르고 작아집니다. 로컬 개발의 경우 거의 전적으로 디버그 빌드를 사용합니다.
느린 컴파일의 근본적인 이유는 Rust의 안전성 및 성능 보장 때문입니다. 컴파일러는 빌림 검사, 타입 검사 및 최적화 패스를 포함한 광범위한 분석을 수행하며, 이는 계산적으로 집중적입니다. 웹 애플리케이션은 본질적으로 많은 종속성을 불러오는 경우가 많아 처리해야 하는 깊고 넓은 종속성 그래프를 생성합니다.
Rust 웹 앱 빌드 성능 향상 전략
컴파일 시간 지연을 방지하기 위한 도구와 기술을 살펴보겠습니다.
1. sccache를 사용한 외부 캐싱
cargo에는 내장된 증분 컴파일 기능이 있지만, sccache는 모든 Rust 프로젝트(및 C/C++ 프로젝트)에 대한 공유, 전역 캐시를 제공하여 캐싱을 한 단계 더 발전시킵니다. 컴파일러 호출을 가로채고 파일이 변경되지 않았고 입력이 동일한 경우 캐시된 출력을 직접 제공합니다. 이는 거의 변경되지 않는 대규모 종속성 트리에 특히 효과적입니다.
설치:
carbo install sccache --locked
구성:
cargo에서 sccache를 사용하도록 하려면 환경 변수를 설정해야 합니다. 일관적인 사용을 위한 가장 간단한 방법은 쉘 구성(.bashrc, .zshrc 등) 또는 프로젝트별 .cargo/config.toml에 추가하는 것입니다.
**옵션 1: 환경 변수 (예: ~/.bashrc 또는 ~/.zshrc)
export RUSTC_WRAPPER="sccache" export SCCACHE_DIR="$HOME/.sccache" # 선택 사항: 캐시 디렉토리 지정 export SCCACHE_CACHE_SIZE="10G" # 선택 사항: 캐시 크기 지정
그런 다음 쉘을 다시 로드하거나 새 터미널을 엽니다.
옵션 2: 프로젝트별 .cargo/config.toml (sccache가 중요한 프로젝트에 권장)
프로젝트 루트에 .cargo/config.toml을 생성하거나 편집합니다.
# .cargo/config.toml [build] rustc-wrapper = "sccache"
sccache 작업 확인:
설정 후 sccache --show-stats를 실행하여 캐싱 활동을 확인합니다.
$ sccache --show-stats Compile stats for sccache version 0.7.3-alpha.0 (bbceb34b 2023-08-04): ... Compile requests 42 Cache hits 30 Cache misses 12 Cache hit rate 71.43% ...
sccache가 변경되지 않은 종속성에 대해 캐시를 히트하므로 특히 첫 번째 전체 빌드 후에 상당한 속도 향상을 확인할 수 있습니다.
2. cargo-watch를 사용한 자동 재컴파일 및 재시작
내부 개발 루프는 즉각적인 피드백을 받는 것입니다. 수동으로 cargo build 또는 cargo run을 실행하는 것은 모든 작은 변경 후 빠르게 지루해집니다. cargo-watch는 소스 파일을 변경 사항에 대해 모니터링하고 수정이 감지되면 명령을 자동으로 다시 실행하여 이 프로세스를 자동화합니다.
설치:
carbo install cargo-watch --locked
웹 애플리케이션 사용:
일반적으로 코드 변경 시 웹 서버를 재컴파일하고 다시 시작하고 싶을 것입니다.
carbo watch -x run
이를 분석해 봅시다.
cargo watch: 메인 명령입니다.-x run: 파일이 변경될 때마다cargo run을 실행합니다.
웹 애플리케이션의 경우, 변경되지 않은 종속성에 대한 전체 재컴파일을 건너뛰고 싶을 수도 있습니다. cargo의 증분 컴파일이 이를 잘 처리하지만, sccache와 cargo-watch를 함께 사용하면 최대 효율성을 보장할 수 있습니다.
간단한 Axum 앱 예시:
src/main.rs:
use axum::{ routing::get, Router, }; #[tokio::main] async fn main() { // 단일 라우트로 애플리케이션 구축 let app = Router::new().route("/", get(handler)); // `localhost:3000`에서 hyper로 실행 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") .await .unwrap(); axum::serve(listener, app).await.unwrap(); } async fn handler() -> &'static str { "Hello, Axum World!" }
이제 cargo watch -x run을 실행합니다. src/main.rs를 저장할 때(예: "Hello, Axum World!"를 "Hi, Axum!"으로 변경) cargo-watch가 변경을 감지하고, sccache는 다른 종속성이 캐시된 경우 src/main.rs 파일만 다시 컴파일되도록 보장하며, 서버가 거의 즉시 다시 시작됩니다.
필요한 경우 -w (watch) 및 -i (ignore) 플래그를 사용하여 폴더를 보거나 무시하도록 지정할 수도 있지만, 기본값이 일반적으로 잘 작동합니다.
3. lld 또는 mold를 사용한 빠른 링킹
컴파일 후 링커는 실행 파일을 만드는 최종 단계를 수행합니다. 대규모 Rust 애플리케이션의 경우 링킹은 빌드 시간의 놀라울 정도로 많은 부분을 차지할 수 있습니다. 기본 ld 링커는 느릴 수 있지만, lld(LLVM의 링커) 및 mold와 같은 최신 대안은 극적인 속도 향상을 제공합니다.
lld (LLVM 링커)
lld는 LLVM 프로젝트의 고성능 링커입니다. 시스템 패키지 관리자를 통해 종종 사용할 수 있습니다.
설치 (Linux - 종종 사전 설치되거나 llvm 설치):
# Ubuntu/Debian sudo apt install lld # Fedora/RHEL sudo dnf install lld
설치 (macOS - Homebrew 경유):
brew install llvm # 이것은 일반적으로 llvm 패키지의 일부로 lld를 설치합니다.
구성:
.cargo/config.toml에서 cargo를 lld와 함께 사용하도록 구성할 수 있습니다.
# .cargo/config.toml [target.x86_64-unknown-linux-gnu] # OS에 맞게 대상 트리플 조정 linker = "clang" # 또는 Linux의 경우 시스템 설정에 따라 "gcc" rustflags = ["-C", "link-arg=-fuse-ld=lld"] [target.aarch64-apple-darwin] # macOS Apple Silicon용 linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=lld"] [target.x86_64-apple-darwin] # macOS Intel용 linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=lld"]
linker = "clang"(또는 gcc)은 Rust에 드라이버로 사용하도록 지시하고, link-arg=-fuse-ld=lld는 해당 드라이버에게 실제 링킹에 lld를 사용하도록 지시합니다. x86_64-unknown-linux-gnu를 특정 대상 트리플(예: Apple Silicon Mac의 경우 aarch64-apple-darwin)로 바꾸십시오. rustc --print target-triple로 대상 트리플을 찾을 수 있습니다.
mold
mold는 Rui Ueyama(lld의 제작자)가 개발한 훨씬 새롭고 매우 빠른 링커입니다. ld 및 lld보다 훨씬 빠르도록 설계되었습니다.
설치 (Linux):
# 권장: GitHub 릴리스에서 사전 빌드된 바이너리 다운로드 # 예: x86-64 Linux의 경우: wget https://github.com/rui314/mold/releases/download/v2.3.0/mold-2.3.0-x86_64-linux.tar.gz tar -xf mold-*.tar.gz sudo cp mold-*/bin/mold /usr/local/bin/mold sudo cp mold-*/lib/mold /usr/local/lib/mold # 일부 설정에서 필요할 수 있습니다.
LD_LIBRARY_PATH를 조정하거나 mold를 시스템 전체에 설치해야 할 수 있습니다.
설치 (macOS - Homebrew 경유):
brew install mold
mold 구성:
lld와 유사하게 .cargo/config.toml을 통해 cargo를 구성합니다.
# .cargo/config.toml [target.x86_64-unknown-linux-gnu] rustflags = ["-C", "link-arg=-fuse-ld=mold"] # Linux linker = "clang" # 또는 "gcc" [target.aarch64-apple-darwin] # macOS Apple Silicon용 rustflags = ["-C", "link-arg=-fuse-ld=mold"] linker = "clang" [target.x86_64-apple-darwin] # macOS Intel용 rustflags = ["-C", "link-arg=-fuse-ld=mold"] linker = "clang"
구성 후 cargo build 출력의 "Linking" 단계에서 즉시 감소를 확인할 수 있으며, 때로는 절반 이상을 줄이기도 합니다.
결합된 워크플로
가장 효과적인 접근 방식은 이러한 모든 도구를 결합하는 것입니다.
- 전역
sccache설정: 쉘 환경에RUSTC_WRAPPER="sccache"가 설정되어 있는지 확인하거나 프로젝트별.cargo/config.toml을 할당합니다. lld또는mold통합: 프로젝트의.cargo/config.toml에 링커 구성을 추가합니다.- 로컬 개발 워크플로:
cargo watch -x run을 사용하여 자동 재컴파일 및 재시작을sccache및 빠른 링커와 결합하여 이점을 얻습니다.
더 빠른 "watch and rerun" 주기를 위해, 특히 작은 변경 중에는 Cargo.toml에서 디버깅 정보의 일부를 건너뛸 수 있습니다(해당 단계에서 광범위한 디버깅이 필요하지 않은 경우).
# Cargo.toml [profile.dev] # 기본값은 2(전체 디버그 정보)이며, 0으로 제거합니다. 1은 "줄 테이블만"용입니다. # 더 빠른 컴파일을 위해 0 또는 1을 사용하지만, 올바른 디버깅을 위해 되돌리는 것을 잊지 마십시오. debug = 0
주의: debug = 0으로 설정하면 디버깅 가능성이 크게 줄어듭니다(예: 중단점이 작동하지 않음). 디버깅 없이 가장 빠른 빌드 시간을 진정으로 원하고 버그와 관련 없는 변경 사항을 반복하는 경우에만 사용하십시오. 값 1은 종종 좋은 균형을 제공합니다.
결론
Rust 웹 개발에서 느린 컴파일 시간은 언어의 고유한 안전성 및 성능 복잡성과 현대 웹 애플리케이션의 깊은 종속성 그래프로 인해 생산성에 상당한 부담이 될 수 있습니다. 그러나 sccache를 지능적인 빌드 캐싱에 전략적으로 활용하고, cargo-watch를 자동 재컴파일 및 재시작에 활용하며, lld 또는 mold와 같은 고급 링커를 사용하여 링킹 오버헤드를 대폭 줄임으로써 Rust 개발 경험을 좌절스러운 기다림에서 유연한 반복으로 변화시킬 수 있습니다. 이러한 도구를 채택하는 것은 귀중한 시간을 절약할 뿐만 아니라 궁극적으로 Rust로 강력하고 성능이 뛰어난 웹 서비스를 구축하는 즐거움을 되찾아 줍니다.

