journalctl 완벽 가이드 — systemd 로그 분석 처음부터 제대로 이해하기
이 글의 핵심
systemd 기반 리눅스에서 로그를 담당하는 journald와 그 조회 도구인 journalctl을 처음부터 차근차근 설명합니다. 저널이 저장되는 방식, 시간·유닛·우선순위별 필터링, 필드 기반 정밀 검색, 출력 포맷 활용, 디스크 정리 정책, 그리고 서비스 장애·부팅 지연·디스크 포화 같은 실전 상황을 로그로 추적하는 방법까지 다룹니다.
들어가며
리눅스 서버를 운영하다 서비스가 갑자기 죽거나, 부팅이 평소보다 오래 걸리거나, 원인 모를 에러가 반복될 때 가장 먼저 들여다봐야 하는 것이 로그입니다. systemd를 초기화 시스템으로 쓰는 최신 리눅스 배포판(Ubuntu, Debian, CentOS/RHEL, Fedora, Arch 등 대부분)에서는 이 로그를 journald라는 데몬이 관리하고, 이를 조회하는 명령어가 journalctl입니다.
문제는 journalctl을 처음 접하면 뭘 봐야 할지 막막하다는 점입니다. tail -f /var/log/syslog처럼 파일 하나를 열어보면 끝나는 게 아니라, 바이너리 포맷으로 저장된 로그를 다양한 옵션으로 필터링해서 봐야 하기 때문입니다. 이 글은 journalctl을 처음 써보는 사람을 대상으로, journald가 왜 존재하는지부터 시작해서 실무에서 바로 쓸 수 있는 필터링 기법, 디스크 관리, 실전 장애 추적 시나리오까지 순서대로 설명합니다.
이미 우분투 필수 명령어 가이드에서 journalctl의 아주 기본적인 사용법(-u, -f, -b, --since, -p err, --disk-usage, --vacuum-time 정도)을 다뤘지만, 이 글은 그보다 훨씬 깊게 들어갑니다. 필드 기반 정밀 검색, 출력 포맷 활용, 카탈로그 메시지, journald 설정 튜닝, 그리고 실제 장애 상황을 재현하며 로그를 추적하는 과정까지 다루는 journalctl 전용 완전 가이드입니다.
대상 독자
journalctl을 처음 써보거나-u,-f정도만 알고 있는 리눅스 입문자- 서비스 장애가 났을 때 로그에서 원인을 빠르게 좁히고 싶은 백엔드/DevOps 엔지니어
- journald의 디스크 사용량을 관리하고 로그 보관 정책을 세워야 하는 시스템 관리자
이 글을 읽으면
- journald가 기존 syslog와 무엇이 다르고 왜 만들어졌는지 이해하게 됩니다.
- 시간, 유닛, 우선순위, 필드 값 등 다양한 기준으로 로그를 정밀하게 필터링할 수 있습니다.
- json 등 다양한 출력 포맷을 활용해 로그를 다른 도구와 연계할 수 있습니다.
- 디스크 공간을 관리하고, 실제 장애 상황에서 로그로 원인을 추적하는 절차를 갖게 됩니다.
1. journald는 왜 존재하는가
기존 syslog 방식의 한계
systemd 이전의 전통적인 리눅스는 syslog(또는 그 구현체인 rsyslog, syslog-ng)라는 텍스트 기반 로깅 체계를 썼습니다. 각 프로그램이 syslog() 함수를 호출하면 syslog 데몬이 이를 받아 /var/log/syslog, /var/log/messages, /var/log/auth.log 같은 평문 텍스트 파일에 한 줄씩 append하는 방식입니다.
이 방식은 단순하고 grep, awk, tail -f 같은 익숙한 텍스트 도구로 바로 다룰 수 있다는 장점이 있지만, 실무에서는 다음과 같은 한계가 드러납니다.
- 구조가 없다: 로그 한 줄은 그냥 텍스트 문자열이라, “이 로그가 어느 프로세스 ID에서, 어느 systemd 유닛에서 나왔는지”를 정확히 알려면 로그 포맷을 직접 파싱해야 합니다. 프로그램마다 로그 포맷이 제각각이라 파싱 규칙도 따로 만들어야 합니다.
- 부팅 초기 로그를 놓치기 쉽다: syslog 데몬 자체가 아직 시작되지 않은 부팅 극초반의 커널 메시지나 초기 서비스 로그는 별도로
dmesg를 봐야 하는 등 조회 도구가 갈라집니다. - 회전(rotation) 설정을 직접 관리해야 한다:
logrotate같은 별도 도구로 파일 크기·보관 기간을 관리해야 하고, 설정을 빠뜨리면 로그가 무한정 쌓여 디스크를 채우는 사고로 이어집니다. - 신뢰성 문제: 시스템이 비정상 종료되는 순간 마지막 로그 몇 줄이 디스크에 flush되지 않고 유실될 수 있습니다.
journald가 해결하는 것
systemd 프로젝트는 이런 문제를 해결하기 위해 journald라는 전용 로깅 데몬을 도입했습니다. journald는 커널 메시지(dmesg), syslog 호환 메시지, systemd 유닛의 stdout/stderr, 그리고 sd_journal_print() API를 통한 구조화 로그까지 하나의 저장소에 모읍니다.
핵심 차이는 로그를 바이너리 포맷으로 저장하면서, 각 로그 항목에 메시지 텍스트뿐 아니라 PID, UID, GID, _SYSTEMD_UNIT, _BOOT_ID, _HOSTNAME 같은 구조화된 필드(메타데이터)를 함께 기록한다는 점입니다. 이 덕분에 “이 시간대에 nginx 유닛에서 났던 에러만” 같은 정밀한 질의가 가능해지고, 부팅마다 별도의 부팅 ID(_BOOT_ID)가 붙어 “지난 부팅에서 무슨 일이 있었는지”도 쉽게 구분할 수 있습니다.
물론 바이너리 포맷에는 트레이드오프가 있습니다. grep으로 직접 파일을 열어 검색할 수 없고, 반드시 journalctl이라는 전용 도구를 거쳐야 합니다. 파일이 손상되면(비정상 종료, 디스크 오류 등) 일부 구간을 읽지 못할 수도 있어 journald는 내부적으로 체크섬과 파일 회전(로테이션)으로 이런 위험을 줄이도록 설계되어 있습니다. 하지만 구조화된 필드 검색, 부팅별 구분, 통합된 조회 인터페이스라는 이점이 이 트레이드오프를 상쇄할 만큼 크기 때문에, 오늘날 대부분의 주요 배포판이 journald를 기본 로깅 데몬으로 채택하고 있습니다.
참고로 많은 배포판은
rsyslog나syslog-ng를 journald와 나란히 설치해, journald가 모은 로그를 다시 전통적인 텍스트 파일(/var/log/syslog등)로도 남기도록 설정해 두기도 합니다. 두 체계가 공존할 수 있다는 점을 알아두면 “왜 두 곳에 로그가 다 있지?”라는 혼란을 줄일 수 있습니다.
2. 저널이란 무엇인가: 저장 위치와 영속성
journalctl로 조회하는 로그 데이터는 실제로는 디스크(또는 메모리)에 있는 저널 파일들의 모음입니다. 이 파일들이 어디에 저장되는지, 재부팅 후에도 남는지는 /etc/systemd/journald.conf의 Storage= 설정에 따라 결정됩니다.
| 위치 | 성격 | 조건 |
|---|---|---|
/run/log/journal/ | 휘발성(tmpfs), 재부팅 시 소실 | 기본값. /var/log/journal이 없으면 여기만 사용 |
/var/log/journal/ | 영구 저장, 재부팅해도 유지 | 디렉터리가 존재하면 자동으로 여기에 저장 |
Storage= 값은 다음 네 가지입니다.
auto(기본값):/var/log/journal디렉터리가 존재하면 영구 저장, 없으면 휘발성 저장(/run/log/journal)만 사용합니다.persistent:/var/log/journal디렉터리가 없으면 자동으로 만들어서라도 영구 저장합니다.volatile:/var/log/journal이 있어도 무시하고 항상/run/log/journal(메모리 기반 tmpfs)만 사용합니다.none: 저널 자체를 저장하지 않습니다. 소켓으로 전달만 하고 버립니다.
많은 최소 설치(minimal install) 이미지나 컨테이너 베이스 이미지는 /var/log/journal 디렉터리를 만들지 않은 상태로 배포되기 때문에, 기본값(auto)임에도 실제로는 휘발성 로그만 쌓이고 있는 경우가 흔합니다. 재부팅 후 “어제 있었던 에러 로그를 보고 싶은데 아무것도 안 나온다”는 상황을 겪었다면 이 케이스일 가능성이 높습니다.
# 영구 저장으로 전환하기
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
# journald 재시작 (설정 즉시 반영)
sudo systemctl restart systemd-journald
# 현재 어디에 저장되고 있는지 확인
journalctl --disk-usage
systemd-tmpfiles --create --prefix /var/log/journal 명령은 단순히 디렉터리만 만드는 것이 아니라, journald가 요구하는 권한(root:systemd-journal, setgid 비트 등)까지 올바르게 설정해 줍니다. mkdir만 직접 실행하고 넘어가면 권한이 맞지 않아 journald가 여전히 휘발성 모드로 동작하는 경우가 있으므로, 반드시 systemd-tmpfiles 명령을 함께 실행하는 것이 안전합니다.
디렉터리를 만든 뒤에는 journald를 재시작해야 변경 사항이 적용됩니다. 재시작 시점까지의 휘발성 로그는 자동으로 영구 저장소로 이관되므로 데이터가 유실되지는 않습니다.
3. 기본 조회: 저널을 처음 열어보기
# 전체 로그를 오래된 것부터 최신순으로, pager(less)로 보기
journalctl
# 실시간으로 새 로그가 쌓이는 대로 따라가기 (tail -f와 동일한 감각)
journalctl -f
# 최신 로그부터 역순으로 보기
journalctl -r
# pager를 연 상태에서 즉시 맨 끝(최신)으로 이동
journalctl -e
# 최근 N줄만 보기 (기본은 10줄)
journalctl -n 50
# -f와 -n을 조합: 최근 50줄을 보여준 뒤 실시간 추적
journalctl -n 50 -f
인자 없이 journalctl만 실행하면 시스템에 기록된 모든 로그를 less 같은 페이저로 열어줍니다. 처음 실행하면 로그 양이 방대해 원하는 부분을 찾기 어려운데, 이때 페이저 안에서 /(검색), G(맨 끝으로 이동), q(종료) 같은 less의 단축키를 그대로 쓸 수 있다는 점을 알아두면 편리합니다. -e 옵션은 이 “맨 끝으로 이동”을 자동으로 해 주는 단축 옵션입니다.
-r(reverse)은 최신 로그를 위에서부터 보고 싶을 때 유용합니다. 기본 출력은 오래된 로그가 위, 최신 로그가 아래에 오는데, 방금 발생한 에러를 빠르게 확인하고 싶을 때는 journalctl -r | head -n 20처럼 조합하면 스크롤 없이 바로 최근 상황을 파악할 수 있습니다.
한 가지 초심자가 자주 놓치는 부분은 파이프로 연결하거나 파일로 리다이렉트할 때는 --no-pager를 붙이는 것이 안전하다는 점입니다. journalctl -u nginx | grep error처럼 파이프를 쓰면 journalctl이 자동으로 페이저를 끄지만, 스크립트에서 명시적으로 동작을 보장하고 싶다면 journalctl --no-pager -u nginx처럼 옵션을 직접 지정하는 습관을 들이는 것이 좋습니다.
4. 필터링 심화
유닛(-u)과 커널 메시지(-k)
# 특정 systemd 유닛의 로그만
journalctl -u nginx.service
# .service는 생략 가능
journalctl -u nginx
# 여러 유닛을 동시에 필터링
journalctl -u nginx -u php-fpm
# 커널 메시지만 보기 (dmesg의 journald 버전)
journalctl -k
# 또는
journalctl --dmesg
-u는 systemd 유닛 이름을 기준으로 필터링합니다. 유닛 이름은 systemctl list-units --type=service로 확인한 것과 동일하며, .service 확장자는 생략해도 자동으로 붙여 인식합니다. 같은 옵션을 여러 번 지정하면 OR 조건으로 동작해, 지정한 유닛들의 로그를 모두 모아서 보여줍니다.
-k(또는 --dmesg)는 커널 링버퍼 메시지만 필터링합니다. 전통적으로 dmesg 명령으로 보던 것과 같은 내용이지만, dmesg는 부팅 이후 누적된 커널 메시지만 볼 수 있는 반면 journalctl -k -b -1처럼 부팅 옵션과 조합하면 이전 부팅의 커널 메시지까지 조회할 수 있다는 차이가 있습니다. 하드웨어 문제(디스크 I/O 에러, OOM killer 개입, 네트워크 카드 리셋 등)를 의심할 때는 이 옵션이 dmesg보다 유용합니다.
부팅별 조회(-b)
# 이번(가장 최근) 부팅의 로그만
journalctl -b
# 이전 부팅(직전에 종료된 세션)의 로그
journalctl -b -1
# 두 번 전 부팅의 로그
journalctl -b -2
# 저장된 부팅 목록과 부팅 ID 확인
journalctl --list-boots
journald는 부팅마다 고유한 부팅 ID를 부여하고, 로그 항목마다 이 ID를 기록합니다. -b는 이 ID를 기준으로 로그를 나눠 보여주는 옵션으로, 인자 없이 쓰면 현재 부팅, -1은 바로 이전 부팅, -2는 그 이전 부팅을 의미합니다. --list-boots로 먼저 부팅 목록과 인덱스, 시작·종료 시각을 확인한 뒤 원하는 인덱스를 -b 뒤에 지정하는 것이 실수를 줄이는 방법입니다.
“어제 갑자기 서버가 재부팅됐는데 원인을 모르겠다”는 상황에서는 journalctl -b -1 -n 100처럼 이전 부팅의 마지막 로그 100줄을 보는 것이 첫 번째로 해야 할 일입니다. 비정상 종료(커널 패닉, OOM, 전원 문제)인 경우 이전 부팅 로그의 끝부분에 단서가 남아 있는 경우가 많습니다.
시간 범위(—since / —until)
# 절대 시각 지정
journalctl --since "2026-09-08 09:00:00" --until "2026-09-08 12:00:00"
# 날짜만 지정 (시각은 00:00:00으로 간주)
journalctl --since "2026-09-08" --until "2026-09-09"
# 상대 시간 표현
journalctl --since "1 hour ago"
journalctl --since "30 min ago"
journalctl --since "2 days ago"
# 자연어 키워드
journalctl --since today
journalctl --since yesterday
journalctl --since "yesterday" --until "today"
# 특정 유닛 + 특정 기간 조합
journalctl -u nginx --since "10 minutes ago"
--since/--until은 사람이 읽기 편한 다양한 시간 표현을 지원합니다. YYYY-MM-DD HH:MM:SS 형식의 절대 시각뿐 아니라 "1 hour ago", "30 min ago"처럼 상대 시간, today/yesterday/now 같은 키워드도 인식합니다. 시각을 생략하고 날짜만 쓰면 자정(00:00:00)으로 해석되므로, --since "2026-09-08"은 “2026년 9월 8일 0시부터”라는 의미가 됩니다.
장애 대응 중에는 모니터링 대시보드에서 이상 징후가 시작된 시각을 먼저 파악한 뒤, 그 시각의 앞뒤 몇 분을 --since/--until로 좁혀서 조회하는 흐름이 가장 효율적입니다. 범위를 너무 넓게 잡으면 관련 없는 로그에 묻히고, 너무 좁게 잡으면 원인이 되는 앞선 로그를 놓칠 수 있으므로 처음에는 여유 있게(예: 이상 징후 시각 기준 앞뒤 10분) 잡고 점점 좁혀가는 방식을 권장합니다.
우선순위 레벨(-p)
journald의 각 로그 항목은 전통적인 syslog 우선순위 체계를 그대로 물려받아 0(가장 심각)부터 7(가장 상세)까지의 레벨을 가집니다.
| 숫자 | 이름 | 의미 |
|---|---|---|
| 0 | emerg | 시스템 사용 불가 수준의 긴급 상황 |
| 1 | alert | 즉시 조치가 필요한 상황 |
| 2 | crit | 치명적 오류 |
| 3 | err | 일반 오류 |
| 4 | warning | 경고 (당장 오류는 아니지만 주의 필요) |
| 5 | notice | 정상이지만 주목할 만한 상황 |
| 6 | info | 일반 정보성 메시지 |
| 7 | debug | 디버깅용 상세 메시지 |
# err(3) 이상만 (0~3): emerg, alert, crit, err
journalctl -p err
# warning(4) 이상만 (0~4)
journalctl -p warning
# 특정 범위 지정: err ~ warning
journalctl -p err..warning
# 특정 유닛의 에러만 최근 순으로
journalctl -u myapp -p err -r
-p에 숫자나 이름 하나만 지정하면 그 레벨과 그보다 심각한(숫자가 더 작은) 레벨을 모두 포함한다는 점이 헷갈리기 쉬운 부분입니다. 즉 -p err는 err(3)만 보여주는 것이 아니라 emerg(0), alert(1), crit(2), err(3)까지 전부 포함합니다. 범위를 정확히 지정하고 싶다면 -p err..warning처럼 ..로 구간을 명시할 수 있습니다.
실무에서 유의할 점은, 이 우선순위는 애플리케이션이 로그를 남길 때 스스로 지정한 값이라는 것입니다. 즉 애플리케이션이 실제로는 심각한 오류인데도 info로 로깅하고 있다면 -p err로는 그 로그를 찾을 수 없습니다. 우선순위 필터링에 의존하기 전에, 대상 애플리케이션이 로그 레벨을 얼마나 신뢰성 있게 남기는지 먼저 확인하는 것이 좋습니다.
텍스트 검색(—grep)
# 메시지 본문에서 정규식으로 검색 (대소문자 구분)
journalctl -u myapp --grep "connection refused"
# 대소문자 구분 없이 검색
journalctl -u myapp --grep -i "timeout"
# --grep과 시간·유닛 필터를 함께 조합
journalctl -u myapp --since "1 hour ago" --grep "OutOfMemory"
--grep은 확장 정규식(ERE)을 지원하는 검색 옵션으로, 메시지 본문에 대해 패턴 매칭을 수행합니다. journalctl -u myapp | grep error처럼 외부 grep에 파이프하는 것과 결과는 비슷해 보이지만, --grep은 journald 내부에서 필터링하기 때문에 매칭된 줄 앞뒤의 컨텍스트를 -B/-A 옵션 없이도 journalctl 자체 옵션(-o, -x 등)과 자연스럽게 조합할 수 있다는 장점이 있습니다. -i를 붙이면 대소문자를 구분하지 않습니다.
특정 실행 파일/프로세스 기준 조회(-g, —identifier)
# syslog identifier(태그) 기준 조회
journalctl -t sshd
# 특정 실행 파일 경로 기준 조회
journalctl _EXE=/usr/sbin/sshd
# 특정 PID 기준 조회 (해당 프로세스가 남긴 로그만)
journalctl _PID=12345
-t(--identifier)는 syslog 방식으로 로깅하는 프로그램이 스스로 남기는 “태그” 문자열(예: sshd, CRON, sudo)을 기준으로 필터링합니다. 뒤에서 다룰 _EXE=, _PID= 같은 필드 기반 검색과 목적은 비슷하지만, -t는 특히 systemd 유닛이 아닌 전통적인 syslog 방식 데몬(예: sshd, cron)의 로그를 걸러낼 때 편리합니다.
5. 필드 기반 정밀 검색
사용 가능한 필드 나열하기(-F)
# journald가 기록한 필드 이름 전체 나열
journalctl -F _SYSTEMD_UNIT
# 특정 필드가 가질 수 있는 값 전체 나열
# (예: 시스템에 등록된 모든 유닛 이름 목록)
journalctl -F _COMM
# _PID 필드가 실제로 로그를 남긴 값들
journalctl -F _PID
journalctl -F <필드명>은 그 필드가 지금까지 로그에서 가질 수 있었던 모든 값의 목록을 보여줍니다. 예를 들어 -F _SYSTEMD_UNIT을 실행하면 현재 저널에 로그가 남아 있는 모든 systemd 유닛 이름을 볼 수 있어, -u로 어떤 유닛을 필터링할 수 있는지 미리 확인하는 용도로 유용합니다. 필드 이름 자체(_PID, _UID, _COMM, _EXE, MESSAGE, PRIORITY 등)는 각 로그 항목의 메타데이터를 구성하며, 대부분 언더스코어(_)로 시작하는 것은 journald가 커널 레벨에서 신뢰성 있게 채워 넣는 “신뢰 필드(trusted field)“이고, 언더스코어 없이 대문자로만 된 필드(MESSAGE, PRIORITY)는 애플리케이션이 직접 채워 넣는 필드입니다.
FIELD=value 형태의 정확한 검색
# 특정 프로세스 ID의 로그만 정확히 조회
journalctl _PID=1234
# 특정 systemd 유닛의 로그만 (-u와 동일한 효과)
journalctl _SYSTEMD_UNIT=nginx.service
# 특정 실행 파일 경로 기준
journalctl _EXE=/usr/bin/python3
# 특정 사용자(UID)가 실행한 프로세스의 로그
journalctl _UID=1000
# 여러 필드를 AND 조건으로 조합
journalctl _SYSTEMD_UNIT=myapp.service _PID=1234
# 같은 필드를 여러 번 지정하면 OR 조건
journalctl _PID=1234 + _PID=5678
일반적인 -u, -p, --grep 같은 옵션은 사실 내부적으로 이 FIELD=value 매칭을 감싼 편의 옵션입니다. 직접 FIELD=value 형태로 쿼리하면 -u/-p가 지원하지 않는 임의의 필드까지 정밀하게 검색할 수 있습니다. 예를 들어 특정 PID 하나가 재시작을 반복하며 여러 번 로그를 남겼을 때, _PID= 조건으로 그 프로세스 인스턴스의 로그만 정확히 골라낼 수 있습니다.
필드 매칭을 조합할 때의 규칙은 명확합니다. 서로 다른 필드를 나열하면 AND로 결합되고, 같은 필드를 +로 구분해 나열하면 OR로 결합됩니다. 이 규칙을 모르면 “왜 조건을 추가했는데 결과가 하나도 안 나오지?”라는 혼란을 겪기 쉬우므로, 여러 조건을 조합할 때는 이 AND/OR 규칙을 먼저 떠올리는 것이 좋습니다.
출력 포맷(-o)
# 사람이 읽기 좋은 기본 포맷 (기본값)
journalctl -u nginx -o short
# 메시지 본문만 출력 (다른 스크립트로 파이프할 때 유용)
journalctl -u nginx -o cat
# 모든 필드를 사람이 읽기 좋게 펼쳐서 출력 (디버깅에 유용)
journalctl -u nginx -o verbose
# JSON 한 줄씩 출력 (프로그램에서 파싱하기 좋음)
journalctl -u nginx -o json
# 보기 좋게 들여쓴 JSON (사람이 구조를 확인할 때)
journalctl -u nginx -o json-pretty -n 5
-o(--output)는 로그 한 줄이 화면에 어떤 형태로 표시될지를 결정합니다. short(기본값)는 우리가 흔히 아는 타임스탬프 호스트명 프로세스명[PID]: 메시지 형태입니다. cat은 메시지 본문만 남기고 타임스탬프나 유닛 정보를 모두 제거하므로, 로그 내용 자체를 다른 명령으로 다시 처리(파이프)할 때 잡음을 줄여줍니다.
verbose는 그 로그 항목이 가진 모든 필드를 하나씩 펼쳐서 보여줍니다. 특정 로그 한 줄이 왜 필터 조건에 걸리거나 걸리지 않는지 이해가 안 될 때, verbose로 실제 필드 값을 눈으로 확인하면 원인을 바로 알 수 있는 경우가 많습니다.
json과 json-pretty는 로그를 다른 도구(스크립트, 로그 수집 파이프라인, jq 등)와 연계할 때 사용합니다. 예를 들어 journalctl -u myapp -o json --since "1 hour ago" | jq '.MESSAGE'처럼 조합하면 메시지 필드만 뽑아내거나, 특정 필드 값 기준으로 추가 가공을 할 수 있습니다. 자체 로그 대시보드나 알림 스크립트를 만들 때 -o json은 사실상 필수 옵션입니다.
6. 카탈로그 메시지 보기(-x)
# 메시지에 등록된 설명(카탈로그)까지 함께 출력
journalctl -x -u systemd-networkd -n 20
systemd는 일부 잘 알려진 로그 메시지(주로 systemd 자체나 핵심 데몬이 남기는 것)에 대해 카탈로그(catalog)라는 부가 설명 데이터베이스를 함께 제공합니다. -x 옵션을 켜면, 해당하는 메시지가 나올 때 원래 로그 줄 아래에 “이 메시지가 왜 발생했는지”, “무엇을 확인해야 하는지” 같은 사람이 작성한 설명이 추가로 붙습니다.
모든 로그에 카탈로그 설명이 있는 것은 아니며, 애플리케이션이 직접 남기는 일반 로그에는 대부분 해당하지 않습니다. 하지만 systemd 자체의 유닛 실패, 의존성 문제, 타임아웃 같은 상황에서는 -x로 켜둔 설명이 원인 파악 시간을 크게 줄여줍니다. 특히 처음 접하는 systemd 관련 에러 메시지를 만났을 때는 습관적으로 -x를 붙여 확인해 보는 것을 권장합니다.
7. 디스크 관리
현재 사용량 확인과 즉시 정리
# 저널이 현재 차지하는 디스크 용량 확인
journalctl --disk-usage
# 전체 저널 크기를 500MB 이하로 줄이기 (오래된 것부터 삭제)
sudo journalctl --vacuum-size=500M
# 1주일보다 오래된 로그 삭제
sudo journalctl --vacuum-time=1week
# 최근 파일 10개만 남기고 나머지 저널 파일 삭제
sudo journalctl --vacuum-files=10
journald는 기본적으로 로그 보관 기간이나 최대 용량에 엄격한 제한을 두지 않습니다(배포판마다 기본 설정 값은 다를 수 있습니다). 아무 설정 없이 오래 운영하면 저널이 디스크 용량 상당 부분을 차지하는 경우가 흔하며, 특히 애플리케이션이 예외를 반복적으로 남기는 버그가 있으면 짧은 시간에도 수 GB씩 쌓일 수 있습니다.
--vacuum-size, --vacuum-time, --vacuum-files 세 옵션은 즉시 정리를 실행합니다. --vacuum-size는 목표 용량까지, --vacuum-time은 지정한 기간보다 오래된 파일을, --vacuum-files는 파일 개수를 기준으로 오래된 저널 파일부터 삭제합니다. 세 옵션 모두 현재 활성 상태로 쓰이고 있는 저널 파일은 삭제하지 않고, 이미 회전되어 닫힌(archived) 파일만 정리 대상으로 삼습니다. 즉 방금 발생한 로그가 삭제될 걱정은 하지 않아도 됩니다.
재발 방지: journald.conf 설정
# /etc/systemd/journald.conf
[Journal]
# 영구 저장소(/var/log/journal)의 최대 사용량
SystemMaxUse=500M
# 영구 저장소의 최소 여유 공간 보장
SystemKeepFree=1G
# 휘발성 저장소(/run/log/journal, tmpfs)의 최대 사용량
RuntimeMaxUse=100M
# 개별 저널 파일 하나의 최대 크기 (이 크기에 도달하면 새 파일로 회전)
SystemMaxFileSize=50M
# 로그 보관 최대 기간
MaxRetentionSec=1month
# 설정 변경 후 반드시 재시작
sudo systemctl restart systemd-journald
수동으로 --vacuum-* 명령을 반복 실행하는 것은 임시방편일 뿐이므로, 운영 서버에서는 journald.conf에 상한을 미리 설정해 두는 것이 근본적인 해결책입니다. SystemMaxUse는 journald가 영구 저장소에서 사용할 수 있는 최대 용량이고, SystemKeepFree는 디스크 전체에서 항상 확보해야 하는 여유 공간입니다. 두 값을 함께 설정하면 “저널 때문에 디스크가 가득 차서 다른 서비스가 쓰기 실패하는” 상황을 예방할 수 있습니다.
RuntimeMaxUse는 /run/log/journal(메모리 기반 tmpfs)에 대한 상한으로, 이 값이 너무 크면 로그 폭주 시 메모리 자체를 압박할 수 있으므로 물리 메모리 용량을 고려해 신중하게 설정해야 합니다. 설정 파일을 고친 뒤에는 daemon-reload가 아니라 systemctl restart systemd-journald로 journald 자체를 재시작해야 값이 반영된다는 점도 기억해 둘 필요가 있습니다.
8. 원격/중앙 로그 수집 기초
서버 한두 대라면 각 서버에 SSH로 접속해 journalctl을 실행하는 것으로 충분하지만, 서버가 늘어나면 로그를 한곳에 모아 검색하고 싶어집니다. systemd 진영에서는 이를 위해 systemd-journal-remote와 systemd-journal-upload라는 별도 패키지를 제공합니다.
간단히 말하면, 로그를 보내는 쪽 서버에서 systemd-journal-upload가 실행되며 로컬 저널을 HTTP(S)로 지속적으로 전송하고, 수집 서버에서는 systemd-journal-remote가 이를 받아 별도의 저널 파일로 저장합니다. 저장된 원격 저널은 journalctl --file=/var/log/journal/remote/remote-<호스트>.journal처럼 파일을 직접 지정해 평소와 동일한 journalctl 문법으로 조회할 수 있습니다.
다만 실무에서는 대규모 로그 수집·검색에 Elasticsearch/Loki/Datadog 같은 전용 로그 파이프라인을 함께 쓰는 경우가 많고, 이때는 journald의 로그를 Vector나 journalbeat 같은 에이전트로 읽어 외부 시스템에 전달하는 구성이 흔합니다. systemd-journal-remote는 별도 인프라 없이 systemd 생태계 안에서 빠르게 로그를 중앙화하고 싶을 때 고려할 만한 가벼운 선택지 정도로 이해하면 됩니다.
9. 실전 트러블슈팅 시나리오
시나리오 1: 서비스가 왜 죽었는지 추적하기
# 1. 현재 서비스 상태와 마지막 종료 코드 확인
systemctl status myapp
# 2. 해당 유닛의 최근 로그를 시간순으로 확인 (페이저 없이)
journalctl -u myapp -n 200 --no-pager
# 3. 만약 여러 번 재시작을 반복하고 있다면, 각 실행의 시작·종료 구간을 나눠서 확인
journalctl -u myapp --since "30 minutes ago" -o short-iso
# 4. 죽기 직전 시스템 전체 상황(메모리 부족, OOM 등)도 함께 확인
journalctl -k --since "30 minutes ago" --grep -i "killed process|out of memory"
서비스 장애의 첫 단추는 systemctl status로 마지막 종료 코드와 Active: 상태를 확인하는 것입니다. 여기서 Result: oom-kill처럼 명확한 원인이 보이면 곧바로 방향을 잡을 수 있지만, 대부분은 Result: exit-code처럼 애플리케이션 자체 오류만 표시되므로 journalctl -u myapp으로 실제 로그 본문을 봐야 합니다.
애플리케이션이 크래시와 재시작을 반복하고 있다면 (Restart=on-failure가 설정된 경우 흔한 패턴), 로그가 여러 실행 구간이 섞여서 출력됩니다. 이때는 -o short-iso로 정확한 타임스탬프를 켜서 각 재시작 시점을 구분하고, 첫 번째 크래시 직전의 로그에 집중하는 것이 중요합니다. 두 번째, 세 번째 재시작의 로그는 대개 같은 원인이 반복되는 것이므로 첫 크래시 하나만 깊게 봐도 충분한 경우가 많습니다.
애플리케이션 로그에 특별한 단서가 없다면, 커널이 메모리 부족으로 프로세스를 강제 종료(OOM kill)했을 가능성을 의심해야 합니다. journalctl -k로 커널 메시지를 확인하면 Out of memory: Killed process <PID> (myapp) 같은 명확한 로그를 찾을 수 있습니다.
시나리오 2: 부팅이 느려진 원인 찾기
# 1. 전체 부팅 소요 시간과 단계별 요약 확인
systemd-analyze
# 2. 가장 오래 걸린 유닛들을 정렬해서 확인
systemd-analyze blame
# 3. 특정 유닛이 무엇을 기다리며 지연됐는지 임계 경로(critical chain) 확인
systemd-analyze critical-chain
# 4. journalctl로 해당 부팅 구간의 실제 로그를 시간순으로 대조
journalctl -b -u <지연이_의심되는_유닛> -o short-monotonic
systemd-analyze는 부팅 시간을 초 단위로 요약해 커널 초기화, initrd, 사용자 공간(userspace) 각 단계가 얼마나 걸렸는지 보여줍니다. systemd-analyze blame은 개별 유닛이 시작을 완료하기까지 걸린 시간을 내림차순으로 정렬해, 어떤 서비스가 부팅을 지연시키는 “범인”인지 빠르게 짚어줍니다.
여기서 journalctl이 필요한 이유는, systemd-analyze는 얼마나 걸렸는지는 알려주지만 왜 오래 걸렸는지는 알려주지 않기 때문입니다. -o short-monotonic 출력 포맷은 부팅 시작(0초)을 기준으로 한 상대 시간을 보여주므로, systemd-analyze critical-chain이 짚어준 지연 구간의 시각과 journalctl 로그의 타임스탬프를 직접 대조하며 그 유닛이 무엇을 기다리고 있었는지(네트워크 대기, DNS 조회 타임아웃, 디스크 마운트 대기 등) 확인할 수 있습니다. 두 도구를 항상 함께 쓰는 것이 부팅 지연 분석의 기본 패턴입니다.
시나리오 3: 디스크가 가득 찼을 때 저널 정리
# 1. 디스크 전체 사용량에서 저널이 차지하는 비중 확인
df -h /var/log
journalctl --disk-usage
# 2. 급한 불부터 끄기: 즉시 용량 확보
sudo journalctl --vacuum-size=200M
# 3. 근본 원인 확인: 어떤 유닛이 로그를 폭주시키고 있는지 찾기
journalctl --since "1 hour ago" -o short | awk '{print $5}' | sort | uniq -c | sort -rn | head
# 4. 재발 방지: journald.conf에 상한 설정 (7절 참고) 후 재시작
sudo systemctl restart systemd-journald
디스크 포화는 대개 두 원인 중 하나입니다. journald 자체가 오랜 기간 상한 없이 쌓인 경우이거나, 특정 애플리케이션이 짧은 시간에 예외를 반복적으로 로깅하며 저널을 폭증시킨 경우입니다. journalctl --disk-usage로 저널이 실제로 얼마를 차지하는지 먼저 확인하고, df -h로 본 전체 디스크 사용량과 비교하면 저널이 범인인지 아닌지 빠르게 판단할 수 있습니다.
급한 상황에서는 --vacuum-size로 즉시 공간을 확보하는 것이 우선입니다. 다만 이는 증상 완화일 뿐이므로, 반드시 어떤 유닛이 로그를 과도하게 남기고 있었는지 확인하는 후속 작업이 필요합니다. 특정 서비스 하나가 짧은 루프에서 반복적으로 에러를 로깅하고 있다면 --vacuum만으로는 며칠 안에 같은 문제가 재발합니다. 근본 원인이 된 애플리케이션 버그를 고치는 것과 별개로, journald.conf에 상한을 설정해 두면 앞으로는 같은 패턴이 발생해도 디스크 전체를 위협하지는 않게 됩니다.
10. 자주 하는 실수와 로그가 안 보일 때 체크리스트
journalctl을 처음 쓸 때 “로그가 안 보인다”, “권한 오류가 난다” 같은 문제를 자주 겪습니다. 다음 순서대로 확인하면 대부분의 원인을 빠르게 좁힐 수 있습니다.
1. 권한 문제
# 일반 사용자로 실행했을 때 일부 로그만 보이거나 오류가 나는 경우
journalctl
# 대부분 권한 부족: sudo로 실행하거나
sudo journalctl
# systemd-journal 그룹에 사용자를 추가해 sudo 없이도 조회 가능하게 만들기
sudo usermod -aG systemd-journal $USER
# 이후 로그아웃 후 재접속해야 그룹 변경이 적용됨
일반 사용자 계정은 기본적으로 다른 사용자나 시스템 유닛의 로그를 볼 권한이 없습니다. systemd-journal 그룹에 속한 사용자는 sudo 없이도 대부분의 로그를 읽을 수 있지만, 그룹에 새로 추가한 직후에는 반드시 재로그인해야 적용된다는 점을 놓치기 쉽습니다. usermod로 그룹을 추가했는데도 여전히 권한 오류가 난다면 세션을 갱신하지 않은 경우가 대부분입니다.
2. Storage=none 설정 확인
grep -i storage /etc/systemd/journald.conf
일부 컨테이너 베이스 이미지나 최소화된 환경에서는 의도적으로 Storage=none으로 설정해 저널 저장 자체를 꺼두는 경우가 있습니다. 이 경우 journalctl을 아무리 실행해도 로그가 나타나지 않으며, journald 설정을 auto나 persistent로 바꾸고 재시작해야 로그가 쌓이기 시작합니다.
3. 시간 필터 조건 재확인
--since/--until 조건이 실제 로그가 발생한 시각과 어긋나 있으면 로그가 없는 것처럼 보입니다. 특히 서버의 타임존 설정이 예상과 다를 때(UTC vs 로컬 시간) 이런 착오가 자주 발생합니다. timedatectl로 현재 시스템 타임존과 시각을 먼저 확인하고, 의심스러우면 시간 필터를 아예 빼고 -n으로 최근 로그부터 확인하는 것이 안전합니다.
4. 유닛 이름 오타
-u 뒤에 지정한 유닛 이름이 실제 유닛 이름과 다르면(예: myapp vs my-app.service) 결과가 비어 있는 것으로 나타납니다. systemctl list-units --type=service --all이나 journalctl -F _SYSTEMD_UNIT으로 실제 등록된 유닛 이름을 먼저 확인하는 습관이 필요합니다.
5. journald 자체가 문제를 겪는 경우
# journald 자체의 상태 확인
systemctl status systemd-journald
# journald의 저널 파일 손상 여부 점검
journalctl --verify
드물지만 비정상 종료 등으로 저널 파일 자체가 손상되어 일부 구간을 읽지 못하는 경우가 있습니다. journalctl --verify는 저널 파일의 체크섬을 검증해 손상된 구간이 있는지 알려줍니다. 손상이 확인되면 systemctl restart systemd-journald로 journald를 재시작해 새 파일로 회전시키는 것이 가장 간단한 복구 방법이며, 손상된 과거 로그 자체는 대부분 복구할 수 없다는 점도 함께 알아두어야 합니다.
마치며
journalctl은 처음에는 옵션이 많아 복잡해 보이지만, 핵심은 결국 “무엇으로 필터링할 것인가”로 요약됩니다. 시간(--since/--until), 유닛(-u), 부팅(-b), 심각도(-p), 텍스트(--grep), 그리고 더 정밀하게는 필드(FIELD=value)까지, 이 축들을 조합할 줄 알면 방대한 로그 속에서도 원하는 정보를 빠르게 찾아낼 수 있습니다.
또한 로그는 쌓아두기만 하면 언젠가 디스크를 위협하는 자원이라는 점도 잊지 말아야 합니다. journald.conf에 SystemMaxUse 같은 상한을 미리 설정해 두는 습관이, 사고가 난 뒤 --vacuum으로 급하게 정리하는 것보다 항상 낫습니다.
이 글에서 다루지 않은 systemctl, apt, ufw 등 우분투 서버 운영 전반은 우분투 필수 명령어 가이드에서, 리눅스 커널과 시스템 내부 동작은 리눅스 완전 가이드에서 이어서 확인할 수 있습니다. 범용 파일·프로세스 명령어가 필요하다면 리눅스·맥 명령어 실전 가이드를, 디스크 용량 문제를 더 깊게 다루고 싶다면 리눅스 디스크 full vs inode 트러블슈팅을 함께 참고하시길 권합니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- 개발자가 알아야 할 우분투 필수 명령어 및 활용법 | apt·systemd·ufw 실전 가이드
- Linux 완전 가이드 — 커널 내부(CFS·페이지 캐시·VFS·시스템콜)와 프로덕션 패턴
- 개발자를 위한 리눅스·맥 명령어 실전 가이드 | 네트워크·파일·프로세스·디버깅
- Linux 디스크 full vs inode full 해결 순서 | 용량·아이노드 트러블슈팅