로그가 안 남는 순간 — 문제는 시스템이 아니라 이해 방식이다
리눅스에서 로그가 남지 않는 문제는 대부분 환경이나 도구의 문제가 아니라, 출력 구조를 잘못 이해한 결과다. 같은 명령어를 실행했는데 어떤 경우에는 로그가 파일에 남고, 어떤 경우에는 터미널에만 출력되거나 일부만 기록되는 현상이 반복된다. 이 상황은 비정상적인 동작처럼 보이지만, 실제로는 리눅스가 설계된 방식 그대로 동작한 결과다. 문제는 시스템이 아니라, 우리가 그 구조를 파일 중심으로 오해하고 있다는 점이다. 이 글은 그 오해를 제거하고, 로그가 왜 사라지는지 구조적으로 설명하는 것을 목표로 한다.
많은 개발자는 로그를 “파일에 기록되는 결과”로 이해한다. 이 관점에서는 파일이 존재하지 않거나 권한 문제가 있을 때 로그가 남지 않는다고 판단하게 된다. 그러나 리눅스에서 로그는 처음부터 파일에 기록되는 것이 아니다. 프로그램은 데이터를 파일이 아니라 스트림으로 출력한다. 이 출력이 파일로 연결될 때만 로그가 파일에 남는다. 따라서 로그가 사라지는 문제는 파일 문제가 아니라, 출력 스트림이 어디로 연결되었는지에 대한 문제다. 이 차이를 이해하지 못하면 동일한 문제를 반복해서 겪게 된다.
이 지점에서 중요한 것은 “어디에 출력되었는가”가 아니라, “어떤 경로로 출력이 흘러갔는가”를 추적하는 것이다. 리눅스에서는 출력이 단일 경로가 아니라 여러 경로로 나뉘어 있으며, 각 경로는 독립적으로 동작한다. 이 구조를 이해하지 않으면 일부 출력만 파일에 남고 나머지는 사라지는 현상을 설명할 수 없다. 따라서 로그 문제를 해결하기 위해서는 먼저 출력이 어떻게 나뉘고 흐르는지를 이해해야 한다. 이 흐름을 이해하기 위해서는 가장 기본적인 개념인 출력 스트림의 구조부터 다시 정의할 필요가 있다.
흔한 해결 방식 — 동작은 하지만 이유를 설명하지 못한다
로그가 남지 않는 문제를 만나면 대부분은 명령어를 조금씩 바꾸면서 해결을 시도한다. 가장 흔한 방식은 출력 리다이렉션을 사용하는 것이다. 예를 들어 다음과 같은 명령어를 사용한다.
command > log.txt
이 명령어는 프로그램의 출력을 파일로 보내기 때문에 직관적으로 문제를 해결하는 것처럼 보인다. 그러나 실제로는 일부 출력만 기록되고, 특정 메시지는 여전히 터미널에 남는다. 이때 많은 사람들은 명령어가 잘못되었거나 프로그램이 비정상적으로 동작한다고 판단한다. 하지만 이 현상은 리눅스의 출력 구조를 그대로 반영한 결과다. 즉, 명령어는 정상적으로 동작하고 있으며, 우리가 출력의 종류를 구분하지 못하고 있는 것이다.
이 문제를 해결하기 위해 다음과 같은 명령어가 추가로 사용된다.
command > log.txt 2>&1
이 명령어는 대부분의 경우 정상적으로 로그를 파일에 남긴다. 하지만 같은 목적을 가지고 다음과 같이 작성하면 결과가 달라진다.
command 2>&1 > log.txt
이 두 명령어는 겉보기에는 동일해 보이지만, 실제로는 완전히 다른 결과를 만든다. 이 차이를 설명하지 못하면 문제는 해결된 것이 아니라 우연히 맞아떨어진 것이다. 즉, 우리는 명령어를 이해하고 사용하는 것이 아니라, 특정 패턴을 외워서 사용하는 상태에 머물러 있다. 이 상태에서는 새로운 상황이 발생할 때마다 동일한 문제를 반복하게 된다.
이 문제의 핵심은 명령어 문법이 아니라, 그 아래에 있는 구조다. 리다이렉션은 단순히 파일로 출력하는 기능이 아니라, 출력 경로를 재구성하는 메커니즘이다. 이 구조를 이해하지 않으면 명령어의 동작을 예측할 수 없다. 따라서 다음 단계에서는 로그를 파일로 저장한다는 개념 자체가 왜 잘못된 출발점인지부터 다시 살펴봐야 한다.
문제의 핵심 — 로그는 파일이 아니라 스트림이다
로그가 파일에 기록된다는 인식은 결과를 기준으로 한 해석이다. 실제로 프로그램은 파일에 직접 데이터를 쓰지 않는다. 프로그램은 데이터를 출력 스트림으로 내보낸다. 이 스트림은 기본적으로 터미널과 연결되어 있으며, 별도의 설정이 없는 경우 화면에 출력된다. 즉, 로그는 처음부터 파일에 존재하는 것이 아니라, 출력된 데이터를 파일로 연결했을 때만 파일에 기록된다. 이 차이를 이해하지 못하면 로그가 사라지는 문제를 설명할 수 없다.
리눅스에서 출력은 하나가 아니라 두 개의 독립된 경로로 나뉜다. 하나는 정상적인 결과를 출력하는 경로이고, 다른 하나는 에러 메시지를 출력하는 경로다. 이 두 경로는 서로 영향을 주지 않는다. 따라서 하나의 경로만 파일로 연결하면 나머지 경로의 출력은 여전히 터미널로 남게 된다. 이것이 “로그 일부만 남는 현상”의 실제 원인이다. 이 현상은 예외적인 상황이 아니라, 리눅스 I/O 구조의 기본 동작이다.
▶ 이 개념을 더 깊게 이해하려면: 리눅스 stdout stderr 리다이렉션 완전 정리 — 2>&1까지 쉽게 이해하기

이 구조를 이해하면 로그 문제를 바라보는 방식이 바뀐다. 로그가 남지 않는 것은 출력이 생성되지 않았기 때문이 아니라, 출력 경로가 올바르게 연결되지 않았기 때문이다. 즉, 문제는 데이터가 아니라 경로다. 이 관점이 잡히면, 이후에 등장하는 모든 개념이 자연스럽게 연결된다. 다음 단계에서는 이 출력 경로가 왜 두 개로 나뉘어 있는지, 그리고 각각이 어떤 역할을 하는지를 구체적으로 살펴볼 필요가 있다.
stdout과 stderr — 출력이 나뉘어 있는 이유를 이해하지 못하면 로그는 항상 틀린다
로그가 일부만 남거나, 에러 메시지가 파일에 기록되지 않는 이유는 단순한 설정 문제가 아니다. 그 원인은 출력이 하나가 아니라 두 개의 독립된 경로로 나뉘어 있기 때문이다. 많은 개발자는 프로그램이 하나의 출력만 가진다고 생각하지만, 리눅스에서는 기본적으로 두 개의 출력 스트림이 존재한다. 하나는 정상적인 결과를 전달하는 경로이고, 다른 하나는 오류나 예외를 전달하는 경로다. 이 두 경로는 서로 영향을 주지 않으며, 각각 별도의 흐름으로 동작한다. 이 구조를 이해하지 못하면 로그가 왜 분리되거나 누락되는지 설명할 수 없다.
stdout은 프로그램의 정상적인 실행 결과를 전달하기 위한 경로다. 이 경로는 기본적으로 터미널과 연결되어 있으며, 사용자가 확인해야 할 결과를 출력한다. 반면 stderr는 오류나 경고와 같은 비정상적인 상황을 전달하기 위한 별도의 경로다. 이 경로는 stdout과 완전히 독립적으로 동작한다. 이 분리는 단순한 설계상의 선택이 아니라, 데이터 흐름을 제어하기 위한 구조적 결정이다. 정상 출력과 오류 출력이 섞이면 후처리나 자동화가 어려워지기 때문에, 리눅스는 처음부터 이 둘을 분리된 스트림으로 설계했다.
이 구조의 중요한 특징은 두 스트림이 동시에 존재한다는 점이다. 프로그램이 실행되면 stdout과 stderr는 각각 별도의 채널로 데이터를 내보낸다. 따라서 stdout만 파일로 연결하면 stderr는 여전히 터미널로 출력된다. 이것이 “로그 일부만 남는 현상”의 직접적인 원인이다. 즉, 로그가 사라진 것이 아니라, 다른 경로로 출력된 것이다. 이 동작은 예외적인 상황이 아니라, 리눅스 I/O의 기본 동작이다.
▶ 이 개념을 더 깊게 이해하려면: 리눅스 stdout stderr 리다이렉션 완전 정리 — 2>&1까지 쉽게 이해하기

이제 중요한 질문이 남는다. 이 두 개의 출력 경로는 어떻게 제어되는가. 단순히 존재를 아는 것만으로는 부족하다. 각각의 경로를 원하는 위치로 보내고, 필요에 따라 합치거나 분리할 수 있어야 한다. 이 제어를 담당하는 메커니즘이 바로 redirection이다.
redirection — 출력이 아니라 경로를 바꾸는 메커니즘
리다이렉션은 출력 내용을 바꾸는 기능이 아니다. 리다이렉션은 출력이 어디로 흘러갈지를 결정하는 기능이다. 이 차이를 이해하지 못하면, 동일한 명령어가 다른 결과를 만드는 이유를 설명할 수 없다. 프로그램은 항상 동일한 방식으로 stdout과 stderr에 데이터를 출력한다. 리다이렉션은 그 출력이 도착하는 목적지를 바꿀 뿐이다. 즉, 출력 자체는 변하지 않지만, 그 경로가 변경된다.
예를 들어 > 연산자는 stdout의 목적지를 파일로 변경한다. 이 연산자는 프로그램이 출력하는 데이터를 가로채는 것이 아니라, stdout이 연결된 대상을 파일로 교체한다. 따라서 프로그램은 여전히 stdout으로 데이터를 출력하지만, 그 데이터는 더 이상 터미널이 아니라 파일로 전달된다. 반대로 stderr는 아무런 영향을 받지 않는다. 이 때문에 stdout만 파일로 저장하면 stderr는 여전히 터미널에 남는다. 이 동작은 리다이렉션이 특정 스트림에만 적용되기 때문이다.
command > out.log 2> err.log
이 명령어는 stdout과 stderr를 각각 다른 파일로 보낸다. 여기서 중요한 것은 두 스트림이 독립적으로 제어된다는 점이다. 각각의 스트림은 별도의 리다이렉션을 통해 다른 목적지로 연결된다. 이 구조를 이해하면, 로그를 분리하거나 통합하는 작업이 단순한 옵션 설정이 아니라, 스트림 경로를 재구성하는 작업이라는 것을 알 수 있다.

리다이렉션은 pipe와 함께 사용될 때 더 강력한 의미를 가진다. pipe는 프로그램 간의 연결을 담당하고, 리다이렉션은 출력의 목적지를 제어한다. 이 두 기능은 서로 다른 역할을 가지지만, 동일한 스트림 구조 위에서 동작한다. 따라서 리다이렉션을 정확히 이해하려면, 스트림이 어떻게 식별되고 연결되는지를 더 깊이 이해해야 한다. 이 지점에서 등장하는 개념이 바로 파일 디스크립터다. 그리고 이 구조를 이해하지 못하면, 2>&1과 같은 표현은 항상 혼란스럽게 남는다.
2>&1 — 문법이 아니라 파일 디스크립터의 연결이다
2>&1은 단순한 문법이 아니라, 파일 디스크립터 간의 관계를 재구성하는 표현이다. 이 표현을 이해하지 못하면 로그가 합쳐지거나 분리되는 이유를 설명할 수 없다. 여기서 1은 stdout을 의미하고, 2는 stderr를 의미한다. 이 숫자는 단순한 번호가 아니라, 각각의 스트림을 식별하는 파일 디스크립터다. 리눅스에서는 모든 입출력이 파일 디스크립터를 통해 이루어지기 때문에, 이 숫자는 실제 데이터 흐름을 결정하는 핵심 요소다.
2>&1은 stderr를 stdout과 동일한 대상으로 연결한다는 의미다. 이때 중요한 것은 값이 복사되는 것이 아니라, 참조가 연결된다는 점이다. 즉, stderr가 stdout이 가리키는 대상을 따라가게 된다. 이 구조 때문에 명령어의 순서가 결과에 직접적인 영향을 준다. 예를 들어 다음 두 명령어는 서로 다른 결과를 만든다.
command > log.txt 2>&1
이 경우 stdout이 먼저 파일로 연결되고, 이후 stderr가 stdout을 따라가기 때문에 두 스트림이 모두 파일로 기록된다.
command 2>&1 > log.txt
이 경우 stderr가 기존 stdout을 따라간 후, stdout만 파일로 변경되기 때문에 stderr는 여전히 터미널로 출력된다.

이 차이는 단순한 문법 차이가 아니라, 파일 디스크립터가 언제 어떤 대상을 참조하는지에 대한 문제다. 즉, 리다이렉션은 문자열을 해석하는 과정이 아니라, 실행 시점에 파일 디스크립터를 재구성하는 과정이다. 이 구조를 이해하면 2>&1은 더 이상 외워야 할 문법이 아니라, 예측 가능한 동작으로 바뀐다.
이제 흐름은 명확해진다. 출력은 두 개의 스트림으로 나뉘고, 리다이렉션은 그 경로를 바꾸며, 파일 디스크립터는 그 연결을 실제로 구현한다. 이 세 가지를 이해하면 로그가 왜 사라지는지 설명할 수 있다. 다음 단계에서는 이 구조가 실제 환경에서 어떻게 문제로 나타나는지, 그리고 어떤 패턴으로 반복되는지를 분석할 필요가 있다.
파이프(pipe)는 왜 stderr를 전달하지 않는가 — 연결 구조를 잘못 이해하면 로그는 항상 끊긴다
파이프를 사용했는데 에러 로그가 다음 명령으로 전달되지 않는다면, 이는 설정 문제가 아니라 구조를 잘못 이해한 결과다. 많은 개발자는 | 연산자가 모든 출력을 다음 프로세스로 전달한다고 생각한다. 그러나 실제로 파이프는 하나의 스트림만 연결한다. 이 구조를 이해하지 못하면, 데이터는 전달되는데 에러는 사라지는 상황이 반복된다. 이 문제는 도구의 문제가 아니라 스트림 구조를 오해한 결과다.
파이프는 stdout만을 다음 프로세스의 stdin으로 연결한다. 이때 stderr는 연결되지 않는다. 이 동작은 설계상의 제한이 아니라 의도된 구조다. 파이프는 데이터 흐름을 연결하기 위한 메커니즘이다. stderr는 오류 전달을 위한 별도 채널이기 때문에, 기본적으로 데이터 흐름에 포함되지 않는다. 따라서 command1 | command2 구조에서 command1의 stdout은 command2로 전달되지만, stderr는 여전히 터미널로 출력된다. 이 동작은 예외가 아니라 기본 동작이다.
이 구조의 핵심은 스트림의 독립성이다. stdout과 stderr는 각각 다른 파일 디스크립터를 가진다. 파이프는 file descriptor 1, 즉 stdout만을 대상으로 동작한다. 따라서 stderr를 함께 전달하려면 명시적으로 두 스트림을 하나로 합쳐야 한다. 이때 사용하는 것이 2>&1이다. 즉, stderr를 stdout에 연결한 후 파이프를 적용해야 두 출력이 함께 전달된다.
command1 2>&1 | command2
이 명령어는 stderr를 stdout에 연결한 뒤, 그 통합된 스트림을 command2로 전달한다. 이 순서가 중요한 이유는 파이프가 stdout만을 대상으로 동작하기 때문이다. 만약 command1 | command2 2>&1 형태로 작성하면, command2의 stderr만 변경될 뿐 command1의 stderr는 여전히 터미널로 출력된다. 이 차이는 파이프가 어느 시점의 stdout을 연결하는지에 따라 결정된다.

이 구조를 이해하면, 로그가 파이프를 통과하지 않는 이유를 설명할 수 있다. 그리고 이 문제는 단순히 리다이렉션을 추가하는 것으로 해결되지 않는다. 먼저 스트림을 합치고, 그 다음에 연결해야 한다. 이제 다음 단계에서는 이러한 출력 흐름이 실제 실행 환경에서 어떻게 달라지는지 살펴볼 필요가 있다. 특히 터미널이 아닌 환경에서는 이 구조가 전혀 다르게 동작한다.
터미널이 아닌 환경 — 로그가 사라지는 진짜 이유는 실행 컨텍스트다
같은 명령어가 터미널에서는 정상적으로 로그를 출력하지만, 백그라운드 실행이나 서비스 환경에서는 로그가 남지 않는 경우가 있다. 이 문제는 리다이렉션이나 스트림 설정이 아니라 실행 컨텍스트의 차이에서 발생한다. 프로그램은 항상 stdout과 stderr로 출력하지만, 그 대상이 무엇인지에 따라 결과가 달라진다. 즉, 출력의 문제는 프로그램이 아니라 연결된 환경에 있다.
터미널에서 실행되는 프로세스는 기본적으로 stdout과 stderr가 터미널 디바이스에 연결되어 있다. 이 경우 출력은 즉시 화면에 나타난다. 그러나 nohup이나 &를 사용하여 백그라운드로 실행하면, 프로세스는 터미널과 분리된다. 이 상태에서 stdout과 stderr는 더 이상 터미널을 가리키지 않는다. nohup은 기본적으로 stdout을 nohup.out 파일로 리다이렉션하고, stderr는 stdout을 따라간다. 따라서 명시적인 설정이 없으면 로그는 예상하지 못한 위치에 기록된다.
nohup command &
이 명령어는 stdout을 nohup.out으로 보내고, stderr도 동일한 파일로 기록한다. 그러나 이 동작은 환경에 따라 다르게 해석될 수 있다. systemd와 같은 서비스 환경에서는 stdout과 stderr가 journald로 전달된다. Docker에서는 stdout과 stderr가 컨테이너 로그로 수집된다. 즉, 동일한 프로그램이라도 실행 환경에 따라 로그의 위치가 완전히 달라진다.
▶ 이 개념을 더 깊게 이해하려면: nohup이란 무엇인가? — 터미널 종료 후에도 프로세스를 유지하는 방법 완벽 정리

이 구조의 핵심은 출력 대상이 고정되어 있지 않다는 점이다. stdout과 stderr는 항상 존재하지만, 그 연결 대상은 실행 환경에 의해 결정된다. 따라서 로그가 보이지 않는다면, 출력이 없는 것이 아니라 다른 위치로 전달된 것이다. 이 문제를 해결하려면 프로그램이 아니라 실행 컨텍스트를 확인해야 한다. 이제 다음 단계에서는 이러한 구조를 기반으로, 실제 환경에서 로그를 안정적으로 남기기 위한 방법을 정리할 필요가 있다.
실무에서 로그를 통제하는 방법 — 출력이 아니라 흐름을 설계해야 한다
로그를 남기는 문제는 출력 명령어를 추가하는 문제가 아니다. 로그는 데이터 흐름의 일부이며, 그 흐름을 어떻게 설계하느냐에 따라 결과가 결정된다. stdout과 stderr는 각각 독립된 채널이며, 실행 환경에 따라 다른 대상에 연결된다. 이 구조를 이해하지 못하면, 로그를 남기기 위한 시도가 반복적으로 실패한다. 따라서 로그를 통제하려면 출력이 아니라 스트림의 흐름을 설계해야 한다.
실무에서는 stdout과 stderr를 명시적으로 통제하는 것이 기본이다. 가장 일반적인 방식은 두 스트림을 하나로 합쳐 파일로 저장하는 것이다. 이는 > file 2>&1 형태로 구현된다. 이 구조는 stdout을 파일로 보내고, stderr를 stdout에 연결하여 동일한 파일에 기록한다. 이 방식은 로그를 하나의 파일로 관리할 수 있다는 장점이 있다. 그러나 이 경우 오류와 정상 출력이 구분되지 않기 때문에, 분석이 어려워질 수 있다.
command > app.log 2>&1
반대로 stdout과 stderr를 분리하여 관리할 수도 있다. 이 경우 각각을 다른 파일로 리다이렉션한다. 이 구조는 오류를 별도로 추적할 수 있다는 장점이 있다. 그러나 로그가 여러 파일로 나뉘기 때문에 관리 복잡도가 증가한다. 따라서 어떤 방식을 선택할지는 시스템의 요구사항에 따라 결정해야 한다. 중요한 것은 어떤 선택을 하든, 스트림의 흐름을 의도적으로 설계해야 한다는 점이다.

이제 구조는 완성된다. 출력은 두 개의 스트림으로 나뉘고, 파이프는 하나만 연결하며, 실행 환경은 그 목적지를 결정한다. 이 세 가지를 이해하면 로그가 왜 사라지는지 설명할 수 있다. 그리고 더 중요한 것은, 새로운 환경에서도 동일한 방식으로 동작을 예측할 수 있다는 점이다. 다음 단계에서는 이러한 구조를 기반으로, 로그 시스템 전체를 설계하는 관점으로 확장할 필요가 있다.
로그는 출력이 아니라 계약이다 — 시스템 전체에서 동일하게 해석되는 구조를 만들어야 한다
로그가 남지 않는 문제는 출력 설정의 문제가 아니다. 이 문제는 시스템 전체에서 로그를 어떻게 정의했는지에 대한 문제다. 많은 환경에서 로그는 단순히 콘솔에 찍히는 문자열로 취급된다. 그러나 실제 시스템에서는 로그는 하나의 인터페이스다. 이 인터페이스가 정의되지 않으면, stdout과 stderr를 정확히 이해해도 로그는 여전히 일관되지 않게 남는다. 따라서 로그 문제를 해결하려면 출력 방식이 아니라, 출력의 의미를 시스템 수준에서 정의해야 한다.
stdout과 stderr는 단순한 출력 채널이 아니다. 이 두 스트림은 서로 다른 의미를 가지도록 설계된 인터페이스다. stdout은 정상적인 데이터 흐름을 전달하기 위한 경로다. stderr는 오류나 예외 상황을 전달하기 위한 경로다. 이 구분이 유지될 때, 이후의 모든 처리 과정이 안정적으로 동작한다. 반대로 이 구분이 무너지면, 로그는 더 이상 해석 가능한 데이터가 아니다. 예를 들어 모든 로그를 stdout으로 출력하면 오류를 분리할 수 없다. 반대로 모든 로그를 stderr로 출력하면 정상 데이터까지 오류로 취급된다. 이 문제는 출력 방식이 아니라 의미 설계의 실패다.
이 구조는 내부적으로 파일 디스크립터를 기반으로 동작한다. 프로세스는 fd 1을 통해 stdout으로 데이터를 보낸다. 프로세스는 fd 2를 통해 stderr로 데이터를 보낸다. 운영체제는 이 두 스트림을 별도로 처리한다. 그러나 운영체제는 이 데이터의 의미를 해석하지 않는다. 운영체제는 단지 데이터를 전달할 뿐이다. 따라서 stdout과 stderr의 의미는 애플리케이션이 정의해야 한다. 이 지점에서 로그는 단순 출력이 아니라, 시스템 간 약속이 된다.

실무에서 이 구조가 중요한 이유는 로그가 단순히 기록되는 것이 아니라, 이후 시스템에서 활용되기 때문이다. Docker는 stdout과 stderr를 그대로 수집한다. systemd는 두 스트림을 journald로 전달한다. 로그 수집 시스템은 이 데이터를 기반으로 분석을 수행한다. 이때 stdout과 stderr의 의미가 명확하지 않으면, 분석 결과는 왜곡된다. 즉, 로그는 단순히 남기는 데이터가 아니라, 이후 시스템이 해석하는 입력 데이터다.
결국 로그 문제는 출력의 문제가 아니라 구조의 문제다. stdout과 stderr는 물리적인 출력 경로가 아니라 의미를 가진 인터페이스다. 이 인터페이스를 어떻게 정의하느냐에 따라 로그의 품질이 결정된다. 따라서 로그를 설계할 때는 “어디에 출력할 것인가”가 아니라 “어떤 의미로 출력할 것인가”를 먼저 정의해야 한다. 이 기준이 명확해지면, 리다이렉션, 파이프, 실행 환경이 바뀌어도 로그는 일관되게 유지된다. 그리고 이 지점에서 로그는 더 이상 출력이 아니라, 시스템 간 데이터 흐름으로 확장된다.