[Rust 가이드] 12.7. 환경 변수 사용

발행: (2026년 5월 24일 AM 07:18 GMT+9)
10 분 소요
원문: Dev.to

Source: Dev.to

12.7.0 시작하기 전에

12장에서는 샘플 프로젝트를 구축합니다: 명령줄 프로그램. 이 프로그램은 grep(Global Regular Expression Print)이며, 전역 정규식 검색 및 출력을 위한 도구입니다. 지정된 파일에서 지정된 텍스트를 검색하는 것이 기능입니다.
이 프로젝트는 다음 단계로 나뉩니다:

  • 명령줄 인수 받기
  • 파일 읽기
  • 리팩터링: 모듈 및 오류 처리 개선
  • TDD(테스트 주도 개발)를 사용해 라이브러리 기능 구현
  • 환경 변수 사용(이번 글)
  • 표준 출력이 아닌 표준 오류에 오류 메시지 쓰기

이 글이 도움이 되었다면 좋아요, 북마크, 팔로우를 눌러 주세요. 시리즈를 따라가며 계속 학습하시길 바랍니다.

아래는 이전 글까지 작성된 전체 코드입니다.

lib.rs:

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result {
        if args.len()  Result> {
    let contents = fs::read_to_string(config.filename)?;
    for line in search(&config.query, &contents) {
        println!("{}", line);
    }
    Ok(())
}

pub fn search(query: &str, contents: &'a str) -> Vec {
    let mut results = Vec::new();
    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }
    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";
        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

main.rs:

use std::env;
use std::process;
use minigrep::Config;

fn main() {
    let args: Vec = env::args().collect();
    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });
    if let Err(e) = minigrep::run(config) {
        println!("Application error: {}", e);
        process::exit(1);
    }
}

이전 섹션에서는 비즈니스 로직을 lib.rs로 옮기는 작업을 마쳤습니다. 이는 테스트 작성에 매우 유용합니다. lib.rs에 있는 로직은 명령줄에서 프로그램을 실행하지 않고도 다양한 인수로 직접 호출하고 반환값을 확인할 수 있기 때문입니다. 즉, 비즈니스 로직을 직접 테스트할 수 있습니다.

TDD는 Test‑Driven Development의 약자로 보통 다음 순서를 따릅니다:

  1. 실패하는 테스트를 작성하고 실행해, 기대한 이유로 실패하는지 확인한다.
  2. 새로운 테스트를 통과시키기에 충분한 최소 코드를 작성하거나 수정한다.
  3. 방금 추가·수정한 코드를 리팩터링하고, 테스트가 여전히 통과하는지 확인한다.
  4. 1단계로 돌아가 계속 진행한다.

TDD는 여러 소프트웨어 개발 방법 중 하나에 불과하지만, 코드 설계를 안내하고 지원하는 데 도움이 됩니다. 테스트를 먼저 작성하고 이를 통과하는 코드를 작성하면 개발 과정에서 높은 테스트 커버리지를 유지할 수 있습니다.

이번 글에서는 TDD를 활용해 프로그램의 검색 로직을 구현합니다: 파일 내용에서 지정된 문자열을 찾아 일치하는 줄을 리스트에 넣는 함수 search_case_insensitive를 만들 것입니다.

우선 대소문자를 구분하지 않는 함수 이름을 search_case_insensitive로 정합니다.

먼저 테스트 모듈을 수정해 대소문자를 구분하는 테스트와 구분하지 않는 테스트를 모두 포함하도록 합니다:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

그 다음 search_case_insensitive의 몸체를 작성합니다:

pub fn search_case_insensitive(query: &str, contents: &'a str) -> Vec {
    vec![]
}

외부에서 호출할 수 있게 하려면 pub으로 선언해야 합니다.

함수에 라이프타임 어노테이션이 필요한 이유는 매개변수가 여러 개이고, 반환값의 라이프타임이 어느 매개변수와 맞아야 할지 러스트가 판단할 수 없기 때문입니다. 반환되는 Vec의 요소는 contents에서 가져온 문자열 슬라이스이므로 반환값은 contents와 같은 라이프타임을 가져야 합니다. 따라서 두 매개변수와 반환값에 동일한 'a 라이프타임을 붙이고, query는 라이프타임이 필요 없습니다.

함수 몸체는 아직 컴파일만 되면 되며, TDD의 첫 단계는 실패하는 테스트를 만드는 것이므로 현재는 실패가 의도된 결과입니다.

이 시점에서 테스트를 실행하면 확실히 실패하지만, 괜찮습니다. 바로 TDD 첫 단계가 기대하는 상황이기 때문입니다.

search_case_insensitive 코드는 search와 매우 유사하므로 몇 가지 작은 변경만 하면 됩니다. 로직은 간단합니다: query를 소문자로 바꾸고, 텍스트의 각 줄을 소문자로 변환해 비교합니다:

pub fn search_case_insensitive(query: &str, contents: &'a str) -> Vec {
    let mut results = Vec::new();
    let query = query.to_lowercase();
    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }
    results
}

to_lowercase 메서드는 문자열을 모두 소문자로 변환합니다.
to_lowercase의 결과는 String이므로 새 query&str이 아니라 소유된 String이 됩니다. 루프 안에서는 containsString을 직접 받지 못하므로 &query를 전달합니다.

다시 테스트를 실행합니다:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `test` profile [unoptimized + debuginfo] target

(s) in 1.33s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests minigrep

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

두 테스트 모두 통과했습니다.


함수가 정상 동작하므로 run 함수 안에서 호출하도록 수정합니다. 먼저 Config 구조체에 일반 검색을 사용할지 대소문자를 구분하지 않는 검색을 사용할지 결정할 수 있는 필드를 추가합니다:

pub struct Config {
    pub query: String,
    pub filename: String,
    pub case_sensitive: bool,
}

run을 수정해 설정을 확인하도록 합니다:

pub fn run(config: Config) -> Result> {
    let contents = fs::read_to_string(config.filename)?;
    let results = if config.case_sensitive {
        search(&config.query, &contents)
    } else {
        search_case_insensitive(&config.query, &contents)
    };
    for line in results {
        println!("{}", line);
    }
    Ok(())
}

새로운 Config 생성자도 환경 변수에 따라 case_sensitive 값을 설정하도록 바꿔야 합니다:

impl Config {
    pub fn new(args: &[String]) -> Result {
        if args.len()  Result {
        if args.len()  Result> {
    let contents = fs::read_to_string(config.filename)?;
    let results = if config.case_sensitive {
        search(&config.query, &contents)
    } else {
        search_case_insensitive(&config.query, &contents)
    };
    for line in results {
        println!("{}", line);
    }
    Ok(())
}

(코드가 중복되어 보이지만 원문 그대로 유지했습니다.)


아래는 전체 코드 스니펫을 정리한 것입니다.

search:

pub fn search(query: &str, contents: &'a str) -> Vec {
    let mut results = Vec::new();
    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }
    results
}

search_case_insensitive:

pub fn search_case_insensitive(query: &str, contents: &'a str) -> Vec {
    let mut results = Vec::new();
    let query = query.to_lowercase();
    for line in contents.lines() {
        // (본문에 이어지는 구현은 원문을 따릅니다)
    }
}
0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.