Rust로 CLI 도구 만들며 배운 소유권 시스템

배경

팀 내에서 반복적으로 사용하는 프로젝트 구조를 자동으로 생성하는 CLI 도구가 필요했다. Node.js로 만들 수도 있었지만, 최근 몇 달간 틈틈이 공부한 Rust를 실전에 적용해보기로 했다.

소유권과의 첫 만남

파일 경로를 다루는 간단한 함수에서 첫 컴파일 에러를 만났다.

fn process_path(path: String) {
    println!("Processing: {}", path);
}

fn main() {
    let project_path = String::from("./my-project");
    process_path(project_path);
    println!("Path was: {}", project_path); // error: value borrowed after move
}

Node.js였다면 아무 문제 없는 코드다. Rust에서는 process_pathproject_path를 넘기는 순간 소유권이 이동하고, 이후엔 사용할 수 없다.

해결 방법은 세 가지였다:

  1. 참조 사용 (&String)
  2. 클론 생성 (project_path.clone())
  3. 소유권 반환

대부분의 경우 참조로 충분했다.

fn process_path(path: &String) {
    println!("Processing: {}", path);
}

fn main() {
    let project_path = String::from("./my-project");
    process_path(&project_path);
    println!("Path was: {}", project_path); // OK
}

실용적인 패턴

CLI 도구를 만들며 자주 사용한 패턴:

1. 함수 파라미터는 &str 선호

// String보다 &str이 더 유연
fn create_file(name: &str) -> std::io::Result<()> {
    std::fs::write(name, "content")?;
    Ok(())
}

2. 구조체 필드는 소유 타입

struct Config {
    project_name: String,  // &str 아님
    template_dir: PathBuf,
}

3. 에러 처리는 ? 연산자

fn setup_project(name: &str) -> Result<(), Box<dyn std::error::Error>> {
    let path = create_directory(name)?;
    copy_template(&path)?;
    initialize_git(&path)?;
    Ok(())
}

느낀 점

컴파일러와 대화하는 느낌이 새로웠다. 에러 메시지가 상세해서 대부분의 소유권 문제는 메시지를 읽고 해결할 수 있었다. 컴파일만 되면 런타임 에러가 거의 없다는 점이 인상적이었다.

완성된 CLI는 단일 바이너리로 배포 가능하고, 실행 속도도 Node.js 버전 대비 체감상 빨랐다. 러닝 커브가 있지만 시스템 도구나 성능이 중요한 유틸리티에는 Rust가 좋은 선택지라고 생각한다.