오류를 표준 출력 대신 표준 에러로 보내기
현재 우리는 모든 출력을 println! 매크로로 터미널에 쓰고 있습니다. 대부분의
터미널에는 두 종류의 출력 스트림이 있습니다. 일반 정보를 위한 표준 출력
(stdout) 과, 에러 메시지를 위한 표준 에러 (stderr) 입니다. 이 구분 덕분에
사용자는 프로그램의 성공적인 출력은 파일로 보내면서도, 에러 메시지는 화면에 그대로
보게 할 수 있습니다.
하지만 println! 매크로는 표준 출력으로만 쓸 수 있으므로, 표준 에러로 출력하려면
다른 것을 사용해야 합니다.
오류가 어디에 쓰이는지 확인하기
먼저, minigrep 가 출력하는 내용이 현재는 어떻게 모두 표준 출력으로 가는지
확인해 봅시다. 여기에는 사실 표준 에러로 보내고 싶은 오류 메시지들도 포함되어
있습니다. 이를 확인하기 위해 표준 출력 스트림을 파일로 리다이렉트하면서, 의도적으로
에러를 발생시켜 보겠습니다. 표준 에러 스트림은 리다이렉트하지 않을 것이므로, 만약
거기로 가는 내용이 있다면 계속 화면에 보일 것입니다.
커맨드라인 프로그램은 에러 메시지를 표준 에러로 보내는 것이 기대되는 동작입니다. 그래야 사용자가 표준 출력을 파일로 리다이렉트하더라도 에러 메시지는 화면에서 볼 수 있기 때문입니다. 하지만 우리 프로그램은 아직 그렇게 “예의 바르게” 동작하지 않습니다. 곧 에러 메시지마저 파일에 저장해 버린다는 사실을 보게 됩니다!
이를 확인하기 위해, 프로그램을 > 와 함께 실행하고 표준 출력을 output.txt 로
리다이렉트해 보겠습니다. 인수는 하나도 주지 않을 것이므로 프로그램은 에러를
발생시켜야 합니다.
$ cargo run > output.txt
> 문법은 셸에게 표준 출력 내용을 화면 대신 output.txt 로 쓰라고 지시합니다.
우리가 기대한 에러 메시지가 화면에 보이지 않았으므로, 그 메시지는 파일 안으로 들어간
것이어야 합니다. 실제 output.txt 내용은 다음과 같습니다.
Problem parsing arguments: not enough arguments
맞습니다. 에러 메시지가 표준 출력으로 찍히고 있습니다. 이런 종류의 에러 메시지는 성공적인 실행 결과만 파일 안에 남도록 표준 에러로 보내는 편이 훨씬 더 유용합니다. 이제 그렇게 바꿔 봅시다.
오류를 표준 에러로 출력하기
목록 12-24의 코드를 사용해 에러 메시지 출력 방식을 바꿉니다. 이 장 앞부분의
리팩터링 덕분에, 에러 메시지를 출력하는 코드는 모두 main 함수 한곳에 모여 있습니다.
표준 라이브러리는 표준 에러 스트림에 출력하는 eprintln! 매크로를 제공하므로,
우리가 에러를 위해 println! 을 호출하던 두 위치를 eprintln! 으로 바꾸면 됩니다.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
eprintln! 으로 표준 출력 대신 표준 에러에 오류 메시지 쓰기이제 다시 같은 방식으로, 아무 인수 없이 프로그램을 실행하고 > 로 표준 출력을
리다이렉트해 봅시다.
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
이제는 에러가 화면에 보이고, output.txt 안에는 아무 것도 없습니다. 이것이 커맨드라인 프로그램에 기대되는 동작입니다.
이번에는 에러를 일으키지 않는 인수를 넣되, 여전히 표준 출력만 파일로 리다이렉트해서 실행해 봅시다.
$ cargo run -- to poem.txt > output.txt
터미널에는 아무 출력도 보이지 않을 것이고, output.txt 안에는 검색 결과가 들어 있게 됩니다.
파일명: output.txt
Are you nobody, too?
How dreary to be somebody!
이로써 우리는 성공적인 출력에는 표준 출력을, 오류 메시지에는 표준 에러를 적절히 사용하게 되었습니다.
정리
이 장에서는 지금까지 배운 주요 개념들을 다시 활용하면서, 러스트에서 흔한 I/O 작업을
어떻게 수행하는지도 함께 살펴봤습니다. 명령줄 인수, 파일, 환경 변수, 그리고 에러
출력을 위한 eprintln! 매크로를 사용함으로써, 이제 여러분은 커맨드라인 애플리케이션을
작성할 준비가 되었습니다. 앞 장의 개념들과 합쳐 보면, 여러분의 코드는 잘 조직되어
있고, 적절한 자료구조에 데이터를 효과적으로 저장하며, 에러도 깔끔하게 처리하고,
테스트도 잘 갖춘 상태가 됩니다.
다음 장에서는 함수형 언어에서 영향을 받은 러스트 기능인 클로저와 반복자를 살펴보겠습니다.