본문 바로가기

표준 에러 출력

정상 검색 결과와 오류 메시지를 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의 화면 표시 순서는 보장하지 않습니다.

println!으로 출력한 일치하는 줄은 표준 출력으로 나가, 파일로 리다이렉션하거나 파이프로 다음 명령에 넘길 수 있습니다. 이 글에서 두 오류 메시지를 eprintln!으로 바꾸면, 오류 설명은 표준 에러로 따로 나가 터미널이나 별도 로그에서 볼 수 있습니다. 성공과 실패는 main이 정상적으로 돌아가는지, process::exit(1)을 부르는지에 따라 정해지는 종료 상태로 셸이나 CI에 알려집니다. 세 채널은 서로 독립적이므로, 화면에 어떤 순서로 나타나는지는 보장되지 않습니다.

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

먼저 minigrep이 지금 출력하는 내용이, 표준 에러로 보내고 싶은 오류 메시지까지 포함해 모두 표준 출력으로 나가는지 확인합니다. 그러려면 표준 출력 스트림을 파일로 리다이렉션하면서 일부러 오류를 일으킵니다. 표준 에러 스트림은 리다이렉션하지 않으므로, 표준 에러로 보낸 내용은 계속 화면에 나타납니다.

명령줄 프로그램은 오류 메시지를 표준 에러 스트림으로 보내야 합니다. 그래야 표준 출력 스트림을 파일로 리다이렉션해도 오류 메시지를 화면에서 볼 수 있습니다. 지금 프로그램은 제대로 동작하지 않습니다. 오류 메시지가 화면 대신 파일에 저장되는 것을 곧 보게 됩니다.

이 동작을 보려면 >와 파일 경로 output.txt를 붙여 프로그램을 실행합니다. 표준 출력 스트림을 이 파일로 리다이렉션하겠다는 뜻입니다. 오류가 나도록 인수는 주지 않습니다.

cargo run > output.txt

> 문법은 셸에게 표준 출력의 내용을 화면 대신 output.txt에 쓰라고 알립니다. bash 같은 유닉스 셸이 프로그램을 실행하기 전에 표준 출력을 그 파일로 바꿔 연결하는 과정은 운영체제 교재의 파일 시스템 실무에서 다룹니다. 화면에 출력될 줄 알았던 오류 메시지가 보이지 않으므로, 메시지가 파일에 들어갔다는 뜻입니다. output.txt에는 다음 내용이 들어 있습니다.

output.txt
Problem parsing arguments: not enough arguments

예상대로 오류 메시지가 표준 출력으로 출력되고 있습니다. 이런 오류 메시지는 표준 에러로 출력되어야 성공한 실행의 데이터만 파일에 남으므로, 이것을 바꿉니다. 화면에 보이는 Compiling이나 Running 같은 줄은 Cargo가 표준 에러로 출력하는 자기 로그이며, minigrep의 출력과는 별개입니다.

표준 에러로 에러 출력하기

오류 메시지를 출력하는 방식을 바꿉니다. 앞에서 리팩터링한 덕분에 오류 메시지를 출력하는 코드가 모두 main 함수 한 곳에 있습니다. 표준 라이브러리는 표준 에러 스트림에 출력하는 eprintln! 매크로를 제공하므로, 오류를 출력하던 두 곳의 println!을 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에 결과가 들어 있습니다. 검색 결과를 출력하는 run 안의 println!은 그대로 표준 출력을 쓰므로 >를 따라 파일로 갑니다.

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

이로써 성공한 출력에는 표준 출력을, 오류 출력에는 표준 에러를 알맞게 쓰고 있다는 것을 확인했습니다.

종료 상태는 출력과 별개로 확인합니다. main이 정상적으로 돌아가면 종료 상태는 0이고, 인수 오류나 파일 오류를 다룬 뒤 process::exit(1)을 부르면 1입니다. 직전 명령의 종료 상태는 bash에서는 echo $?로, PowerShell에서는 $LASTEXITCODE로 볼 수 있습니다.

cargo run -- monomorphization poem.txt > output.txt
echo $?
0

일치하는 줄이 없으면 표준 출력은 비어 있지만 오류는 아니므로 표준 에러에도 아무것도 나오지 않고 종료 상태는 0입니다. 다른 grep 도구는 일치하는 줄이 없을 때 1을 돌려주기도 하므로, 상태 규칙은 도구마다 다를 수 있습니다. cargo run은 Cargo의 로그를 함께 출력하므로, 세 채널을 정확히 나눠 확인하려면 빌드된 실행 파일을 직접 실행하거나 통합 테스트에서 자식 프로세스로 실행해 표준 출력, 표준 에러, 종료 상태를 따로 검사합니다.

더 깊이: 두 스트림의 버퍼와 println!의 패닉

io::stdout 문서에 따르면 표준 출력의 핸들은 모두 뮤텍스로 접근을 맞추는 전역 버퍼 하나를 함께 쓰며, 터미널에 연결되어 있을 때는 기본으로 줄 단위로 버퍼링해 줄바꿈 문자를 만날 때마다 내보냅니다. 반면 io::stderr의 핸들은 버퍼링하지 않습니다. 그래서 줄바꿈 없이 print!로 쓴 안내는 flush를 부르기 전까지 화면에 나타나지 않을 수 있지만, eprint!로 쓴 것은 곧바로 나갑니다. 두 스트림을 같은 터미널로 보내도 화면에 나타나는 순서가 코드의 순서와 다를 수 있는 까닭 가운데 하나가 이것입니다.

println!은 부를 때마다 표준 출력을 잠급니다. println! 문서는 반복문 안에서 많이 부르면 이것이 병목이 될 수 있으므로 io::stdout().lock()으로 한 번 잠근 뒤 writeln!으로 쓰라고 권합니다. 또 println!은 표준 출력에 쓰기가 실패하면 패닉을 내며, 문서는 논블로킹(non-blocking) 표준 출력에 쓸 때 그런 오류가 날 수 있다고 적습니다. 출력 실패를 패닉이 아니라 값으로 다루고 싶다면 writeln!이 돌려주는 io::Result를 직접 처리합니다.

정리

이번 장에서는 지금까지 배운 주요 개념 몇 가지를 다시 짚고, 러스트에서 흔한 입출력 작업을 어떻게 하는지 다루었습니다. 명령줄 인수, 파일, 환경 변수, 그리고 오류를 출력하는 eprintln! 매크로를 써서, 이제 명령줄 애플리케이션을 쓸 준비가 되었습니다. 앞 장들의 개념과 함께 쓰면, 코드를 잘 정리하고, 알맞은 자료 구조에 데이터를 효율적으로 저장하고, 오류를 잘 다루고, 잘 테스트할 수 있습니다.

  • 표준 출력에는 프로그램의 결과를, 표준 에러에는 오류 메시지를 보냅니다. println!은 표준 출력에, eprintln!은 표준 에러에 출력합니다.
  • >는 표준 출력만 파일로 리다이렉션하므로, 오류 메시지를 표준 에러로 보내면 결과 파일에 섞이지 않고 화면에 남습니다.
  • 종료 상태는 출력과 별개의 채널이며, 정상 반환이면 0, process::exit(1)이면 1입니다.
  • 일치하는 줄이 없는 것은 오류가 아니므로 이 minigrep은 빈 출력과 종료 상태 0으로 끝납니다.
  • cargo run의 Compiling, Running 같은 줄은 Cargo가 표준 에러로 출력하는 로그입니다.

연습 문제

1. 어디에 무엇이 남을까

cargo run -- to missing.txt > output.txt를 실행하면 화면, output.txt, 종료 상태는 각각 어떻게 되나요? eprintln!으로 바꾸기 전이었다면 무엇이 달랐을까요?

해설 보기

설정은 만들어지지만 run이 파일 오류를 돌려주므로, Application error:로 시작하는 메시지가 표준 에러로 나가 화면에 보이고, output.txt는 비어 있으며 종료 상태는 1입니다. 바꾸기 전에는 이 메시지가 표준 출력으로 나가 output.txt에 들어가고 화면에는 Cargo의 로그만 보였을 것입니다. 종료 상태는 두 경우 모두 1입니다.

2. 파이프로 이어 쓰기

minigrep의 결과를 다른 명령으로 넘겨 일치한 줄의 수를 세려 합니다. 결과와 오류가 모두 표준 출력으로 나간다면 어떤 문제가 생기나요?

해설 보기

파이프는 표준 출력만 다음 명령으로 넘기므로, 오류 메시지도 결과의 한 줄로 세어집니다. 예를 들어 파일이 없을 때 일치한 줄이 없는데도 1줄로 세어지고, 사용자는 화면에서 오류를 보지 못합니다. 오류를 표준 에러로 보내면 다음 명령에는 결과만 넘어가고 오류는 화면에 남으며, 실패 여부는 종료 상태로 따로 확인할 수 있습니다.

이것으로 11장을 마칩니다. 다음 장의 클로저: 자신의 환경을 캡처하는 익명 함수부터는 함수형 언어의 영향을 받은 러스트의 기능인 클로저와 반복자를 살펴봅니다.