1. 정의 / 결론
컨테이너 로그를 stdout으로 보낸다는 것은 애플리케이션이 로그 파일을 직접 관리하지 않고, 표준 출력과 표준 에러로 로그를 내보내고 컨테이너 런타임이 이를 수집하게 만드는 방식을 뜻한다.
컨테이너 환경에서 이 방식이 기본이 된 이유는, 컨테이너가 일시적인 실행 단위이기 때문에 내부 파일 로그에 의존하면 로그 유실, 수집 복잡성, 운영 불일치가 바로 발생하기 때문이다.
2. 핵심 요약
컨테이너에서는 로그를 “저장”하기보다 “흘려보내는 것”이 기본이다.
파일 로그는 컨테이너 삭제, 재배포, 스케일아웃 구조와 잘 맞지 않는다.
stdout 기반 로그는 Docker, Kubernetes, 로그 수집 시스템이 같은 방식으로 처리할 수 있어서 운영 경로가 단순해진다.
3. 왜 필요한가
전통적인 서버 환경에서는 애플리케이션이 /var/log/app.log 같은 파일에 직접 로그를 남겨도 구조적으로 큰 문제가 없었다. 서버가 오래 살아 있고, 프로세스도 비교적 고정되어 있으며, 로그를 확인하는 방식도 “서버 접속 → 파일 열기”로 충분했기 때문이다. 이 구조에서는 로그 파일이 곧 기록 저장소였다.
컨테이너 환경은 전제가 다르다. 컨테이너는 이미지로부터 생성되고, 장애나 배포 때문에 언제든지 교체될 수 있다. 같은 서비스가 여러 개의 컨테이너 인스턴스로 동시에 실행되기도 한다. 이 상황에서 각 컨테이너가 내부 파일에 로그를 남기면, 로그 위치가 컨테이너마다 흩어지고, 컨테이너가 사라질 때 로그도 같이 사라질 수 있다. 즉, 로그가 애플리케이션 내부 상태에 묶여 버린다.
그래서 해결 구조가 바뀐다. 애플리케이션은 로그를 파일에 저장하는 역할을 내려놓고 stdout, stderr로 출력만 한다. Docker나 containerd 같은 런타임이 그 출력을 수집한다. Kubernetes는 다시 그 로그를 kubectl logs로 보여주거나, Fluent Bit, Fluentd, Vector, Logstash, Loki 같은 수집기로 넘긴다. 이 구조에서는 애플리케이션은 로그 생성만 담당하고, 저장과 전송은 플랫폼이 담당한다. 문제를 애플리케이션 안이 아니라 플랫폼 계층에서 풀게 되는 것이다.
이 방식의 실제 영향은 크다. 스케일아웃된 수십 개의 Pod 로그를 한 방식으로 수집할 수 있고, 컨테이너가 교체돼도 로그 수집 체계는 그대로 유지된다. 운영자는 애플리케이션마다 다른 파일 경로를 외울 필요가 없고, 개발자는 파일 권한, 로그 디렉터리 생성, 로테이션 스크립트 같은 부수 작업을 애플리케이션 코드에 섞지 않아도 된다. 로그 경로가 아니라 출력 인터페이스가 표준화되는 셈이다.
4. 예제
예제 1. Python 애플리케이션이 stdout, stderr로 로그 출력하기
import sys
print("INFO: application started")
print("ERROR: database connection failed", file=sys.stderr)
실행 결과는 일반 출력과 에러 출력이 각각 stdout, stderr로 나간다. Docker는 두 스트림을 모두 수집한다.
이렇게 되는 이유는 컨테이너 런타임이 프로세스의 표준 스트림을 직접 붙잡고 있기 때문이다. 애플리케이션이 파일을 열지 않아도, 프로세스가 살아 있는 동안 나가는 출력은 모두 런타임이 관찰할 수 있다.
이 방식은 단순한 스크립트, 배치 잡, API 서버 모두에 동일하게 적용된다. 파일 경로를 맞추는 대신 출력 대상만 맞추면 된다.
예제 2. Docker에서 stdout 로그 확인하기
docker run --name demo python:3.12 python -c "print('hello from container')"
docker logs demo
결과로 hello from container가 출력된다.
애플리케이션이 파일을 만든 적이 없어도 로그가 확인되는 이유는, Docker가 컨테이너 프로세스의 표준 출력을 로그 드라이버로 저장하고 있기 때문이다.
실무에서는 애플리케이션 팀이 “로그 파일이 어디 있나요?”를 묻기보다, 컨테이너가 무엇을 stdout으로 내보내는지를 먼저 확인해야 한다.
예제 3. Kubernetes에서 Pod 로그 확인하기
kubectl logs my-app-pod
kubectl logs -f my-app-pod
첫 번째 명령은 현재까지 쌓인 로그를 보여주고, 두 번째 명령은 실시간 스트리밍을 보여준다.
이 구조가 가능한 이유는 Kubernetes가 노드의 컨테이너 런타임 로그를 표준 방식으로 노출하기 때문이다. Pod 내부 파일을 직접 읽는 방식이 아니라, 컨테이너의 표준 출력 기록을 조회하는 구조다.
실무에서는 장애 시 가장 먼저 kubectl logs를 확인하게 된다. 파일 기반 구조였다면 Pod 내부 경로와 셸 접근 여부부터 따져야 했을 것이다.
예제 4. Java Logback을 콘솔 출력으로 설정하기
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%level] %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
결과적으로 애플리케이션 로그가 파일이 아니라 콘솔로 나간다. 컨테이너에서는 이 콘솔 출력이 곧 로그 수집 대상이 된다.
이렇게 해야 하는 이유는, 파일 Appender를 쓰면 컨테이너 내부 디스크와 경로 관리가 다시 애플리케이션 책임으로 들어오기 때문이다.
Spring Boot, Quarkus, Micronaut 같은 서버 애플리케이션에서도 컨테이너 환경이라면 ConsoleAppender가 기본에 가깝다.
예제 5. 잘못된 파일 로그 방식과 stdout 방식 비교
# 잘못된 방향
/app/logs/app.log
# 컨테이너 친화적 방향
stdout / stderr
파일 로그 방식에서는 로그를 보기 위해 컨테이너 내부 파일시스템을 열어야 한다.
stdout 방식에서는 docker logs, kubectl logs, 중앙 로그 시스템이 같은 데이터를 읽는다.
차이는 저장 위치가 아니라 책임 분리에 있다. 파일 로그는 애플리케이션이 저장까지 책임지고, stdout 방식은 애플리케이션이 생성만 책임진다.
컨테이너에서는 후자가 구조적으로 맞다.
5. 실무 적용
1) Kubernetes 기반 API 서버 운영
상황은 Spring Boot API 서버가 Deployment로 여러 Pod에 분산 배포되는 경우다.
문제는 Pod가 재시작되거나 재스케줄링되면 내부 파일 로그를 추적하기가 어렵다는 점이다.
해결 방법은 Logback을 ConsoleAppender로 통일하고, 노드 단에서 Fluent Bit가 stdout 로그를 Elasticsearch나 Loki로 보내게 만드는 것이다.
효과는 Pod 개수가 늘어나도 로그 수집 경로가 바뀌지 않는다는 점이다. 장애 분석이 파일 위치 추적이 아니라 서비스 단위 조회로 바뀐다.
2) 배치 작업 컨테이너 운영
상황은 하루 단위로 Job 또는 CronJob이 실행되는 구조다.
문제는 배치 컨테이너가 종료되면 내부 파일도 함께 사라질 수 있다는 점이다.
해결 방법은 실행 과정 전체를 stdout으로 출력하고, Job 완료 후에도 중앙 로그 저장소에서 조회하게 만드는 것이다.
효과는 종료된 컨테이너의 파일을 보존하려고 별도 볼륨을 붙이지 않아도 된다는 점이다. 로그 보존 정책은 플랫폼에서 통제한다.
3) 멀티 언어 서비스 혼합 운영
상황은 Python, Java, Node.js 서비스가 함께 운영되는 구조다.
문제는 언어마다 로그 프레임워크와 파일 경로가 달라 운영 방식이 제각각이 되는 것이다.
해결 방법은 모든 서비스에서 콘솔 출력만 사용하게 하고, 로그 포맷만 JSON 또는 공통 패턴으로 맞춘다.
효과는 수집 에이전트 설정이 단순해지고, 서비스별 예외 처리가 줄어든다. 언어별 차이를 로그 경로가 아니라 필드 구조 수준으로만 관리하면 된다.
4) 사이드카나 서비스메시 환경
상황은 애플리케이션 컨테이너 외에 프록시나 사이드카가 함께 붙는 경우다.
문제는 파일 로그를 각 컨테이너가 따로 남기면 수집 포인트가 늘어난다는 점이다.
해결 방법은 애플리케이션과 사이드카 모두 stdout을 사용하게 하고, 컨테이너 이름이나 라벨로 구분해 중앙 수집한다.
효과는 수집 구조가 컨테이너 개수에 비례해 복잡해지지 않는다. 각 컨테이너가 같은 출력 계약을 따르기 때문이다.
6. 흔한 실수
실수 1. 컨테이너 안에서 /var/log/app.log를 직접 관리하려고 함
잘못된 사용은 전통적인 서버 습관대로 로그 파일을 직접 만드는 것이다.
실제 결과는 컨테이너가 재생성되면 로그가 사라지거나, 로그를 보려면 Pod 내부로 들어가야 하는 상황이 생긴다.
원인은 컨테이너 내부 파일시스템을 장기 보존 저장소처럼 취급했기 때문이다. 컨테이너는 교체 가능한 실행 단위이지 로그 아카이브가 아니다.
올바른 방법은 표준 출력으로 로그를 보내고, 보존은 외부 로그 시스템이나 런타임 로그 저장 계층에 맡기는 것이다.
실수 2. stdout에는 일반 로그만, 에러는 파일로 따로 저장함
잘못된 사용은 INFO는 stdout으로 보내고 ERROR는 별도 파일에만 남기는 구성이다.
실제 결과는 kubectl logs에서는 정상 흐름만 보이고, 실패 원인은 컨테이너 내부 파일을 따로 뒤져야 한다.
원인은 표준 에러의 역할을 파일 저장으로 오해했기 때문이다. stderr는 파일로 보내라는 뜻이 아니라, 에러 성격의 출력 채널을 분리하라는 뜻이다.
올바른 방법은 일반 로그는 stdout, 에러 로그는 stderr로 보내고 둘 다 런타임이 수집하게 만드는 것이다.
실수 3. 로그 로테이션을 애플리케이션 내부에서 강하게 제어함
잘못된 사용은 애플리케이션이 일자별 파일 분리, 압축, 삭제 정책까지 모두 안고 가는 것이다.
실제 결과는 컨테이너 설정과 로그 플랫폼 정책이 충돌하고, 디스크 사용량도 애플리케이션 관점에서만 관리된다.
원인은 로그 저장 정책의 책임이 애플리케이션에 있다고 본 것이다. 컨테이너 환경에서는 저장과 보존 주체가 플랫폼이어야 한다.
올바른 방법은 애플리케이션은 출력만 하고, 로테이션과 보관 기간은 Docker 로그 드라이버나 중앙 로그 저장소에서 관리하는 것이다.
실수 4. docker logs가 보이지 않는데 애플리케이션 로그가 없다고 판단함
잘못된 사용은 docker logs 결과가 비어 있으면 애플리케이션이 로그를 안 남긴다고 판단하는 것이다.
실제 결과는 로그는 남기고 있었지만 파일 Appender만 사용해서 표준 출력에는 아무것도 없는 경우가 많다.
원인은 “로그 생성”과 “로그 노출 경로”를 구분하지 않았기 때문이다. 로그를 남기는 것과 운영자가 그 로그를 볼 수 있는 것은 다른 문제다.
올바른 방법은 컨테이너 환경에서는 먼저 콘솔 출력 구성이 되어 있는지 확인해야 한다.
실수 5. 로그를 너무 구조 없이 한 줄 문자열로만 출력함
잘못된 사용은 error occurred, something wrong 같은 비정형 문자열만 반복하는 것이다.
실제 결과는 중앙 로그 시스템에 모였더라도 검색, 필터링, 집계가 어렵다. stdout을 쓴다고 자동으로 운영성이 좋아지는 것은 아니다.
원인은 출력 채널만 표준화하고 로그 내용 구조는 방치했기 때문이다.
올바른 방법은 timestamp, level, service, trace id, message 같은 필드를 포함한 구조적 로그를 stdout으로 내보내는 것이다.
실수 6. 로그를 표준 출력으로 보내면서도 영속 볼륨을 의무적으로 붙임
잘못된 사용은 stdout 로그 구조를 채택했는데도 습관적으로 로그용 PVC나 호스트 마운트를 붙이는 것이다.
실제 결과는 저장 경로는 늘어나고, 운영 복잡성은 줄지 않는다. 어느 로그가 기준인지도 흐려진다.
원인은 파일 로그 기반 사고를 완전히 버리지 못했기 때문이다. stdout 구조는 로그 저장소를 외부 계층으로 밀어내는 데 목적이 있다.
올바른 방법은 규제나 특수 요구가 없는 한, 로그 영속화는 중앙 수집 시스템에서 처리하고 애플리케이션 컨테이너는 무상태로 유지하는 것이다.
7. 관련 개념
stdout / stderr
프로세스가 외부로 내보내는 기본 출력 채널이다. 일반 메시지와 에러 메시지를 분리하는 데 사용된다.
로그 드라이버
Docker가 stdout/stderr를 어떤 방식으로 저장하거나 전달할지 결정하는 계층이다. json-file, journald, fluentd 등이 있다.
사이드카 로그 수집기
애플리케이션 옆에서 로그를 수집하거나 전달하는 보조 컨테이너다. 다만 최근에는 노드 단 수집기가 더 일반적이다.
구조적 로그
사람이 읽는 문자열 대신 key-value 또는 JSON 형태로 출력하는 로그 방식이다. stdout 기반 운영과 잘 결합된다.
8. 더 깊이 보기
컨테이너 로그가 stdout 중심으로 설계된 이유는 단순히 편해서가 아니다. 컨테이너는 프로세스를 패키징한 실행 단위이고, 그 본질은 “짧게 말하면 하나의 표준 입출력 인터페이스를 가진 프로세스”에 가깝다. 이 관점에서 로그는 별도 파일시스템 기능이 아니라, 프로세스 출력의 한 종류로 취급하는 편이 더 자연스럽다. 즉, 로그를 파일로 만드는 순간 프로세스 외부 인터페이스가 다시 내부 저장소 문제로 되돌아간다.
또 하나의 구조적 이유는 오케스트레이션 계층 때문이다. Kubernetes는 애플리케이션마다 다른 로그 경로를 이해하려 하지 않는다. 대신 컨테이너가 stdout/stderr로 무엇을 내보냈는지라는 공통 인터페이스만 본다. 이 공통 인터페이스 덕분에 배포 방식, 언어, 프레임워크가 달라도 동일한 운영 명령과 수집 체계가 가능해진다. 플랫폼이 애플리케이션 내부 구현을 몰라도 로그를 처리할 수 있는 이유가 여기에 있다.
시스템 관점에서도 이 구조는 분산 시스템에 맞다. 분산 환경에서 각 인스턴스가 파일을 로컬에 저장하면, 운영자는 로그 수집이라는 별도의 분산 문제를 다시 풀어야 한다. stdout 방식은 이 문제를 초기에 차단한다. 로그는 발생 지점에서 로컬 파일로 축적되지 않고, 즉시 플랫폼 수집 경로로 올라간다. 즉, 로컬 저장 중심 사고를 스트림 중심 사고로 바꾸는 것이다. 컨테이너 철학과 로그 철학이 같은 방향을 보게 된다.
9. 정리
컨테이너 로그를 stdout으로 보내는 이유는 파일 로그보다 간단해서가 아니라, 컨테이너의 무상태성, 재배포 가능성, 스케일아웃 구조와 맞기 때문이다.
파일 로그는 컨테이너 내부 상태를 늘리고, 로그 수집 책임을 애플리케이션 쪽으로 되돌린다.
stdout 기반 로그는 애플리케이션은 출력만 담당하고, 런타임과 플랫폼이 수집·보존·전송을 담당하게 만든다.
Docker와 Kubernetes에서 docker logs, kubectl logs가 기본 도구가 된 이유도 이 구조 위에 있기 때문이다.
컨테이너 환경에서는 로그 파일 경로를 설계하는 것보다, 어떤 로그를 어떤 형식으로 stdout/stderr에 내보낼지 설계하는 것이 먼저다.