Rust 실전 강좌 #1 프로젝트 설계와 clap: 서브커맨드 CLI의 뼈대 잡기
기초 강좌 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 인터페이스는 곧 타입 정의입니다.
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의 설명문이 되는 것도 눈여겨볼 부분입니다.
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로 정리하겠습니다.