stdout은 프로그램의 정상 결과를 출력하는 표준 출력이고, stderr는 오류 메시지를 출력하는 표준 에러다.
둘은 화면에서는 함께 보일 수 있지만 운영체제 내부에서는 서로 다른 출력 경로로 처리된다.
이 차이를 이해해야 리다이렉션, 로그 분리, 파이프 처리, 컨테이너 로그 구조를 정확히 이해할 수 있다.

핵심 요약

리눅스는 프로그램의 출력을 하나로 다루지 않는다. 정상 결과는 stdout, 오류 메시지는 stderr로 분리한다. 이 구조 덕분에 결과 데이터만 파일로 저장하거나, 에러만 따로 모으거나, 정상 출력만 다음 명령으로 넘길 수 있다. 화면에서는 둘 다 같은 텍스트처럼 보이지만, 쉘과 운영체제는 이를 전혀 다른 스트림으로 취급한다.

왜 필요한가

프로그램의 출력이 하나뿐이라고 가정하면 바로 문제가 생긴다. 정상 결과와 오류 메시지가 뒤섞이면 사람이 보기에도 불편하지만, 더 큰 문제는 자동 처리에 있다. 어떤 명령의 결과를 파일로 저장해 후속 작업의 입력으로 써야 할 때 오류 메시지까지 함께 들어가면 그 파일은 더 이상 순수한 데이터가 아니다. 데이터 파일이자 에러 로그가 되어 버린다. 그 순간 후속 처리 로직은 예상하지 못한 문자열을 만나게 되고, 파싱 실패나 집계 오류가 발생한다.

기존 방식의 한계는 파이프라인에서 더 크게 드러난다. 유닉스 계열 시스템은 작은 명령들을 연결해서 큰 작업을 만든다. 그런데 에러 메시지가 정상 데이터와 같은 흐름으로 다음 명령에 전달되면 grep, sort, uniq, awk 같은 도구는 오류 텍스트까지 데이터처럼 받아들인다. 그러면 결과가 오염된다. 운영체제가 처음부터 출력 경로를 분리한 이유는 단순히 예쁘게 보이게 하려는 것이 아니라, 정상 데이터 흐름을 보호하기 위해서다.

실제 운영 환경에서는 로그 관리 문제가 더 직접적이다. 애플리케이션 실행 결과와 장애 메시지가 한 파일에 뒤섞이면 운영자는 “결과 파일을 봐야 하는지”, “로그 파일을 봐야 하는지”부터 헷갈리게 된다. 반대로 stdoutstderr를 분리하면 산출물은 산출물대로 유지되고, 오류는 오류대로 분석할 수 있다. 즉 이 구조는 출력 구분이 아니라 시스템 결과와 시스템 이상 신호를 분리하는 설계다.

파일 디스크립터와 기본 구조

리눅스는 표준 입출력을 숫자로도 다룬다.

  • 0 : stdin
  • 1 : stdout
  • 2 : stderr

이 숫자 체계를 모르면 쉘 문법이 암호처럼 보인다. >는 사실 1>와 같다. 즉 기본적으로 stdout을 리다이렉션한다. 반면 2>stderr를 대상으로 한다. 2>&1은 “2번 스트림을 1번 스트림이 향하는 곳으로 보낸다”는 뜻이다. 이 숫자 체계를 이해하면 리다이렉션 문법을 외우지 않고 해석할 수 있다.

예제 1. stdout과 stderr는 실제로 분리되어 있다

echo "hello"
ls /not/exist/path

결과는 보통 이렇게 보인다.

hello
ls: cannot access '/not/exist/path': No such file or directory

왜 이렇게 되는가.
echo "hello"는 정상 결과를 출력하므로 stdout으로 나간다. 반면 ls /not/exist/path는 존재하지 않는 경로를 접근했기 때문에 오류 메시지를 stderr로 출력한다. 두 줄 모두 화면에 보이니 같은 종류의 출력이라고 오해하기 쉽지만, 내부적으로는 완전히 다른 통로를 사용한다.

언제 쓰는가.
이 예제는 가장 기본적인 확인용이다. 이후의 리다이렉션 동작이 이해되지 않을 때 항상 이 예제로 돌아오면 된다. 핵심은 “화면에 함께 보이는 것”과 “같은 스트림을 타는 것”은 다르다는 점이다.

예제 2. stdout만 파일로 저장한다

echo "hello" > out.txt
ls /not/exist/path

결과는 다음과 같다.

  • out.txt에는 hello가 저장된다.
  • ls의 오류 메시지는 여전히 화면에 출력된다.

왜 이렇게 되는가.
>는 기본적으로 stdout만 파일로 보낸다. echo의 출력은 1번 스트림을 타고 파일로 들어가지만, ls의 오류는 2번 스트림이므로 영향을 받지 않는다. 그래서 화면에는 오류가 남고 파일에는 정상 결과만 들어간다.

언제 쓰는가.
정상 결과만 후속 처리 대상일 때 쓴다. 예를 들어 명령의 산출물만 저장하고, 오류는 운영자가 즉시 콘솔에서 확인하고 싶을 때 유용하다. 배치 작업의 결과 파일을 깨끗하게 유지하려는 경우에도 같은 원리다.

예제 3. stderr만 파일로 저장한다

ls /not/exist/path 2> err.txt

결과는 다음과 같다.

  • 화면에는 아무것도 보이지 않는다.
  • err.txt에 오류 메시지가 저장된다.

왜 이렇게 되는가.
2>는 2번 스트림인 stderr만 다른 곳으로 보낸다. 이 경우 명령은 정상 결과가 없고 오류만 발생했기 때문에, 그 오류 텍스트 전체가 파일로 이동한다.

언제 쓰는가.
장애 원인만 따로 수집하고 싶을 때 쓴다. 예를 들어 대량 배치 작업에서 실패한 항목의 메시지만 모아 별도 검토하고 싶다면 이런 방식이 필요하다. 정상 산출물과 진단 로그를 섞지 않는다는 점이 핵심이다.

예제 4. stdout과 stderr를 하나의 파일에 함께 저장한다

ls /not/exist/path > all.log 2>&1

결과는 다음과 같다.

  • all.log에 정상 출력과 오류 출력이 모두 저장된다.

왜 이렇게 되는가.
먼저 > all.log가 실행되면서 stdout의 목적지가 파일로 바뀐다. 그 다음 2>&1이 실행되면서 stderr도 현재의 stdout이 향하는 목적지, 즉 all.log로 따라간다. 순서가 중요하다. 먼저 파일로 보내고, 그 다음 합쳐야 한다.

언제 쓰는가.
하나의 실행 로그 파일이 필요할 때 쓴다. 배치 작업 전체 기록을 남기거나, 문제 재현을 위해 모든 출력을 한 곳에 모아야 할 때 적합하다. 다만 결과 데이터와 에러 로그의 성격이 다를 경우에는 무조건 합치는 것이 정답은 아니다.

예제 5. 출력을 모두 버린다

command > /dev/null 2>&1

결과는 다음과 같다.

  • 화면에는 아무 출력도 보이지 않는다.
  • 파일에도 아무것도 남지 않는다.

왜 이렇게 되는가.
/dev/null은 데이터를 받아도 저장하지 않고 폐기하는 특수 파일이다. stdout을 먼저 /dev/null로 보내고, stderrstdout을 따라가게 하면 두 스트림 모두 사라진다.

언제 쓰는가.
반복 실행되는 점검 스크립트나 백그라운드 작업에서 출력이 불필요할 때 사용한다. 다만 디버깅 단계에서 이 패턴을 남용하면 문제 원인을 잃어버리므로 주의가 필요하다.

실무 적용 1. 결과 파일과 에러 로그를 분리하는 배치 작업

상황.
배치 스크립트가 데이터 처리 결과를 파일로 남기고, 실패한 항목의 에러 메시지는 별도 로그에 남겨야 한다.

문제.
모든 출력을 한 파일에 넣으면 결과 데이터 안에 오류 문자열이 섞인다.

잘못된 방식은 이렇다.

process_data.sh > result.log 2>&1

실제 결과.
result.log에는 정상 결과와 에러 메시지가 함께 들어간다. 사람이 보기에는 한 파일이라 편해 보일 수 있지만, 후속 프로그램이 이 파일을 읽는 순간 오류 메시지 줄 때문에 파싱이 깨질 수 있다.

왜 틀렸는가.
산출물 파일과 장애 로그는 용도가 다르다. 하나는 후속 처리 대상이고, 다른 하나는 운영 분석 대상이다. 둘을 한 파일에 넣으면 두 목적을 동시에 망친다.

올바른 방법은 다음과 같다.

process_data.sh > result.log 2> error.log

효과.
result.log는 순수한 산출물 파일이 되고, error.log는 장애 메시지 파일이 된다. 이 구조는 데이터 정합성과 장애 분석 가능성을 동시에 보장한다.

실무 적용 2. 파이프라인에서 정상 데이터 흐름만 넘기기

상황.
명령 결과를 다음 명령으로 넘겨서 필터링하거나 집계해야 한다.

문제.
오류 메시지가 섞이면 파이프라인 결과가 왜곡된다.

예를 들어 다음 명령을 보자.

find /tmp -name "*.log" | sort | uniq

정상 상황에서는 find가 찾은 파일 목록만 sort, uniq로 전달된다. 그런데 권한 없는 디렉터리가 섞이면 find는 오류 메시지를 stderr로 출력한다. 기본 파이프는 stdout만 넘기므로 그 자체는 괜찮다. 하지만 운영자는 종종 “왜 화면에 에러는 보이는데 결과 목록은 계속 나오지?”라는 혼란을 겪는다.

에러까지 조용히 숨기고 정상 데이터만 다루고 싶다면 이렇게 쓴다.

find /tmp -name "*.log" 2>/dev/null | sort | uniq

효과.
파이프라인에는 정상 파일 목록만 남고, 권한 오류 메시지는 제거된다. 결과 데이터 정제라는 관점에서 stderr 분리의 의미가 드러나는 장면이다.

실무 적용 3. 컨테이너 로그 수집 구조

상황.
컨테이너 환경에서는 애플리케이션 로그를 로깅 시스템이 수집해야 한다.

문제.
애플리케이션 내부 파일에만 로그를 쓰면 컨테이너 교체, 스케일링, 디스크 관리가 복잡해진다.

적용 방법은 단순하다. 애플리케이션 로그를 파일이 아니라 표준 스트림으로 내보낸다.

python app.py

애플리케이션이 내부에서 stdout, stderr로 로그를 출력하면 Docker나 쿠버네티스가 이 스트림을 수집할 수 있다.

효과.
로그 경로가 런타임 표준 구조에 맞춰진다. 컨테이너 수명과 로그 저장 경로를 분리할 수 있고, 외부 수집기와 연결하기도 쉬워진다. 그래서 “컨테이너 로그를 stdout으로 보낸다”는 말은 단순 취향이 아니라 수집 구조를 런타임 표준에 맞추는 설계다.

흔한 실수 1. >가 모든 출력을 저장한다고 생각한다

잘못된 사용:

command > output.log

실제 결과.
정상 출력만 output.log에 들어가고, 오류 메시지는 화면에 그대로 남는다.

왜 틀렸는가.
>stdout만 처리한다. stderr는 별도 스트림이라 영향을 받지 않는다.

올바른 방법:

command > output.log 2>&1

또는 산출물과 에러를 분리하려면:

command > output.log 2> error.log

흔한 실수 2. 2>&1 순서를 거꾸로 쓴다

잘못된 사용:

command 2>&1 > output.log

실제 결과.
stdout은 파일로 가지만 stderr는 여전히 콘솔에 남을 수 있다.

왜 틀렸는가.
stderrstdout을 따라갈 때, 그 시점의 stdout은 아직 터미널을 향하고 있다. 그 뒤에야 stdout이 파일로 바뀐다. 따라서 둘이 같은 파일로 모이지 않는다.

올바른 방법:

command > output.log 2>&1

해결 원리.
먼저 stdout의 목적지를 파일로 정하고, 그 다음 stderr를 그 목적지에 합류시켜야 한다.

흔한 실수 3. 파이프가 stderr도 다음 명령으로 넘긴다고 생각한다

잘못된 사용:

command | grep error

실제 결과.
grepstdout만 받는다. stderr로 출력된 오류 메시지는 파이프를 타지 않는다.

왜 틀렸는가.
파이프 |는 기본적으로 1번 스트림만 다음 명령의 입력으로 넘긴다. 2번 스트림은 별도 처리다.

올바른 방법:

command 2>&1 | grep error

이렇게 해야 stderr까지 stdout에 합쳐진 뒤 파이프로 전달된다.

흔한 실수 4. /dev/null로 에러를 너무 빨리 버린다

잘못된 사용:

command 2>/dev/null

실제 결과.
명령은 실패했는데 원인 메시지가 남지 않는다.

왜 틀렸는가.
stderr 전체를 버리기 때문에 디버깅 단서가 사라진다.

올바른 방법.
평소에는 파일로 남기고, 정말 필요 없는 경우에만 버리는 편이 낫다.

command 2> error.log

문제 원인을 확인한 뒤에야 필요하면 /dev/null을 쓰는 것이 맞다.

흔한 실수 5. 결과 파일과 로그 파일의 성격을 구분하지 않는다

잘못된 사용:

generate_report.sh > report.txt 2>&1

실제 결과.
report.txt 안에 보고서 데이터와 에러 메시지가 같이 들어간다.

왜 틀렸는가.
보고서 파일은 후속 소비 대상이고, 에러 메시지는 운영자 분석 대상이다. 성격이 다른 데이터를 하나로 합치면 둘 다 품질이 떨어진다.

올바른 방법:

generate_report.sh > report.txt 2> report-error.log

이렇게 해야 결과물의 품질과 로그 분석 가능성을 동시에 지킬 수 있다.

관련 개념

stdin은 프로그램이 입력을 받는 표준 입력이다.
리다이렉션은 표준 스트림의 목적지를 파일이나 장치로 바꾸는 문법이다.
파이프는 stdout을 다른 명령의 stdin으로 연결하는 구조다.
/dev/null은 출력을 폐기하는 특수 파일이다.

더 깊이 보기

운영체제 관점에서 프로그램은 “화면에 글자를 쓴다”기보다 “열려 있는 파일 디스크립터에 바이트를 기록한다”고 보는 편이 정확하다. 터미널 역시 파일처럼 취급되기 때문에 stdoutstderr가 모두 같은 터미널 장치에 연결될 수 있다. 그래서 사용자 눈에는 합쳐져 보인다. 하지만 경로 자체는 여전히 분리되어 있다.

이 구조는 유닉스 철학과도 연결된다. 프로그램은 하나의 일을 하고, 결과는 stdout, 문제는 stderr로 분리해 내보낸다. 그러면 다른 도구가 결과만 받아 연결할 수 있다. 즉 스트림 분리는 단순 출력 구분이 아니라, 작은 도구들을 안전하게 조합하기 위한 전제다.

또 하나 봐야 할 것은 종료 코드다. stdoutstderr는 메시지 전달 경로이고, 종료 코드는 성공과 실패 상태를 나타낸다. 오류 메시지가 없다고 성공은 아니고, 오류 메시지가 있다고 무조건 실패 종료도 아니다. 자동화 스크립트에서는 출력 내용과 종료 코드를 함께 봐야 한다. 이 지점까지 이해해야 표준 스트림 개념이 실제 운영과 연결된다.

정리

stdout은 정상 결과를 위한 표준 출력이고, stderr는 오류 메시지를 위한 표준 에러다.
둘은 화면에서는 같이 보일 수 있지만 운영체제 내부에서는 완전히 다른 스트림이다.
이 분리 덕분에 결과 파일과 에러 로그를 나눌 수 있고, 파이프라인의 데이터 오염을 막을 수 있으며, 컨테이너 로그 수집 구조도 단순해진다.
리다이렉션 문법은 외워서 끝나는 것이 아니라, 1번과 2번 스트림의 목적지를 제어하는 규칙으로 이해해야 제대로 쓸 수 있다.