본문 바로가기

표준 에러 출력

정상 검색 결과와 오류 메시지를 stdout·stderr로 분리해 파이프와 리다이렉션에서도 명령줄 도구가 올바르게 동작하게 합니다.

이 시점에서는 터미널로 출력되는 모든 것이 println! 매크로를 사용하여 작성되고 있는 상태입니다.

대부분의 터미널에는 두 종류의 출력이 있습니다.

범용적인 정보를 위한 표준 출력(standard output) (stdout)과 에러 메시지를 위한 표준 에러(standard error) (stderr) 두 가지죠.

이러한 구분은 사용자로 하여금 성공한 프로그램의 출력값을 파일로 향하게끔 하지만 에러 메시지는 여전히 화면에 나타나도록 해줄 수 있습니다.

println! 매크로는 표준 출력으로의 출력 기능만 있으므로, 표준 에러로 출력하기 위해서는 다른 무언가를 사용해야 합니다.

RUST · MINIGREP

검색 결과·진단·종료 상태는 따로 확인한다

이 절은 두 오류 출력만 eprintln!로 바꿉니다. 성공 데이터는 stdout으로, 실패 설명은 stderr로 보내며 main의 정상 반환과 process::exit(1)이 상태를 정합니다.

CLI의 세 채널println 결과는 stdout 리다이렉션, eprintln 오류는 stderr 진단, 정상 반환과 exit1은 종료 상태를 결정한다. 세 채널의 화면 순서를 계약으로 삼지 않는다.
println!: 일치한 줄stdout · 검색 데이터
연결 관계println!: 일치한 줄 · stdout · 검색 데이터 → 파일·다음 명령 · stdout 리다이렉션·파이프
파일·다음 명령stdout 리다이렉션·파이프
eprintln!: 오류stderr · 실패 설명
연결 관계eprintln!: 오류 · stderr · 실패 설명 → 터미널·별도 로그 · stderr를 독립적으로 관찰
터미널·별도 로그stderr를 독립적으로 관찰
정상 반환 / exit(1)종료 상태 · 성공 / 실패
연결 관계정상 반환 / exit(1) · 종료 상태 · 성공 / 실패 → 셸·CI · 상태 코드로 분기
셸·CI상태 코드로 분기
stdout·stderr·종료 상태는 독립 채널여섯노드세관계의독립된쌍. println검색줄→stdout파일/다음명령, eprintln오류→stderr별도로그, 정상반환/exit1→셸의종료상태이다. stdout목적지에서stderr발생으로가는간선이나화면출력순서를보장하는간선은없다. 오류문구출력과상태1은별도작업이며정상일치없음은빈stdout/오류없음/상태0일수있다.
println!: 일치한 줄stdout · 검색 데이터
연결 관계println!: 일치한 줄 · stdout · 검색 데이터 → 파일 · 다음 명령 · stdout 리다이렉션
파일 · 다음 명령stdout 리다이렉션
eprintln!: 오류stderr · 실패 설명
연결 관계eprintln!: 오류 · stderr · 실패 설명 → 터미널 · 별도 로그 · stderr 독립 관찰
터미널 · 별도 로그stderr 독립 관찰
정상 반환 / exit(1)종료 상태
연결 관계정상 반환 / exit(1) · 종료 상태 → 셸 · CI · 상태 코드로 분기
셸 · CI상태 코드로 분기

세 채널은 독립 관측값

stdout · 검색 결과

println!의 일치한 줄은 파일 리다이렉션이나 파이프의 입력입니다.

stderr · 오류 진단

eprintln!의 인수·파일 오류는 별도 스트림입니다. stdout만 리다이렉션해도 남습니다.

상태 · 셸과 CI 판단

main 정상 반환은 0, 처리된 인수·파일 오류는 process::exit(1)로 1입니다. 결과 스트림과 별도로 검사합니다.

인수 누락

검색 데이터는 비어 있고 stderr에 인수 진단이 기록되며 상태는 1입니다. stdout 파일로 진단이 섞이지 않습니다.

정상 검색 · to

Are you nobody, too?
How dreary to be somebody!

IGNORE_CASE를 해제한 본문의 검색은 stdout에 두 줄, minigrep의 stderr는 비어 있고 상태는 0입니다.

정상 검색 · 일치 없음

이 minigrep 구현은 stdout이 비어도 오류가 아니므로 상태 0입니다. 다른 grep 도구의 상태 규칙과는 다를 수 있습니다.

cargo run의 Cargo 로그는 minigrep의 출력과 별개입니다. 빌드된 실행 파일을 자식 프로세스로 직접 호출하면 세 채널을 분리해 검사할 수 있습니다. stdout·stderr의 화면 표시 순서는 보장하지 않습니다.


에러가 기록되었는지 검사하기

먼저 minigrep이 출력하는 내용들이 현재 표준 에러 쪽에 출력하고 싶은 에러 메시지를 포함하여 어떤 식으로 표준 출력에 기록되는지 관찰해봅시다.

의도적으로 에러를 발생시키면서 표준 출력 스트림을 파일 쪽으로 리디렉션하여 이를 확인할 것입니다.

표준 에러 스트림은 리디렉션하지 않을 것이므로 표준 에러 쪽으로 보내진 내용들은 계속 화면에 나타날 것입니다.

커맨드 라인 프로그램은 표준 에러 스트림 쪽으로 에러 메시지를 보내야 하므로 표준 출력 스트림이 파일로 리디렉션되더라도 여전히 에러 메시지는 화면에서 볼 수 있습니다.

이 프로그램은 현재 제대로 동작하지 않습니다.

이제 에러 메시지가 화면 대신 파일 쪽에 저장되는 것을 확인해 보겠습니다.

이 동작을 확인해 보기 위해서 프로그램을 >와 파일 경로 output.txt와 함께 실행해 보려 하는데, 이 파일 경로는 표준 출력 스트림이 리디렉션될 곳입니다.

아무런 인수를 넣지 않을 것인데, 이는 에러를 발생시켜야 합니다.

$ cargo run > output.txt

> 문법은 셸에게 표준 출력의 내용을 화면 대신 output.txt에 작성하라고 알려줍니다.

화면에 출력되리라 기대되었던 에러 메시지는 보이지 않으므로, 이는 결국 파일 안으로 들어갔음에 틀림없다는 의미입니다.

아래는 output.txt에 담긴 내용입니다.

Problem parsing arguments: not enough arguments

네, 에러 메시지가 표준 출력에 기록되고 있네요.

이런 종류의 에러 메시지는 표준 에러로 출력되게 함으로써 성공적인 실행으로부터 나온 데이터만 파일로 향하게 만드는 것이 훨씬 유용합니다.

그렇게 바꿔보겠습니다.


표준 에러로 에러 출력하기

에러 메시지 출력 방식을 변경하기 위해 예제 11-24의 코드를 사용해 보겠습니다.

이 장에서 앞서 했던 리팩터링 덕분에 에러 메시지를 출력하는 모든 코드는 단 하나의 함수 main 안에 있습니다.

표준 라이브러리는 표준 에러 스트림으로 출력하는 eprintln! 매크로를 제공하므로, 에러 출력을 위해 println!을 호출하고 있는 두 군데를 eprintln!로 바꿔봅시다.

예제 11-24: eprintln!를 사용하여 표준 출력 대신 표준 에러로 에러 메시지 작성하기
src/main.rs
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) = minigrep::run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

이제 동일한 방식, 즉 아무런 인수 없이 >로 표준 출력을 리디렉션하여 프로그램을 다시 실행해봅시다.

$ cargo run > output.txt
Problem parsing arguments: not enough arguments

이제는 에러가 화면에 보이고 output.txt에는 아무것도 없는데, 이것이 커맨드 라인 프로그램에 대해 기대한 동작입니다.

다시 한번 프로그램을 실행하는데 이번에는 다음과 같이 에러를 내지 않는 인수를 사용하고 표준 출력을 파일로 리디렉션해 봅시다.

$ cargo run -- to poem.txt > output.txt

minigrep의 정상 검색 결과는 터미널 대신 output.txt에 담깁니다. cargo run 자체의 빌드·실행 로그는 별도의 Cargo 출력이므로 아래 예시에서는 생략합니다.

output.txt
Are you nobody, too?
How dreary to be somebody!

이는 이제 성공적인 출력에 대해서는 표준 출력을, 에러 출력에 대해서는 표준 에러를 적절히 사용하고 있음을 입증합니다.

println!과 eprintln!의 차이는 단순한 출력 위치 차이가 아니라, 성공 데이터와 실패 설명을 분리해 셸 리디렉션이 의도대로 동작하게 만드는 프로그램 계약입니다.

표준 출력·표준 에러·종료 상태는 세 개의 다른 관측값입니다. 오류 메시지를 eprintln!으로 바꾸어도 상태 1을 만드는 것은 process::exit(1) 호출입니다. 반대로 결과 줄이 없다는 것만으로 오류 종료라고 판단할 수 없습니다. 정상 파일에서 질의와 일치하는 줄이 없으면 run이 Ok(())를 반환하여 이 main도 정상 종료합니다.

예시 이 장 끝의 바이너리를 이미 빌드했다면 POSIX 셸에서는 ./target/debug/minigrep frog poem.txt > matches.txt 2> errors.txt로 두 스트림을 별도 파일에 보낼 수 있습니다. >는 표준 출력만, 2>는 표준 에러만 바꿉니다. 프로그램이 종료한 직후 printf '%s\n' "$?"로 그 명령의 종료 상태를 확인합니다. 중간에 다른 명령을 실행하면 그 명령의 상태를 보게 됩니다. Windows의 경로·셸 문법은 다르므로 이 예를 그대로 복사하지 않습니다. Cargo를 거치지 않는 이 형태는 Cargo 로그와 프로그램 오류를 섞지 않기 위한 작은 확인 절차입니다.

독립 연습: 읽을 수 있는 poem.txt에서 일치하는 줄이 없는 질의를 사용한 경우와, 존재하지 않는 파일을 지정한 경우를 비교하세요. 각 경우에 matches.txt, errors.txt, 종료 상태는 어떤 결과를 가져야 할까요? 환경 변수·질의는 두 경우 외의 오류를 만들지 않는다고 가정합니다.

결과 없음과 실패 종료 구별하기

일치 없음은 정상 검색이므로 표준 출력 파일이 비고 프로그램 오류도 없으며 종료 상태는 0입니다. 존재하지 않는 경로는 파일 읽기의 오류가 run에서 반환되어 표준 출력에 결과를 쓰지 않고 표준 에러에 Application error 메시지를 쓰며 상태 1로 끝납니다. 출력 파일이 둘 다 비어 보인다는 관측 하나로 성공을 확정하지 말고 종료 상태도 함께 읽습니다. 오류의 세부 문구는 플랫폼에 따라 달라지며, 이 답은 원 코드의 분기 추론이지 현재 환경의 실행 기록이 아닙니다.


테스트 요약

이번 장에서는 여태껏 배운 몇몇 주요 개념들을 재점검하고 러스트에서의 통상적인 입출력 연산이 수행되는 방식을 다루었습니다.

커맨드 라인 인수, 파일, 환경 변수, 그리고 에러 출력을 위한 eprintln! 매크로를 사용함으로써, 이제는 커맨드 라인 애플리케이션을 작성할 준비가 되었습니다.

이전 장의 개념들과의 조합을 통하여 여러분은 코드를 잘 구성하고, 적절한 데이터 구조에 효율적으로 데이터를 저장하며, 에러를 잘 처리하고 테스트할 수 있을 것입니다.

다음으로는 함수형 언어로부터 영향을 받은 러스트의 기능 몇 가지를 탐구해 보겠습니다.

바로 클로저와 반복자죠.