이미지 로딩 중...

Rust로 머클트리 구현하기 2편 - Rust 개발 환경 설정 - 슬라이드 1/9
A

AI Generated

2025. 11. 12. · 2 Views

Rust로 머클트리 구현하기 2편 - Rust 개발 환경 설정

Rust 개발을 시작하기 위한 완벽한 환경 설정 가이드입니다. rustup 설치부터 VS Code 설정, Cargo 프로젝트 생성까지 실무에서 필요한 모든 과정을 단계별로 안내합니다.


목차

  1. Rust 설치 - rustup으로 시작하기
  2. VS Code 설정 - Rust 개발을 위한 최적 환경
  3. Cargo 프로젝트 생성 - Rust 프로젝트의 시작
  4. Cargo.toml 이해하기 - 프로젝트 설정의 중심
  5. 프로젝트 구조 이해하기 - 파일과 디렉토리의 역할
  6. 첫 번째 Rust 프로그램 작성하기 - Hello, World부터 시작
  7. Rust 기본 문법 - 변수와 불변성
  8. 에디션 이해하기 - Rust 2021의 의미

1. Rust 설치 - rustup으로 시작하기

시작하며

여러분이 새로운 프로그래밍 언어를 배우려고 할 때, 가장 먼저 막히는 부분이 바로 개발 환경 설정이죠. 특히 Rust는 시스템 프로그래밍 언어라서 처음 접하시는 분들은 "어떻게 설치하지?", "무엇을 설치해야 하지?" 하는 의문이 들 수 있습니다.

다른 언어들처럼 여러 버전의 컴파일러를 수동으로 관리하거나, 복잡한 빌드 도구를 따로 설치해야 한다면 정말 번거로울 것입니다. 실제로 많은 개발자들이 환경 설정 단계에서 시간을 많이 소비하곤 합니다.

바로 이럴 때 필요한 것이 rustup입니다. rustup은 Rust의 공식 설치 도구로, 단 한 번의 명령어로 컴파일러, 표준 라이브러리, 패키지 매니저까지 모든 것을 자동으로 설치하고 관리해줍니다.

심지어 여러 버전의 Rust를 쉽게 전환할 수도 있어서, 실무에서 레거시 프로젝트와 신규 프로젝트를 동시에 관리할 때도 매우 유용합니다.

개요

간단히 말해서, rustup은 Rust의 공식 툴체인 설치 및 버전 관리 도구입니다. 실무에서 프로젝트마다 다른 Rust 버전을 요구하는 경우가 있습니다.

어떤 프로젝트는 안정적인 stable 버전을 사용하고, 다른 프로젝트는 최신 기능을 테스트하기 위해 nightly 버전을 사용할 수 있죠. rustup을 사용하면 이런 상황에서도 버전을 손쉽게 전환할 수 있어 개발 생산성이 크게 향상됩니다.

기존에 C나 C++에서는 컴파일러를 직접 다운로드하고 환경변수를 수동으로 설정해야 했다면, rustup은 이 모든 과정을 자동화했습니다. 설치부터 업데이트, 버전 관리까지 단일 명령어로 해결할 수 있습니다.

rustup의 핵심 특징은 세 가지입니다. 첫째, 크로스 플랫폼 지원으로 Windows, macOS, Linux에서 동일한 방식으로 작동합니다.

둘째, 툴체인 프로필 관리로 프로젝트별 Rust 버전을 자동 선택합니다. 셋째, 컴포넌트 관리로 필요한 도구들(rust-analyzer, clippy 등)을 간편하게 추가할 수 있습니다.

이러한 특징들이 Rust 개발을 처음 시작하는 개발자들에게 진입 장벽을 크게 낮춰주는 이유입니다.

코드 예제

# Linux/macOS에서 rustup 설치
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Windows에서는 https://rustup.rs 에서 실행 파일 다운로드

# 설치 확인
rustc --version  # Rust 컴파일러 버전 확인
cargo --version  # Cargo 패키지 매니저 버전 확인

# Rust 업데이트
rustup update

# 툴체인 버전 확인
rustup show

설명

이것이 하는 일: rustup은 Rust 개발에 필요한 모든 도구를 설치하고, 여러 버전을 관리하며, 프로젝트별로 적절한 버전을 자동 선택해줍니다. 첫 번째로, curl 명령어를 통해 rustup 설치 스크립트를 다운로드하고 실행합니다.

이 스크립트는 여러분의 운영체제를 자동으로 감지하고, 적절한 Rust 툴체인을 다운로드합니다. --proto와 --tlsv1.2 옵션은 보안 연결을 보장하며, -sSf 옵션은 조용한 모드로 실행하되 오류 시 중단하도록 합니다.

이렇게 하는 이유는 네트워크 문제나 보안 이슈가 있을 때 즉시 설치를 중단하여 시스템을 보호하기 위함입니다. 그 다음으로, 설치가 완료되면 rustc(컴파일러)와 cargo(패키지 매니저)가 자동으로 설치됩니다.

--version 명령어로 각 도구의 버전을 확인하면서 설치가 성공적으로 완료되었는지 검증합니다. 내부적으로는 PATH 환경변수에 ~/.cargo/bin 디렉토리가 추가되어, 어디서든 Rust 도구들을 실행할 수 있게 됩니다.

마지막으로, rustup update 명령어로 언제든지 최신 버전으로 업데이트할 수 있습니다. rustup show 명령어는 현재 설치된 툴체인 정보를 보여주는데, 이를 통해 stable, beta, nightly 중 어떤 버전이 활성화되어 있는지 확인할 수 있습니다.

이렇게 설치하면 Rust 생태계의 모든 도구를 일관되게 관리할 수 있습니다. 여러분이 이 명령어들을 실행하면 단 몇 분 만에 완전한 Rust 개발 환경을 구축할 수 있습니다.

수동으로 컴파일러를 설치하고 환경변수를 설정하는 것에 비해 시간을 90% 이상 절약할 수 있으며, 설정 오류의 가능성도 거의 없습니다. 또한 팀 프로젝트에서 모든 개발자가 동일한 Rust 버전을 사용할 수 있어 "내 컴퓨터에서는 되는데요" 문제를 예방할 수 있습니다.

실전 팁

💡 설치 후 터미널을 재시작해야 PATH가 제대로 적용됩니다. 만약 rustc 명령어가 인식되지 않는다면, source ~/.cargo/env 명령어로 즉시 환경변수를 로드할 수 있습니다.

💡 회사 방화벽 뒤에서 설치 시 프록시 설정이 필요할 수 있습니다. 이 경우 RUSTUP_DIST_SERVER와 RUSTUP_UPDATE_ROOT 환경변수를 회사의 미러 서버로 설정하면 됩니다.

💡 rustup default stable 명령어로 기본 툴체인을 stable로 고정하면, 예상치 못한 nightly 버전 변경으로 인한 빌드 실패를 방지할 수 있습니다.

💡 rustup component add clippy rust-analyzer 명령어로 코드 린터와 IDE 지원 도구를 추가하면 개발 경험이 크게 향상됩니다.

💡 CI/CD 환경에서는 rustup을 캐싱하여 빌드 시간을 단축할 수 있습니다. GitHub Actions의 경우 actions-rs/toolchain 액션을 사용하면 자동으로 캐싱이 적용됩니다.


2. VS Code 설정 - Rust 개발을 위한 최적 환경

시작하며

여러분이 Rust 컴파일러를 설치했다면, 이제 코드를 작성할 에디터가 필요합니다. 물론 메모장에서도 코드를 작성할 수는 있지만, 자동완성, 오류 감지, 디버깅 같은 기능 없이 개발하는 것은 마치 지도 없이 등산하는 것과 같습니다.

특히 Rust는 복잡한 타입 시스템과 소유권 개념을 가지고 있어서, IDE의 도움 없이 개발하면 컴파일 오류를 해결하는 데만 몇 시간씩 소비할 수 있습니다. 실제로 많은 Rust 초보자들이 "왜 이 코드가 컴파일되지 않지?"라는 질문을 반복하며 좌절하곤 합니다.

바로 이럴 때 필요한 것이 제대로 설정된 VS Code입니다. rust-analyzer 확장과 함께 사용하면 실시간 타입 체크, 인라인 오류 표시, 자동 임포트, 리팩토링 도구 등을 활용할 수 있어 개발 생산성이 5배 이상 향상됩니다.

마치 숙련된 Rust 개발자가 옆에서 코드를 검토해주는 것처럼 느껴질 것입니다.

개요

간단히 말해서, VS Code에 rust-analyzer를 설치하면 Rust 개발을 위한 완벽한 IDE 환경을 구축할 수 있습니다. 실무에서 Rust 코드를 작성할 때 가장 많이 겪는 문제는 복잡한 타입 오류와 소유권 규칙 위반입니다.

rust-analyzer는 코드를 작성하는 즉시 이런 오류들을 감지하고, 심지어 수정 방법까지 제안해줍니다. 예를 들어, 변수의 소유권이 이동되었는데 다시 사용하려고 하면, "이 변수는 이미 소유권이 이동되었습니다.

clone()을 사용하거나 참조(&)를 사용하세요"라고 구체적으로 알려줍니다. 기존에 간단한 텍스트 에디터에서 개발했다면 컴파일을 해봐야만 오류를 알 수 있었지만, rust-analyzer를 사용하면 타이핑하는 순간 바로 피드백을 받을 수 있습니다.

이는 개발 사이클을 "작성 → 컴파일 → 오류 확인 → 수정"에서 "작성하면서 즉시 수정"으로 바꿔줍니다. rust-analyzer의 핵심 특징은 세 가지입니다.

첫째, 실시간 코드 분석으로 타입 추론과 오류 감지를 즉시 수행합니다. 둘째, 코드 액션 제공으로 자동 임포트, 메서드 생성, 리팩토링을 한 번의 클릭으로 실행할 수 있습니다.

셋째, 인라인 힌트로 변수 타입과 함수 시그니처를 코드 위에 자동으로 표시해줍니다. 이러한 특징들이 Rust의 높은 학습 곡선을 크게 완화시켜주는 이유입니다.

코드 예제

// VS Code에서 rust-analyzer 설치 후 작동 예시

// 타입이 자동으로 추론되고 표시됩니다
let numbers = vec![1, 2, 3, 4, 5];  // Vec<i32>로 표시됨

// 함수 시그니처가 인라인으로 보입니다
fn calculate_sum(items: &[i32]) -> i32 {
    items.iter().sum()  // sum()에 마우스를 올리면 상세 정보 표시
}

// 오류가 즉시 감지되고 수정 방법이 제안됩니다
let text = String::from("hello");
println!("{}", text);
// let len = text.len();  // 오류: text가 이미 이동됨
// 수정 제안: text.clone() 또는 &text 사용

설명

이것이 하는 일: rust-analyzer는 Rust 코드를 실시간으로 분석하여 타입 정보, 오류, 경고를 IDE에 표시하고, 코드 작성을 도와주는 지능형 제안을 제공합니다. 첫 번째로, VS Code 확장 마켓플레이스에서 "rust-analyzer"를 검색하여 설치합니다.

설치가 완료되면 rust-analyzer는 백그라운드에서 Rust 프로젝트를 분석하기 시작합니다. 이때 Cargo.toml 파일을 기반으로 프로젝트 구조를 파악하고, 모든 의존성을 다운로드하여 인덱싱합니다.

이렇게 하는 이유는 외부 크레이트의 함수와 타입도 자동완성할 수 있게 하기 위함입니다. 그 다음으로, 코드를 작성하면 rust-analyzer가 실시간으로 타입 추론을 수행합니다.

예를 들어 vec![1, 2, 3]을 작성하면, 컴파일러 수준의 분석을 통해 이것이 Vec<i32> 타입임을 즉시 파악하고 화면에 표시합니다. 내부적으로는 Rust 컴파일러의 일부를 라이브러리로 사용하여, 실제 컴파일과 동일한 정확도로 타입을 분석합니다.

함수를 호출할 때는 파라미터 힌트가 자동으로 표시되어, 어떤 타입의 인자를 전달해야 하는지 즉시 알 수 있습니다. 마지막으로, 코드에 오류가 있으면 빨간 밑줄과 함께 상세한 오류 메시지가 표시됩니다.

예를 들어 소유권이 이동된 변수를 다시 사용하려고 하면, 오류 위치를 정확히 가리키고 "Quick Fix" 메뉴를 통해 여러 해결 방법을 제안합니다. 코드 액션 기능을 사용하면 누락된 임포트를 자동으로 추가하거나, 메서드 시그니처를 변경하면 모든 호출 지점을 한 번에 업데이트할 수도 있습니다.

여러분이 rust-analyzer를 사용하면 Rust 학습 속도가 2-3배 빠라집니다. 컴파일 오류 메시지를 해석하는 데 시간을 쓰는 대신, IDE가 즉시 문제를 알려주고 해결책을 제시하기 때문입니다.

또한 실무에서 대규모 코드베이스를 탐색할 때 "정의로 이동", "모든 참조 찾기" 같은 기능으로 코드 이해 시간을 크게 단축할 수 있습니다. 리팩토링 시에도 자동화된 도구들이 실수를 방지해주어 코드 품질을 높게 유지할 수 있습니다.

실전 팁

💡 rust-analyzer 설정에서 "rust-analyzer.checkOnSave.command"를 "clippy"로 변경하면, 저장할 때마다 추가적인 린트 검사를 수행하여 코드 품질을 더욱 향상시킬 수 있습니다.

💡 대규모 프로젝트에서 rust-analyzer가 느리다면, "rust-analyzer.cargo.features"를 설정하여 필요한 feature만 활성화하면 성능이 개선됩니다.

💡 inlay hints가 너무 많아 방해된다면 Ctrl+Shift+P에서 "Inlay Hints: Toggle"으로 켜고 끌 수 있습니다. 학습 초기에는 켜두고, 익숙해지면 끄는 것을 추천합니다.

💡 "Error Lens" 확장을 함께 설치하면 오류 메시지가 코드 라인 끝에 인라인으로 표시되어 더욱 빠르게 오류를 파악할 수 있습니다.

💡 원격 서버에서 개발할 때는 "Remote - SSH" 확장을 사용하되, rust-analyzer를 원격 서버에 설치해야 제대로 작동합니다. 로컬 설치만으로는 작동하지 않습니다.


3. Cargo 프로젝트 생성 - Rust 프로젝트의 시작

시작하며

여러분이 개발 환경을 완벽하게 설정했다면, 이제 실제 프로젝트를 만들 차례입니다. 다른 언어에서는 프로젝트 구조를 직접 만들고, 빌드 스크립트를 작성하고, 의존성 관리 파일을 수동으로 설정해야 했던 경험이 있으실 겁니다.

이런 초기 설정 작업은 생각보다 많은 시간을 소비하게 만듭니다. "프로젝트 구조를 어떻게 구성하지?", "빌드 설정은 어떻게 하지?", "테스트 디렉토리는 어디에 만들지?" 같은 질문들이 실제 코딩을 시작하기도 전에 여러분을 지치게 만들 수 있습니다.

바로 이럴 때 필요한 것이 Cargo의 프로젝트 생성 기능입니다. cargo new 명령어 하나로 표준화된 프로젝트 구조, Cargo.toml 설정 파일, Git 저장소, 심지어 "Hello, World!" 예제까지 모두 자동으로 생성됩니다.

Rust 커뮤니티의 베스트 프랙티스가 이미 적용된 프로젝트 템플릿을 제공하여, 여러분은 초기 설정에 신경 쓰지 않고 바로 비즈니스 로직 구현에 집중할 수 있습니다.

개요

간단히 말해서, Cargo는 Rust의 공식 빌드 도구이자 패키지 매니저로, 프로젝트 생성부터 빌드, 테스트, 배포까지 모든 것을 관리합니다. 실무에서 새로운 Rust 프로젝트를 시작할 때마다 동일한 디렉토리 구조를 만들고 설정 파일을 작성하는 것은 비효율적입니다.

cargo new를 사용하면 단 몇 초 만에 모든 개발자가 동일한 프로젝트 구조로 시작할 수 있어, 팀 내 일관성을 유지하고 코드 리뷰를 쉽게 만듭니다. 예를 들어, 모든 소스 코드는 src/ 디렉토리에, 테스트는 tests/ 디렉토리에 자동으로 구성되어, 팀원 누구나 프로젝트 구조를 직관적으로 이해할 수 있습니다.

기존에 수동으로 프로젝트를 구성했다면 디렉토리 구조, 빌드 스크립트, 의존성 관리를 각각 설정해야 했지만, Cargo는 이 모든 것을 자동화했습니다. npm이나 Maven처럼 단일 명령어로 모든 것을 처리할 수 있습니다.

Cargo의 핵심 특징은 세 가지입니다. 첫째, 규칙 기반 프로젝트 구조로 별도의 설정 없이 src/main.rs 또는 src/lib.rs만 있으면 자동으로 빌드됩니다.

둘째, 통합된 의존성 관리로 Cargo.toml에 크레이트를 추가하면 자동으로 다운로드하고 빌드합니다. 셋째, 빌드 최적화와 캐싱으로 변경된 파일만 다시 컴파일하여 빌드 속도를 최소화합니다.

이러한 특징들이 Rust 개발자들이 Cargo 없이는 개발하기 어렵다고 말하는 이유입니다.

코드 예제

# 바이너리 프로젝트 생성 (실행 파일)
cargo new merkle-tree
cd merkle-tree

# 라이브러리 프로젝트 생성
cargo new --lib merkle-tree-lib

# 프로젝트 빌드 (개발용)
cargo build

# 최적화된 릴리즈 빌드
cargo build --release

# 빌드하고 실행
cargo run

# 테스트 실행
cargo test

설명

이것이 하는 일: Cargo는 프로젝트 템플릿을 생성하고, 소스 코드를 컴파일하며, 의존성을 관리하고, 테스트를 실행하는 등 Rust 개발의 모든 워크플로우를 통합 관리합니다. 첫 번째로, cargo new 명령어를 실행하면 프로젝트 디렉토리와 기본 파일들이 생성됩니다.

바이너리 프로젝트의 경우 src/main.rs가 생성되고, 라이브러리 프로젝트는 --lib 플래그를 사용하여 src/lib.rs가 생성됩니다. 이렇게 하는 이유는 Rust가 프로젝트 타입에 따라 다른 진입점을 사용하기 때문입니다.

동시에 Cargo.toml 파일이 생성되는데, 이 파일은 프로젝트의 메타데이터와 의존성을 정의합니다. Git 저장소도 자동으로 초기화되어 .gitignore 파일까지 포함됩니다.

그 다음으로, cargo build 명령어를 실행하면 Cargo가 Cargo.toml을 읽고 필요한 모든 의존성을 crates.io에서 다운로드합니다. 내부적으로는 Cargo.lock 파일을 생성하여 정확한 의존성 버전을 고정하고, 팀원 모두가 동일한 버전을 사용하도록 보장합니다.

컴파일 과정에서는 rustc 컴파일러를 호출하고, 빌드 결과물을 target/debug/ 디렉토리에 저장합니다. --release 플래그를 사용하면 최적화 옵션이 활성화되어 target/release/에 더 빠르고 작은 바이너리가 생성됩니다.

마지막으로, cargo run은 빌드와 실행을 한 번에 수행합니다. 소스 코드가 변경되었다면 자동으로 다시 빌드하고, 변경되지 않았다면 이전 빌드 결과를 재사용하여 시간을 절약합니다.

cargo test는 프로젝트 내의 모든 테스트 함수(#[test] 어노테이션이 붙은 함수)를 찾아서 실행하고, 결과를 보기 좋게 정리하여 출력합니다. tests/ 디렉토리의 통합 테스트도 자동으로 실행되어, 별도의 테스트 실행 스크립트가 필요 없습니다.

여러분이 Cargo를 사용하면 프로젝트 설정에 소비하는 시간을 거의 0으로 만들 수 있습니다. 새로운 프로젝트를 시작할 때 5-10분이 걸리던 초기 설정이 단 몇 초로 단축됩니다.

또한 표준화된 구조 덕분에 오픈소스 프로젝트를 처음 접할 때도 빠르게 코드베이스를 이해할 수 있습니다. 빌드 캐싱 기능으로 인해 대규모 프로젝트에서도 증분 빌드 시간이 몇 초 이내로 유지되어 개발 사이클이 매우 빨라집니다.

실전 팁

💡 cargo new 대신 cargo init를 사용하면 현재 디렉토리에 프로젝트를 생성합니다. 이미 디렉토리를 만들어놓은 경우 유용합니다.

💡 cargo check 명령어는 컴파일만 하고 실행 파일을 생성하지 않아서 cargo build보다 훨씬 빠릅니다. 타입 오류만 확인하고 싶을 때 사용하세요.

💡 cargo watch를 설치하면(cargo install cargo-watch) 파일이 변경될 때마다 자동으로 빌드나 테스트를 실행하여 개발 경험이 크게 향상됩니다.

💡 Cargo.toml의 [profile.dev] 섹션에서 opt-level = 1을 설정하면 디버그 빌드도 어느 정도 최적화되어 개발 중 프로그램 속도가 개선됩니다.

💡 CI/CD 환경에서는 cargo build --locked를 사용하여 Cargo.lock 파일의 버전을 강제하면, 예상치 못한 의존성 업데이트로 인한 빌드 실패를 방지할 수 있습니다.


4. Cargo.toml 이해하기 - 프로젝트 설정의 중심

시작하며

여러분이 Cargo 프로젝트를 생성하면 가장 먼저 보게 되는 파일이 바로 Cargo.toml입니다. 이 파일을 처음 보면 "이게 뭐지?", "어떻게 수정해야 하지?" 하는 생각이 들 수 있습니다.

특히 다른 언어의 설정 파일과는 형식이 달라서 당황할 수도 있죠. 실무에서 프로젝트를 진행하다 보면 외부 라이브러리를 추가하거나, 프로젝트 메타데이터를 수정하거나, 빌드 설정을 변경해야 하는 경우가 자주 발생합니다.

Cargo.toml을 제대로 이해하지 못하면 이런 작업을 할 때마다 구글링을 해야 하고, 잘못된 설정으로 인해 빌드가 실패하는 상황을 겪을 수 있습니다. 바로 이럴 때 필요한 것이 Cargo.toml에 대한 정확한 이해입니다.

이 파일은 프로젝트의 이름, 버전, 의존성, 빌드 옵션 등 모든 메타데이터를 TOML 형식으로 정의합니다. 각 섹션의 의미를 이해하면 프로젝트를 자유자재로 설정할 수 있고, 팀 프로젝트에서도 의존성 관리를 명확하게 할 수 있습니다.

개요

간단히 말해서, Cargo.toml은 Rust 프로젝트의 매니페스트 파일로, 프로젝트 메타데이터, 의존성, 빌드 설정을 정의합니다. 실무에서 가장 자주 수정하는 부분은 [dependencies] 섹션입니다.

외부 크레이트를 사용하려면 여기에 크레이트 이름과 버전을 추가해야 합니다. 예를 들어, JSON 처리를 위해 serde를 추가하거나, 비동기 프로그래밍을 위해 tokio를 추가하는 경우가 많습니다.

Cargo는 이 정보를 바탕으로 crates.io에서 자동으로 크레이트를 다운로드하고 빌드합니다. 기존에 npm의 package.json이나 Python의 requirements.txt를 사용해봤다면, Cargo.toml이 비슷한 역할을 한다는 것을 쉽게 이해할 수 있습니다.

하지만 Cargo.toml은 의존성뿐만 아니라 프로젝트 타입, 최적화 옵션, feature 플래그 등 더 많은 설정을 포함합니다. Cargo.toml의 핵심 섹션은 네 가지입니다.

첫째, [package]는 프로젝트 이름, 버전, 에디션을 정의합니다. 둘째, [dependencies]는 런타임에 필요한 외부 크레이트를 지정합니다.

셋째, [dev-dependencies]는 개발과 테스트에만 사용되는 크레이트를 지정합니다. 넷째, [profile.*]는 빌드 최적화 수준을 설정합니다.

이러한 섹션들을 적절히 설정하는 것이 효율적인 Rust 프로젝트 관리의 핵심입니다.

코드 예제

[package]
name = "merkle-tree"          # 프로젝트 이름
version = "0.1.0"              # 시맨틱 버저닝
edition = "2021"               # Rust 에디션
authors = ["Your Name <you@example.com>"]

[dependencies]
# 일반 의존성 - 런타임에 필요
sha2 = "0.10"                  # 버전 지정
serde = { version = "1.0", features = ["derive"] }  # feature 활성화

[dev-dependencies]
# 테스트/개발용 의존성
criterion = "0.5"              # 벤치마크 도구

[profile.release]
# 릴리즈 빌드 최적화
opt-level = 3                  # 최대 최적화
lto = true                     # Link Time Optimization

설명

이것이 하는 일: Cargo.toml은 프로젝트의 모든 설정을 한 곳에서 관리하여, Cargo가 프로젝트를 빌드하고 관리하는 데 필요한 정보를 제공합니다. 첫 번째로, [package] 섹션은 프로젝트의 기본 정보를 정의합니다.

name은 프로젝트의 고유 식별자로, 나중에 이 프로젝트를 라이브러리로 공개할 때 crates.io에서 사용될 이름입니다. version은 시맨틱 버저닝(major.minor.patch)을 따르며, API 변경 사항을 추적하는 데 사용됩니다.

edition은 사용할 Rust 언어 버전을 지정하는데, 2021이 현재 권장되는 최신 에디션입니다. 이렇게 하는 이유는 에디션마다 언어 기능과 기본 동작이 다르기 때문에, 프로젝트별로 명시적으로 지정해야 호환성 문제를 방지할 수 있기 때문입니다.

그 다음으로, [dependencies] 섹션에서 외부 크레이트를 지정합니다. sha2 = "0.10"처럼 간단히 버전만 지정할 수도 있고, serde처럼 중괄호를 사용하여 추가 옵션을 설정할 수도 있습니다.

features 옵션은 크레이트의 선택적 기능을 활성화하는데, serde의 derive feature를 활성화하면 자동 직렬화 코드 생성이 가능해집니다. 내부적으로 Cargo는 이 정보를 바탕으로 의존성 그래프를 구성하고, 버전 충돌을 자동으로 해결합니다.

[dev-dependencies]의 크레이트는 cargo test나 cargo bench 실행 시에만 포함되어, 최종 바이너리 크기를 줄일 수 있습니다. 마지막으로, [profile.release] 섹션은 릴리즈 빌드의 최적화 수준을 제어합니다.

opt-level = 3은 최대 최적화를 의미하며, 컴파일 시간은 길어지지만 실행 속도가 크게 향상됩니다. lto = true는 Link Time Optimization을 활성화하여 여러 크레이트를 하나로 통합 최적화하는데, 바이너리 크기와 실행 속도 모두 개선됩니다.

반대로 [profile.dev]는 개발 중 빌드로, 기본적으로 opt-level = 0으로 설정되어 빌드 속도를 우선시합니다. 여러분이 Cargo.toml을 이해하면 프로젝트 설정을 자유롭게 커스터마이즈할 수 있습니다.

의존성 추가가 필요할 때 즉시 할 수 있고, feature 플래그를 활용하여 불필요한 코드를 제외해 바이너리 크기를 줄일 수 있습니다. 또한 workspace 기능을 사용하면 여러 크레이트를 하나의 저장소에서 관리하는 모노레포 구조도 쉽게 구성할 수 있어, 대규모 프로젝트 관리가 훨씬 효율적이 됩니다.

실전 팁

💡 의존성 버전을 "1.0"으로 지정하면 1.x.x 범위의 모든 버전과 호환됩니다. 정확한 버전이 필요하다면 "=1.0.0"처럼 = 기호를 사용하세요.

💡 cargo add serde 명령어를 사용하면 Cargo.toml을 수동으로 편집하지 않고도 의존성을 추가할 수 있습니다. 버전도 자동으로 최신 버전으로 설정됩니다.

💡 Git 저장소의 특정 브랜치를 의존성으로 사용하려면 git = "https://github.com/user/repo" 형식을 사용하세요. 개발 중인 크레이트를 테스트할 때 유용합니다.

💡 [patch.crates-io] 섹션을 사용하면 특정 크레이트를 로컬 버전이나 포크로 임시 교체할 수 있어, 버그 수정이나 기능 추가를 테스트할 때 편리합니다.

💡 optional dependencies를 설정하면 feature 플래그를 통해 선택적으로 의존성을 활성화할 수 있어, 라이브러리의 유연성을 높일 수 있습니다.


5. 프로젝트 구조 이해하기 - 파일과 디렉토리의 역할

시작하며

여러분이 cargo new로 프로젝트를 생성하면 여러 파일과 디렉토리가 자동으로 만들어집니다. 처음 보면 "이 파일들은 뭐지?", "어디에 코드를 작성해야 하지?", "테스트는 어디에 넣지?" 같은 질문이 생깁니다.

특히 다른 언어에서 온 개발자들은 Rust의 프로젝트 구조가 생소하게 느껴질 수 있습니다. 실무에서 프로젝트 구조를 제대로 이해하지 못하면 코드를 잘못된 위치에 작성하거나, 모듈 시스템을 잘못 사용하여 컴파일 오류를 겪게 됩니다.

또한 테스트 코드를 어디에 배치해야 할지, 바이너리와 라이브러리를 어떻게 구분해야 할지 헷갈릴 수 있습니다. 바로 이럴 때 필요한 것이 Rust 프로젝트 구조에 대한 명확한 이해입니다.

Cargo는 "Convention over Configuration" 철학을 따라서, 규칙만 지키면 별도의 설정 없이도 모든 것이 자동으로 작동합니다. src/main.rs는 바이너리 진입점, src/lib.rs는 라이브러리 루트, tests/는 통합 테스트 디렉토리 등 각 파일과 디렉토리의 역할이 명확하게 정의되어 있습니다.

개요

간단히 말해서, Rust 프로젝트는 규칙 기반 디렉토리 구조를 사용하여, 특정 위치에 파일을 배치하면 Cargo가 자동으로 용도를 파악하고 빌드합니다. 실무에서 가장 많이 사용하는 구조는 src/ 디렉토리입니다.

모든 소스 코드는 여기에 위치하며, main.rs가 있으면 바이너리 프로젝트, lib.rs가 있으면 라이브러리 프로젝트로 인식됩니다. 둘 다 있을 수도 있는데, 이 경우 라이브러리 코드를 main.rs에서 사용하는 구조가 됩니다.

예를 들어, 대부분의 비즈니스 로직은 lib.rs에 구현하고, main.rs는 단순히 CLI 인터페이스만 제공하는 식으로 분리할 수 있습니다. 기존에 Java나 Python에서는 패키지 구조를 명시적으로 선언해야 했다면, Rust는 파일 시스템 구조가 곧 모듈 구조가 됩니다.

src/ 아래의 디렉토리와 파일이 자동으로 모듈로 인식됩니다. Rust 프로젝트의 핵심 디렉토리는 다섯 가지입니다.

첫째, src/는 모든 소스 코드를 포함합니다. 둘째, tests/는 통합 테스트를 위한 독립적인 바이너리들을 포함합니다.

셋째, examples/는 라이브러리 사용 예제를 포함합니다. 넷째, benches/는 성능 벤치마크 코드를 포함합니다.

다섯째, target/은 빌드 결과물이 저장되는 곳으로 Git에 커밋하지 않습니다. 이러한 구조를 이해하면 어떤 Rust 프로젝트든 빠르게 파악할 수 있습니다.

코드 예제

# 표준 Rust 프로젝트 구조
merkle-tree/
├── Cargo.toml              # 프로젝트 매니페스트
├── Cargo.lock              # 정확한 의존성 버전 (자동 생성)
├── src/
│   ├── main.rs            # 바이너리 진입점
│   ├── lib.rs             # 라이브러리 루트 (선택)
│   └── merkle/            # 모듈 디렉토리
│       ├── mod.rs         # 모듈 선언
│       └── tree.rs        # 하위 모듈
├── tests/
│   └── integration_test.rs  # 통합 테스트
├── examples/
│   └── basic_usage.rs     # 사용 예제
└── target/                # 빌드 결과물 (Git 제외)

설명

이것이 하는 일: Cargo는 프로젝트 디렉토리 구조를 분석하여 각 파일의 용도를 자동으로 파악하고, 적절한 방식으로 컴파일하고 링크합니다. 첫 번째로, src/main.rs가 있으면 Cargo는 이것을 바이너리 크레이트로 인식합니다.

main.rs에는 반드시 fn main() 함수가 있어야 하며, 이것이 프로그램의 진입점이 됩니다. 여러 바이너리가 필요하다면 src/bin/ 디렉토리에 추가 바이너리를 배치할 수 있는데, 각 파일이 독립적인 실행 파일로 빌드됩니다.

이렇게 하는 이유는 하나의 프로젝트에서 CLI 도구와 서버 데몬 같은 여러 실행 파일을 관리할 수 있게 하기 위함입니다. 그 다음으로, src/lib.rs가 있으면 라이브러리 크레이트로 인식됩니다.

lib.rs는 외부에 공개할 API를 정의하고, pub 키워드를 사용하여 공개 인터페이스를 명시합니다. 내부적으로 src/ 아래의 디렉토리는 자동으로 모듈이 되는데, 예를 들어 src/merkle/ 디렉토리가 있으면 mod merkle;로 선언하여 사용할 수 있습니다.

모듈 디렉토리 안의 mod.rs 파일은 모듈의 루트 역할을 하며, 하위 모듈들을 다시 내보낼 수 있습니다. 마지막으로, tests/ 디렉토리의 각 파일은 별도의 통합 테스트 바이너리로 컴파일됩니다.

이 테스트들은 프로젝트를 외부 라이브러리처럼 사용하므로, 실제 사용자가 경험하는 것과 동일한 방식으로 API를 테스트할 수 있습니다. examples/ 디렉토리의 파일들은 cargo run --example basic_usage처럼 실행할 수 있어, 문서화와 테스트를 동시에 제공합니다.

target/ 디렉토리는 빌드 모드별로 구분되어 target/debug/와 target/release/에 각각 결과물이 저장되며, 이 디렉토리는 언제든 삭제해도 다시 빌드할 수 있습니다. 여러분이 이 구조를 이해하면 프로젝트를 체계적으로 구성할 수 있습니다.

코드를 적절한 모듈로 분리하여 유지보수성을 높이고, 테스트와 예제를 올바른 위치에 배치하여 프로젝트 품질을 향상시킬 수 있습니다. 또한 오픈소스 프로젝트를 처음 접할 때도 이 구조만 알면 어디서부터 코드를 읽어야 할지 즉시 파악할 수 있어, 학습 속도가 크게 빨라집니다.

대규모 프로젝트에서는 workspace를 활용하여 여러 크레이트를 논리적으로 그룹화할 수도 있습니다.

실전 팁

💡 src/bin/ 디렉토리를 사용하면 하나의 프로젝트에서 여러 CLI 도구를 관리할 수 있습니다. 각 파일이 cargo run --bin 명령어로 실행됩니다.

💡 tests/ 디렉토리의 common/ 서브디렉토리는 테스트 바이너리로 빌드되지 않아서, 여러 테스트에서 공유하는 헬퍼 함수를 넣기에 좋습니다.

💡 모듈 파일을 src/merkle.rs 또는 src/merkle/mod.rs 둘 중 하나로 작성할 수 있는데, 하위 모듈이 많다면 디렉토리 방식이 더 깔끔합니다.

💡 .cargo/config.toml 파일을 생성하면 프로젝트별 Cargo 설정(빌드 타겟, 링커 옵션 등)을 커스터마이즈할 수 있습니다.

💡 target/ 디렉토리가 너무 커지면 cargo clean으로 정리하세요. 디스크 공간을 많이 확보할 수 있으며, 다음 빌드 시 처음부터 다시 컴파일됩니다.


6. 첫 번째 Rust 프로그램 작성하기 - Hello, World부터 시작

시작하며

여러분이 개발 환경을 완벽하게 설정하고 프로젝트 구조도 이해했다면, 이제 실제로 코드를 작성할 시간입니다. 새로운 언어를 배울 때 가장 먼저 하는 것이 "Hello, World!" 프로그램이죠.

단순해 보이지만, 이 과정에서 컴파일, 실행, 출력까지의 전체 워크플로우를 경험할 수 있습니다. Rust를 처음 접하는 개발자들은 다른 언어와 문법이 달라서 당황할 수 있습니다.

"fn은 뭐지?", "println!에 느낌표가 왜 붙지?", "세미콜론을 꼭 써야 하나?" 같은 질문들이 생길 수 있습니다. 또한 컴파일 오류 메시지를 처음 보면 어떻게 해결해야 할지 막막할 수도 있습니다.

바로 이럴 때 필요한 것이 간단한 예제로 시작하는 것입니다. cargo new로 생성된 기본 프로젝트에는 이미 "Hello, world!" 예제가 포함되어 있어, 즉시 cargo run으로 실행해볼 수 있습니다.

이 과정에서 Rust의 기본 문법, 함수 선언, 매크로 사용법을 자연스럽게 익히게 되고, 컴파일-실행 사이클에 익숙해질 수 있습니다.

개요

간단히 말해서, Rust의 "Hello, World!" 프로그램은 fn main() 함수와 println! 매크로를 사용하여 콘솔에 텍스트를 출력하는 가장 기본적인 프로그램입니다.

실무에서 새로운 개념을 학습할 때는 항상 가장 단순한 예제부터 시작하는 것이 좋습니다. "Hello, World!" 프로그램은 복잡한 로직 없이 언어의 기본 구조만 보여주기 때문에, Rust의 함수 선언 방식, 매크로 문법, 문자열 리터럴 사용법을 한눈에 파악할 수 있습니다.

예를 들어, println!의 느낌표는 이것이 함수가 아닌 매크로임을 나타내며, Rust의 독특한 메타프로그래밍 기능을 암시합니다. 기존에 C나 Java에서 main 함수를 작성해봤다면, Rust의 문법이 조금 다르다는 것을 알 수 있습니다.

함수 선언에 fn 키워드를 사용하고, 반환 타입이 없으면 생략할 수 있으며, 코드 블록은 중괄호로 감쌉니다. Rust 첫 프로그램의 핵심 요소는 세 가지입니다.

첫째, fn main()은 프로그램의 진입점으로, 모든 실행 가능한 Rust 프로그램에 필수입니다. 둘째, println!은 표준 출력에 텍스트를 출력하는 매크로로, 자동으로 줄바꿈이 추가됩니다.

셋째, 세미콜론은 문장의 끝을 나타내며, 대부분의 경우 필수입니다. 이러한 요소들을 이해하면 Rust 코드의 기본 구조를 파악할 수 있습니다.

코드 예제

// src/main.rs
fn main() {
    // 콘솔에 "Hello, world!" 출력
    println!("Hello, world!");

    // 변수를 사용한 출력
    let name = "Rust";
    println!("Hello, {}!", name);

    // 여러 값을 포맷팅하여 출력
    let version = "1.75";
    println!("{} 버전 {}에 오신 것을 환영합니다!", name, version);
}

설명

이것이 하는 일: main 함수는 프로그램이 실행될 때 자동으로 호출되며, println! 매크로를 사용하여 콘솔에 포맷된 텍스트를 출력합니다.

첫 번째로, fn main() 함수 선언은 Rust 프로그램의 시작점을 정의합니다. fn은 "function"의 약자이고, main은 특별한 이름으로 프로그램이 실행될 때 운영체제가 자동으로 호출합니다.

괄호 ()는 이 함수가 파라미터를 받지 않음을 의미하며, 반환 타입이 명시되지 않았으므로 아무것도 반환하지 않는 void 함수입니다. 이렇게 하는 이유는 간단한 프로그램에서는 복잡한 시그니처가 필요 없기 때문입니다.

그 다음으로, println!("Hello, world!")가 실행됩니다. println!은 느낌표가 붙어 있어서 매크로임을 알 수 있는데, 매크로는 컴파일 타임에 코드를 생성하는 메타프로그래밍 기능입니다.

내부적으로 println!은 std::fmt 모듈을 사용하여 포맷 문자열을 파싱하고, 인자들을 문자열로 변환한 후 표준 출력 스트림에 씁니다. 문자열 끝에 자동으로 줄바꿈 문자(\n)가 추가되어, 다음 출력이 새 줄에서 시작됩니다.

마지막으로, 변수와 포맷 문자열을 결합하여 동적인 출력을 만들 수 있습니다. let name = "Rust"는 불변 변수를 선언하고, println!("{}", name)의 중괄호 {}는 플레이스홀더로 작동하여 변수 값으로 대체됩니다.

여러 플레이스홀더를 사용하면 복잡한 출력도 쉽게 만들 수 있으며, {:?}나 {:#?} 같은 특수 포맷터로 디버깅 정보도 출력할 수 있습니다. 세미콜론은 각 문장의 끝을 표시하며, 생략하면 컴파일 오류가 발생합니다.

여러분이 이 간단한 프로그램을 실행하면 Rust의 전체 워크플로우를 경험할 수 있습니다. cargo run 한 번으로 컴파일과 실행이 동시에 이루어지며, 컴파일 오류가 있으면 친절한 오류 메시지와 함께 수정 방법까지 제안받습니다.

또한 println! 매크로의 강력한 포맷팅 기능을 익히면, 나중에 복잡한 데이터를 출력하고 디버깅하는 데 큰 도움이 됩니다.

이 작은 프로그램이 Rust 여행의 첫 걸음이 되어, 점점 더 복잡한 시스템을 구축하는 기반이 됩니다.

실전 팁

💡 println! 대신 print!를 사용하면 줄바꿈 없이 출력됩니다. 진행 표시줄이나 로딩 애니메이션을 만들 때 유용합니다.

💡 eprintln!을 사용하면 표준 에러 스트림(stderr)에 출력되어, 정상 출력과 에러 메시지를 분리할 수 있습니다. 로깅에 매우 유용합니다.

💡 포맷 문자열에서 {:?}는 Debug 트레이트를, {:#?}는 pretty-print Debug를 사용합니다. 복잡한 구조체를 출력할 때 가독성이 크게 향상됩니다.

💡 cargo run --release를 사용하면 최적화된 버전으로 실행되어, 성능을 테스트할 때 실제에 가까운 결과를 얻을 수 있습니다.

💡 println!의 인자 순서를 바꾸고 싶다면 {0}, {1} 같은 인덱스를 사용하거나, {name} 같은 이름 있는 플레이스홀더를 사용할 수 있습니다.


7. Rust 기본 문법 - 변수와 불변성

시작하며

여러분이 "Hello, World!"를 실행해보셨다면, 이제 Rust의 핵심 개념 중 하나인 변수와 불변성을 이해할 차례입니다. 다른 언어에서 변수를 선언하고 값을 자유롭게 변경해봤다면, Rust의 접근 방식이 처음에는 낯설게 느껴질 수 있습니다.

실무에서 버그의 상당수는 의도치 않은 변수 값 변경에서 발생합니다. "이 변수가 어디서 수정되었지?", "왜 예상과 다른 값이 들어있지?" 같은 질문을 하며 디버깅에 많은 시간을 소비한 경험이 있을 것입니다.

특히 멀티스레드 환경에서는 여러 스레드가 동시에 같은 변수를 수정하려고 할 때 데이터 레이스가 발생할 수 있습니다. 바로 이럴 때 필요한 것이 Rust의 불변성 개념입니다.

Rust는 기본적으로 모든 변수를 불변으로 만들어, 의도하지 않은 변경을 컴파일 타임에 방지합니다. 변수를 변경하려면 명시적으로 mut 키워드를 사용해야 하므로, 코드를 읽는 사람이 "이 변수는 변경될 수 있구나"라고 즉시 알 수 있습니다.

이는 코드의 안전성과 가독성을 동시에 향상시키는 강력한 기능입니다.

개요

간단히 말해서, Rust의 변수는 기본적으로 불변(immutable)이며, 변경 가능하게 만들려면 mut 키워드를 명시적으로 사용해야 합니다. 실무에서 대부분의 변수는 실제로 변경할 필요가 없습니다.

값을 계산하고 저장한 후 읽기만 하는 경우가 훨씬 많죠. Rust는 이런 일반적인 패턴을 기본으로 만들어, 안전한 코드를 작성하도록 유도합니다.

예를 들어, 설정 값이나 계산 결과처럼 한 번 설정되면 변경되지 않아야 하는 데이터는 불변 변수로 선언하여, 실수로 수정하는 것을 방지할 수 있습니다. 기존에 JavaScript나 Python에서는 변수를 선언하면 자유롭게 재할당할 수 있었지만, Rust는 안전성을 위해 명시적인 가변성 선언을 요구합니다.

이는 처음에는 번거로워 보이지만, 버그를 조기에 발견하는 데 큰 도움이 됩니다. Rust 변수의 핵심 특징은 세 가지입니다.

첫째, 기본 불변성으로 let으로 선언된 변수는 재할당이 불가능합니다. 둘째, 명시적 가변성으로 let mut을 사용하면 변경 가능한 변수가 됩니다.

셋째, 타입 추론으로 대부분의 경우 타입을 명시하지 않아도 컴파일러가 자동으로 추론합니다. 이러한 특징들이 Rust 코드의 안전성과 간결성을 동시에 보장하는 이유입니다.

코드 예제

fn main() {
    // 불변 변수 - 기본값
    let x = 5;
    println!("x의 값: {}", x);
    // x = 6;  // 컴파일 오류! 불변 변수는 재할당 불가

    // 가변 변수 - mut 키워드 사용
    let mut y = 10;
    println!("y의 초기값: {}", y);
    y = 20;  // 가능! mut으로 선언됨
    println!("y의 변경값: {}", y);

    // 타입 명시
    let z: i32 = 100;  // i32는 32비트 정수

    // 섀도잉 - 같은 이름으로 새 변수 선언
    let count = 5;
    let count = count + 1;  // 새로운 불변 변수
    println!("count: {}", count);
}

설명

이것이 하는 일: Rust의 변수 시스템은 컴파일 타임에 변수 사용 패턴을 검증하여, 의도하지 않은 데이터 변경을 방지하고 코드의 안전성을 보장합니다. 첫 번째로, let x = 5는 불변 변수 x를 선언하고 5로 초기화합니다.

이후 x = 6처럼 재할당을 시도하면 컴파일러가 즉시 오류를 발생시킵니다. 오류 메시지는 "cannot assign twice to immutable variable"이라고 명확하게 알려주며, "help: consider making this binding mutable: mut x"처럼 해결 방법까지 제안합니다.

이렇게 하는 이유는 개발자가 변수의 의도를 명확히 표현하도록 강제하여, 나중에 코드를 읽는 사람이 이 변수가 변경될 수 있는지 없는지를 즉시 파악할 수 있게 하기 위함입니다. 그 다음으로, let mut y = 10은 가변 변수를 선언합니다.

mut 키워드가 붙으면 y = 20처럼 재할당이 가능해집니다. 내부적으로 컴파일러는 이 변수에 대한 모든 참조를 추적하여, 다른 곳에서 불변 참조를 가지고 있는 동안에는 변경할 수 없도록 보장합니다.

이것이 Rust의 "공유 가능하거나 변경 가능하거나, 둘 중 하나만" 규칙의 기초가 됩니다. 타입 명시는 선택적이지만, let z: i32 = 100처럼 명시하면 코드의 의도를 더 명확하게 표현할 수 있습니다.

마지막으로, 섀도잉(shadowing)은 같은 이름으로 새로운 변수를 선언하는 기능입니다. let count = 5 다음에 let count = count + 1을 하면, 이전 count를 가리는 새로운 불변 변수가 생성됩니다.

이는 재할당과는 다른데, 새 변수를 만드는 것이므로 타입도 변경할 수 있습니다. 예를 들어 let spaces = " "로 문자열을 선언한 후 let spaces = spaces.len()으로 숫자로 변환할 수 있습니다.

이는 일시적인 변환 작업에서 매우 유용하며, mut을 사용하는 것보다 더 명확한 의도를 전달합니다. 여러분이 불변성 개념을 받아들이면 코드 품질이 크게 향상됩니다.

버그를 찾기 위해 "어디서 이 변수가 변경되었을까?"를 추적할 필요가 없어지고, 변수가 변경될 수 있는 범위가 명확히 제한됩니다. 멀티스레드 프로그래밍에서도 불변 데이터는 여러 스레드에서 안전하게 공유할 수 있어, 복잡한 동기화 메커니즘 없이도 안전한 병렬 처리가 가능합니다.

또한 리팩토링 시 변수 사용 패턴을 쉽게 파악할 수 있어, 대규모 코드베이스 유지보수가 훨씬 수월해집니다.

실전 팁

💡 변수를 선언할 때는 일단 let으로 시작하고, 컴파일러가 "재할당이 필요하다"고 오류를 알려주면 그때 mut을 추가하세요. 이렇게 하면 정말 필요한 경우에만 가변 변수를 사용하게 됩니다.

💡 const 키워드로 상수를 선언하면 컴파일 타임에 값이 결정되고, 프로그램 전체에서 동일한 값을 사용합니다. const MAX_POINTS: u32 = 100_000처럼 사용하며, 반드시 타입을 명시해야 합니다.

💡 섀도잉은 입력 값을 여러 단계로 변환할 때 유용합니다. parse()로 문자열을 숫자로 변환하거나, trim()으로 공백을 제거할 때 같은 변수 이름을 재사용할 수 있습니다.

💡 clippy를 실행하면(cargo clippy) 사용하지 않는 mut 선언을 찾아줍니다. 변수를 mut으로 선언했지만 실제로 변경하지 않으면 경고를 표시하여 코드를 깔끔하게 유지할 수 있습니다.

💡 함수 파라미터도 기본적으로 불변입니다. 파라미터를 변경하려면 fn foo(mut x: i32)처럼 mut을 명시해야 하며, 이는 함수 내부에서만 적용되고 호출자의 변수에는 영향을 주지 않습니다.


8. 에디션 이해하기 - Rust 2021의 의미

시작하며

여러분이 Cargo.toml 파일을 보면 edition = "2021"이라는 설정을 발견하게 됩니다. "에디션이 뭐지?", "버전과는 다른 건가?", "왜 필요한 거지?" 같은 의문이 생길 수 있습니다.

처음 보는 개념이라 혼란스러울 수 있죠. 실무에서 프로그래밍 언어가 업데이트되면 기존 코드가 동작하지 않는 breaking change가 발생할 수 있습니다.

Python 2에서 3로 넘어갈 때 많은 프로젝트가 마이그레이션에 큰 어려움을 겪었던 것처럼, 언어의 변화는 개발자들에게 부담이 될 수 있습니다. "최신 기능을 사용하고 싶지만 기존 코드를 다 고쳐야 하나?"라는 고민을 하게 됩니다.

바로 이럴 때 필요한 것이 Rust의 에디션 시스템입니다. 에디션은 Rust가 언어를 발전시키면서도 하위 호환성을 유지하는 독특한 방법입니다.

서로 다른 에디션의 코드가 같은 프로젝트에서 공존할 수 있고, 의존성 크레이트들이 각각 다른 에디션을 사용해도 문제없이 작동합니다. 새로운 에디션은 약 3년마다 출시되며, 개선된 문법과 기본 동작을 제공하면서도 기존 코드를 그대로 사용할 수 있습니다.

개요

간단히 말해서, Rust 에디션은 언어의 메이저 업데이트를 관리하는 시스템으로, 하위 호환성을 깨지 않으면서도 언어를 발전시킬 수 있게 해줍니다. 실무에서 레거시 프로젝트와 신규 프로젝트를 동시에 관리할 때, 각 프로젝트가 다른 에디션을 사용할 수 있다는 것은 큰 장점입니다.

2018 에디션으로 작성된 라이브러리를 2021 에디션 프로젝트에서 문제없이 사용할 수 있습니다. 예를 들어, 회사의 오래된 내부 라이브러리가 2015 에디션을 사용하더라도, 새로운 프로젝트에서 최신 2021 에디션의 기능을 모두 활용할 수 있습니다.

기존에 다른 언어들은 메이저 버전 업그레이드 시 전체 생태계가 분열되는 문제를 겪었지만, Rust는 에디션 시스템으로 이를 해결했습니다. 모든 에디션은 같은 컴파일러로 빌드되며, 바이너리 수준에서 완전히 호환됩니다.

Rust 에디션의 핵심 특징은 세 가지입니다. 첫째, opt-in 방식으로 프로젝트가 명시적으로 에디션을 선택합니다.

둘째, 상호 운용성으로 다른 에디션의 크레이트들이 자유롭게 함께 사용될 수 있습니다. 셋째, 마이그레이션 도구인 cargo fix --edition으로 대부분의 변경 사항을 자동으로 적용할 수 있습니다.

이러한 특징들이 Rust 생태계가 분열 없이 지속적으로 발전할 수 있는 이유입니다.

코드 예제

# Cargo.toml에서 에디션 지정
[package]
name = "merkle-tree"
version = "0.1.0"
edition = "2021"  # Rust 2021 에디션 사용

# 에디션별 주요 변화 예시
# Rust 2021의 주요 기능
fn main() {
    // 2021: 클로저가 자동으로 변수를 캡처하는 방식 개선
    let data = vec![1, 2, 3];
    let closure = || println!("Length: {}", data.len());

    // 2021: 배열을 IntoIterator로 변환 가능
    for item in [1, 2, 3] {  // 2018에서는 &[1, 2, 3] 필요
        println!("{}", item);
    }

    // 2021: 매크로에서 패턴 매칭 개선
}

설명

이것이 하는 일: 에디션 시스템은 각 프로젝트가 사용할 Rust 언어 버전을 명시하여, 새로운 기능과 개선사항을 도입하면서도 기존 코드를 안전하게 보호합니다. 첫 번째로, Cargo.toml의 edition 필드는 해당 프로젝트가 어떤 Rust 언어 규칙을 따를지 결정합니다.

edition = "2021"로 설정하면 2021 에디션의 문법과 기본 동작이 적용됩니다. 이렇게 하는 이유는 언어 변화로 인한 breaking change를 선택적으로 적용할 수 있게 하기 위함입니다.

예를 들어, 2021 에디션에서는 클로저가 변수를 캡처하는 방식이 개선되어 불필요한 move가 줄어들었지만, 이것이 기존 코드의 동작을 미묘하게 변경할 수 있기 때문에 명시적인 opt-in이 필요합니다. 그 다음으로, 서로 다른 에디션의 크레이트들이 함께 컴파일될 때 Rust 컴파일러는 각 크레이트의 에디션 정보를 유지하면서 빌드합니다.

내부적으로는 같은 컴파일러 백엔드를 사용하므로, 생성된 바이너리 코드는 완전히 호환됩니다. 예를 들어, 2018 에디션 라이브러리와 2021 에디션 애플리케이션을 링크할 때 ABI(Application Binary Interface)가 동일하므로 함수 호출, 데이터 구조 공유 등이 자연스럽게 작동합니다.

이는 Rust가 에디션을 구문 수준의 차이로만 취급하고, 의미론적으로는 동일하게 유지하기 때문입니다. 마지막으로, cargo fix --edition 명령어는 코드를 새로운 에디션으로 자동 마이그레이션합니다.

이 도구는 코드를 파싱하고, 에디션 간 차이를 분석하여, 자동으로 수정 가능한 부분은 변경하고 수동 수정이 필요한 부분은 경고를 표시합니다. 예를 들어, 2018에서 2021로 마이그레이션할 때 배열 순회 코드의 &를 제거하거나, 예약어가 된 단어를 raw identifier(r#async)로 변환하는 작업을 자동으로 수행합니다.

대부분의 코드베이스는 거의 수동 작업 없이 새 에디션으로 전환할 수 있습니다. 여러분이 에디션 개념을 이해하면 Rust 생태계의 장기 전략을 파악할 수 있습니다.

새로운 프로젝트에서는 항상 최신 에디션을 사용하여 개선된 기능과 더 나은 개발 경험을 얻을 수 있습니다. 레거시 코드를 유지보수할 때는 급하게 마이그레이션하지 않아도 되며, 필요할 때 점진적으로 업데이트할 수 있습니다.

또한 오픈소스 라이브러리를 사용할 때 에디션 차이로 인한 호환성 문제를 걱정할 필요가 없어, 안심하고 다양한 크레이트를 조합할 수 있습니다.

실전 팁

💡 새 프로젝트는 항상 최신 에디션(현재는 2021)으로 시작하세요. 레거시 에디션은 기존 코드 유지보수용으로만 사용하는 것이 좋습니다.

💡 cargo fix --edition --allow-dirty를 사용하면 커밋되지 않은 변경사항이 있어도 마이그레이션을 진행할 수 있지만, 먼저 백업하거나 Git으로 커밋하는 것이 안전합니다.

💡 에디션 가이드(https://doc.rust-lang.org/edition-guide/)에서 각 에디션의 변경사항을 상세히 확인할 수 있습니다. 마이그레이션 전에 꼭 읽어보세요.

💡 의존성 크레이트가 오래된 에디션을 사용해도 걱정하지 마세요. 에디션은 컴파일 경계에서만 의미가 있으며, 런타임 동작이나 성능에는 영향을 주지 않습니다.

💡 2024 에디션이 곧 출시될 예정이므로, Rust 블로그를 주시하여 새로운 기능과 마이그레이션 가이드를 확인하세요. 보통 안정화 후 6개월 정도는 기다렸다가 업데이트하는 것이 안전합니다.


#Rust#rustup#Cargo#환경설정#개발도구#rust

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.