Rust 실전 강좌 #7 릴리스 최적화와 크로스 컴파일: 작고 빠른 바이너리를 어디서나

4 분 소요

테스트(6편)라는 안전망이 생겼으니, 이제 남에게 건넬 바이너리를 만듭니다. 이번 편의 목표는 두 가지입니다. release 빌드를 한 번 더 조여 작고 빠르게 만드는 것, 그리고 내 기계가 아닌 OS·아키텍처를 위한 바이너리를 만드는 것입니다.

release 프로필 조이기 #

4편에서 --release의 효과를 측정했는데, 그 release에도 조일 나사가 남아 있습니다. Cargo.toml에 프로필 설정을 추가합니다.

Cargo.toml
[profile.release]
lto = true           # 링크 타임 최적화: 크레이트 경계를 넘는 인라인·제거
codegen-units = 1    # 병렬 코드 생성을 포기하고 최적화 품질을 최대로
strip = true         # 디버그 심벌 제거로 바이너리 크기 축소

각 옵션은 무료가 아니라 교환입니다.

  • lto(link-time optimization): 보통은 크레이트 단위로 최적화가 끝나지만, lto는 링크 시점에 전체를 다시 보며 크레이트 경계를 넘는 인라인과 죽은 코드 제거를 수행합니다. 대가는 컴파일 시간 증가입니다.
  • codegen-units = 1: 기본값은 코드 생성을 여러 조각으로 나눠 병렬 컴파일하는 것인데, 조각 경계에서 최적화 기회가 사라집니다. 1로 두면 품질은 최대, 컴파일은 가장 느립니다.
  • strip: 디버그 심벌을 제거해 크기를 크게 줄입니다. 대가는 배포 바이너리의 백트레이스가 빈약해지는 것입니다.

정리하면 개발 반복이 느려지는 대가를 배포 품질과 맞바꾸는 설정이고, release 프로필에만 두기 때문에 일상의 cargo run·cargo test에는 영향이 없습니다. loglens 정도의 프로젝트에서도 strip 하나로 바이너리가 수 MB에서 1MB대까지 내려가는 것을 흔히 봅니다. 한 가지, 크기 축소 목록에 자주 등장하는 panic = "abort"(되감기 없이 즉시 종료)는 신중해야 합니다. 크기는 줄지만 패닉 시 정리 동작과 백트레이스 품질을 희생하므로, 사용자의 버그 신고를 받아야 하는 도구라면 기본값 유지가 무난합니다.

크로스 컴파일: 타깃이라는 개념 #

내 맥에서 개발했지만 loglens가 돌 곳은 리눅스 서버입니다. Rust는 컴파일 타깃을 “트리플"로 부릅니다. x86_64-unknown-linux-gnu(아키텍처-벤더-OS-라이브러리)처럼 생겼고, rustup으로 타깃을 추가할 수 있습니다.

빌드
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl

타깃 중에 -musl을 고른 것이 이 편의 핵심 결정입니다. 기본 타깃(-gnu)의 바이너리는 시스템의 glibc에 동적으로 링크되는데, 빌드 환경의 glibc가 실행 서버보다 새 버전이면 그 유명한 GLIBC_2.xx not found 에러로 실행이 거부됩니다. musl 타깃은 C 라이브러리까지 정적으로 링크한 단일 파일을 만들므로, 오래된 배포판이든 알파인 컨테이너든 복사만 하면 돕니다. “설치 없이 파일 하나"라는 CLI 도구의 미덕이 완성되는 지점이라, 리눅스 배포용 Rust CLI의 사실상 표준 선택입니다.

링커 문제와 cross #

그런데 위 명령을 맥에서 그대로 실행하면 대개 링크 단계에서 멈춥니다. Rust 컴파일러는 어느 타깃의 코드든 만들 수 있지만, 마지막에 바이너리를 조립하는 링커는 타깃 플랫폼의 것이 필요하기 때문입니다. 기본 방법은 타깃별 링커를 설치하고 설정하는 것이고, 실무에서 널리 쓰는 우회로는 cross입니다.

cross 빌드
cargo install cross
cross build --release --target x86_64-unknown-linux-musl

cross는 타깃별 도구 체인이 미리 갖춰진 도커 컨테이너 안에서 cargo build를 대신 실행합니다. 명령 인터페이스가 cargo와 같아서 배우는 비용이 거의 없고, 링커 설정이라는 늪을 통째로 건너뜁니다. 도커가 있어야 한다는 것이 유일한 전제입니다. 그리고 다음 편에서 보겠지만, 배포 자동화에서는 이 문제를 다른 방식으로도 풉니다. 각 OS의 러너에서 각자 빌드하면 크로스 컴파일 자체가 줄어듭니다. cross는 “내 기계에서 지금 만들어야 할 때"의 도구로 자리를 잡으면 됩니다.

macOS와 Windows 사용자용 바이너리도 같은 원리입니다. Apple Silicon(aarch64-apple-darwin)과 Intel 맥(x86_64-apple-darwin), Windows(x86_64-pc-windows-msvc)를 각각 빌드해 배포 대상 목록을 채웁니다. 이 목록이 다음 편 릴리스 자동화의 매트릭스가 됩니다.

정리 #

  • release 프로필의 lto, codegen-units = 1, strip은 컴파일 시간을 내주고 실행 속도와 크기를 받는 교환입니다. 배포 프로필에만 적용되므로 개발 반복은 그대로입니다.
  • panic = "abort"는 크기를 더 줄이지만 백트레이스 품질을 희생합니다. 버그 신고를 받는 도구라면 기본값이 무난합니다.
  • 크로스 컴파일은 rustup target add + --target이 기본형입니다. 리눅스 배포는 glibc 버전 지옥을 피하는 musl 정적 링크가 사실상 표준입니다.
  • 컴파일은 돼도 링커가 발목을 잡을 때, cross가 도커로 타깃 도구 체인을 통째로 대신합니다.
  • 배포 대상 트리플 목록(리눅스 musl, 맥 2종, Windows)이 다음 편의 재료입니다. crates.io 발행과 GitHub Releases 자동화로 시리즈를 완결하겠습니다.
X