Rust 실전 강좌 #1 프로젝트 설계와 clap: 서브커맨드 CLI의 뼈대 잡기

4 분 소요

기초 강좌 9편에서 소유권부터 수명까지 문법의 등뼈를 세웠습니다. 이번 실전 강좌는 그 문법으로 실제 도구 하나를 처음부터 끝까지 만드는 8편입니다. 설계와 인자 파싱에서 시작해 에러 설계, 파일 IO, 이터레이터 집계, 병렬화, 테스트를 거쳐 릴리스 빌드와 배포까지, “동작하는 코드"가 아니라 “남에게 설치하라고 말할 수 있는 도구"가 목표입니다.

무엇을 만드나: 액세스 로그 분석기 loglens #

만들 것은 웹 서버 액세스 로그를 분석하는 CLI 도구입니다. 이름은 loglens로 하겠습니다.

사용 예시
loglens stats access.log            # 요청 수, 상태 코드 분포
loglens top access.log --count 10   # 요청 많은 경로 상위 10개
loglens errors access.log           # 5xx 라인만 추출

소재를 로그 분석으로 고른 데는 이유가 있습니다. 수백만 줄짜리 파일을 다루므로 이터레이터와 병렬화의 성능 이야기가 장식이 아니라 본론이 되고, 입력이 항상 지저분하므로(깨진 라인, 예상 밖 형식) 에러 처리가 실전 그 자체가 됩니다. 그리고 CLI 도구는 Rust의 대표 무대입니다. ripgrep과 uv가 보여 줬듯, 런타임 설치 없는 단일 바이너리와 밀리초 단위의 시작 속도는 CLI에서 가장 빛나는 강점입니다.

프로젝트 생성과 clap 도입 #

프로젝트 셋업
cargo new loglens
cd loglens
cargo add clap --features derive

인자 파싱은 표준 라이브러리로도 가능하지만, 실무의 사실상 표준은 clap입니다. --help 자동 생성, 타입 변환, 오타 제안까지 포함해서, 인자 파싱에 쓸 시간을 도구의 본론에 돌려줍니다. --features derive는 구조체 선언으로 CLI를 정의하는 방식을 켜는 스위치입니다.

인터페이스를 타입으로 선언한다 #

clap의 derive 방식에서 CLI 인터페이스는 곧 타입 정의입니다.

src/main.rs
use clap::{Parser, Subcommand};
use std::path::PathBuf;

#[derive(Parser)]
#[command(version, about = "액세스 로그 분석 도구")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// 요청 수와 상태 코드 분포를 요약한다
    Stats { file: PathBuf },
    /// 요청이 많은 경로 상위 N개를 보여 준다
    Top {
        file: PathBuf,
        #[arg(long, default_value_t = 10)]
        count: usize,
    },
    /// 서버 에러(5xx) 라인만 추출한다
    Errors { file: PathBuf },
}

기초 5편에서 “열거형은 여러 모습 중 하나를 표현한다"고 했는데, 서브커맨드가 정확히 그 구조입니다. 사용자는 stats, top, errors 중 하나를 고르고, 각 선택지는 서로 다른 인자를 갖습니다. clap은 이 열거형 선언에서 파서와 도움말을 전부 만들어 냅니다. /// 문서 주석이 그대로 --help의 설명문이 되는 것도 눈여겨볼 부분입니다.

src/main.rs
fn main() {
    let cli = Cli::parse();

    match cli.command {
        Commands::Stats { file } => {
            println!("stats: {}", file.display());
            todo!("3편에서 구현");
        }
        Commands::Top { file, count } => {
            println!("top {count}: {}", file.display());
            todo!("4편에서 구현");
        }
        Commands::Errors { file } => {
            println!("errors: {}", file.display());
            todo!("3편에서 구현");
        }
    }
}

Cli::parse() 한 줄이 인자 파싱 전부입니다. 잘못된 인자는 clap이 에러 메시지와 함께 종료 코드 2로 알아서 처리하고, match는 기초 5편의 규칙대로 세 서브커맨드의 처리를 전부 강제합니다. 나중에 서브커맨드를 추가하면 이 match가 컴파일 에러로 “처리 안 한 곳"을 알려 줄 것입니다. todo!()는 “타입은 맞지만 아직 구현 안 함"을 선언하는 매크로로, 뼈대를 먼저 세우고 살을 붙이는 이 시리즈의 진행 방식과 정확히 맞습니다.

실행해 보기 #

실행
$ cargo run -- --help
액세스 로그 분석 도구

Usage: loglens <COMMAND>

Commands:
  stats   요청 수와 상태 코드 분포를 요약한다
  top     요청이 많은 경로 상위 N개를 보여 준다
  errors  서버 에러(5xx) 라인만 추출한다

cargo run -- 뒤의 인자가 프로그램에 전달됩니다. 선언만 했는데 도움말, 버전 표시(--version), 인자 검증이 전부 동작합니다. loglens top access.log --count 5처럼 옵션까지 파싱되고, count는 이미 usize 타입입니다. 문자열을 숫자로 바꾸는 코드는 한 줄도 쓰지 않았습니다.

8편의 지도 #

  • #1 설계와 clap ← 이번 글
  • #2 에러 설계 — anyhow와 thiserror, 죽는 방법을 설계한다
  • #3 파일 IO와 파싱 — BufReader, 로그 한 줄을 구조체로
  • #4 이터레이터 실전 — 집계, top-N, release 빌드의 차이
  • #5 rayon 병렬화 — 데이터 레이스 없는 map-reduce
  • #6 테스트 — 파서 단위 테스트에서 CLI 통합 테스트까지
  • #7 릴리스 최적화와 크로스 컴파일
  • #8 배포 — crates.io와 GitHub Releases 자동화

정리 #

  • 실전 소재는 액세스 로그 분석 CLI입니다. 대용량 입력과 지저분한 입력이라는 성질이 성능·에러 처리 학습의 실전 무대가 됩니다.
  • CLI 인터페이스는 clap derive로 타입 선언이 됩니다. 서브커맨드는 열거형이고, match가 처리 누락을 컴파일 타임에 막아 줍니다.
  • /// 문서 주석이 곧 --help 문안입니다. 선언 하나로 파싱, 검증, 도움말, 버전 표시까지 얻습니다.
  • todo!()로 뼈대를 먼저 세웠습니다. 타입이 맞는 뼈대는 이후 편에서 안심하고 살을 붙일 수 있는 토대입니다.
  • 다음 편은 에러 설계입니다. 파일이 없을 때, 형식이 다를 때 이 도구가 어떻게 죽고 어떻게 알릴지를 anyhow와 thiserror로 정리하겠습니다.
X