새벽 3시, DB 백업 스크립트가 실행됐어야 하는데 다음 날 아침에 확인해보니 결과물이 없습니다. crontab에 분명히 등록했는데 왜 안 됐는지 알 방법조차 없습니다. 로그도 없고, 에러도 없고, 그냥 조용히 실패한 겁니다. 반대로 디스크가 서서히 가득 차가는데, 매주 정리해야 할 로그 파일을 지우는 작업을 매번 수동으로 하고 있다면? cron을 제대로 이해하면 이 두 문제가 모두 해결됩니다.
Cron & 작업 스케줄링
서버는 잠들지 않습니다. 새벽 2시에 데이터베이스를 백업하고, 매주 일요일마다 오래된 로그를 정리하고, 매시간 디스크 사용량을 체크하는 일들을 사람이 직접 할 수는 없습니다. 이런 반복 작업을 자동화하는 것이 cron과 systemd timer입니다.
cron은 Unix 시스템에서 수십 년간 사용되어 온 작업 스케줄러입니다. 간단하고 강력하지만, 몇 가지 함정이 있어 처음 접하는 사람들이 자주 실수를 겪습니다. 이 챕터에서는 cron의 동작 원리부터 실무에서 바로 쓸 수 있는 예제, 그리고 흔한 트러블슈팅까지 모두 다룹니다.
학습 목표
- cron 데몬이 어떻게 동작하는지 내부 원리를 이해할 수 있다
- crontab 5-필드 문법(
분 시 일 월 요일)을 정확히 읽고 쓸 수 있다 - 특수 문자(
*,,,-,/)와 단축어(@daily,@reboot)를 활용할 수 있다 crontab -e,crontab -l,crontab -r의 차이와 위험성을 이해할 수 있다/etc/crontab,/etc/cron.d/, 사용자 crontab의 차이를 설명할 수 있다/etc/cron.hourly/등 디렉토리 기반 스케줄링을 활용할 수 있다- cron 실행 로그를 확인하고 문제를 진단할 수 있다
- systemd timer 유닛을 작성하고 cron과 비교할 수 있다
- PATH 누락, 이메일 출력, 실수 삭제 등 실무 트러블슈팅을 해결할 수 있다
- 1cron 데몬 동작 원리와 crontab 파일 구조
- 25-필드 문법(분 시 일 월 요일) 및 특수 문자 완전 분석
- 3crontab -e / -l / -r 명령 사용법과 실수 방지
- 4/etc/crontab, /etc/cron.d/, cron.hourly/daily/weekly/monthly 디렉토리 활용
- 5cron 실행 로그 확인 및 PATH·환경변수 트러블슈팅
- 6systemd timer 유닛 작성과 cron 비교
systemctl status cron # 또는 systemctl status crondcrontab -lgrep CRON /var/log/syslog | tail -20 # 또는 journalctl -u croncrontab 삭제 전 반드시 crontab -l > ~/crontab.bak 으로 백업할 것
cron 데몬 동작 원리

새벽 3시에 DB 백업 스크립트가 실행됐어야 하는데, 다음 날 아침에 확인해보니 결과물이 없습니다. crontab에 분명히 등록했는데 왜 안 됐는지 알 방법이 없습니다. cron이 실제로 어떻게 돌아가는지 이해하면 — 어떤 사용자 권한으로 실행되는지, 환경변수가 왜 일반 터미널과 다른지, 실행 기록이 어디에 남는지 — 이런 무음 실패(silent failure) 상황을 추적할 수 있습니다.
cron은 데몬(daemon) 프로세스입니다. 시스템이 부팅될 때 시작되어 백그라운드에서 계속 실행되며, 1분마다 깨어나 실행할 작업이 있는지 확인합니다.
crond 프로세스
# cron 데몬이 실행 중인지 확인
systemctl status cron # Debian/Ubuntu
systemctl status crond # RHEL/CentOS/Rocky
# 프로세스 목록에서 확인
ps aux | grep cron
root 847 0.0 0.0 16568 1024 ? Ss Mar25 0:00 /usr/sbin/cron -f
동작 흐름
cron 데몬은 시작할 때 모든 crontab 파일을 메모리에 읽어들입니다. 이후 매 분마다 현재 시각과 각 작업의 스케줄을 비교해, 일치하는 작업을 별도의 자식 프로세스로 실행합니다.
[crond 데몬]
│
├─ 부팅 시: /etc/crontab, /etc/cron.d/*, 사용자 crontab 로드
│
└─ 매 분마다:
├─ 현재 시각 확인 (예: 02:00, 월요일)
├─ 일치하는 항목 탐색
└─ fork() → 자식 프로세스에서 명령어 실행
crontab 파일이 저장되는 위치
사용자가 crontab -e로 편집한 내용은 시스템 어딘가에 저장됩니다. 직접 편집하면 안 되지만, 위치를 알아두면 유용합니다.
# 사용자 crontab 저장 위치
ls /var/spool/cron/crontabs/ # Debian/Ubuntu
ls /var/spool/cron/ # RHEL/CentOS
# 현재 사용자 crontab 파일
cat /var/spool/cron/crontabs/$(whoami)
중요:
/var/spool/cron/파일을 직접 편집하지 마세요. 반드시crontab -e명령을 사용해야 합니다. 직접 수정하면 cron이 변경 사항을 인식하지 못하거나, 파일이 손상될 수 있습니다.
crontab 변경 감지
최신 cron 구현체들은 파일 변경을 자동으로 감지합니다. crontab -e로 저장하면 데몬이 파일 변경을 감지하고 자동으로 재로드합니다. 별도로 systemctl reload cron을 실행할 필요가 없습니다.
crontab 문법 완전 분석

*/5 * * * *와 0 */5 * * *는 전혀 다른 의미입니다. 하나는 5분마다, 다른 하나는 5시간 정각마다 실행됩니다. cron 문법의 5개 시각 필드에서 *, */n, ,, - 각각이 어떤 의미인지 헷갈리면 의도와 다른 시각에 작업이 실행되고, 운이 나쁘면 너무 자주 돌아서 서버에 부하를 줍니다. 한 번 제대로 파악해두면 이후 crontab 작성은 몇 초면 됩니다.
crontab의 각 줄은 6개 필드로 구성됩니다. 처음 5개는 실행 시각을 정의하고, 마지막은 실행할 명령어입니다.
┌───────────── 분 (0-59)
│ ┌─────────── 시 (0-23)
│ │ ┌───────── 일 (1-31)
│ │ │ ┌─────── 월 (1-12)
│ │ │ │ ┌───── 요일 (0-7, 0과 7 모두 일요일)
│ │ │ │ │
* * * * * /path/to/command
기본 예제
# 매일 새벽 2시 30분에 실행
30 2 * * * /usr/local/bin/backup.sh
# 매시간 정각에 실행
0 * * * * /usr/local/bin/health-check.sh
# 매주 월요일 오전 9시에 실행
0 9 * * 1 /usr/local/bin/weekly-report.sh
# 매월 1일 자정에 실행
0 0 1 * * /usr/local/bin/monthly-cleanup.sh
# 매일 오전 6시와 오후 6시에 실행
0 6,18 * * * /usr/local/bin/twice-daily.sh
특수 문자 4가지
* (별표) — 모든 값
해당 필드의 모든 유효한 값을 의미합니다.
# 매 분마다 실행 (분 필드가 *)
* * * * * /path/to/command
# 매일 오전 8시 (분=0, 시=8, 나머지는 모두)
0 8 * * * /path/to/command
, (쉼표) — 목록
여러 값을 나열합니다.
# 매주 월요일과 수요일과 금요일 오전 9시
0 9 * * 1,3,5 /path/to/command
# 1월, 4월, 7월, 10월 1일 자정 (분기별)
0 0 1 1,4,7,10 * /path/to/command
- (하이픈) — 범위
연속된 값의 범위를 지정합니다.
# 월~금(1-5) 오전 9시
0 9 * * 1-5 /path/to/command
# 오전 9시~오후 5시 매시간 정각 (업무 시간)
0 9-17 * * 1-5 /path/to/command
/ (슬래시) — 간격
"N마다"를 의미합니다. */N 또는 범위/N 형태로 씁니다.
# 15분마다 실행
*/15 * * * * /path/to/command
# 2시간마다 실행 (0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22시)
0 */2 * * * /path/to/command
# 오전 8시~오후 8시 사이 2시간마다
0 8-20/2 * * * /path/to/command
복합 예제
# 평일 업무시간(9-18시)에 30분마다
*/30 9-18 * * 1-5 /path/to/command
# 매월 첫째 주 월요일과 셋째 주 월요일에 실행 (날짜와 요일을 같이 쓰면 OR 조건)
0 9 1-7,15-21 * 1 /path/to/command
주의: 일(day of month)과 요일(day of week)을 둘 다
*가 아닌 값으로 지정하면 OR 조건으로 처리됩니다. 예를 들어0 0 15 * 5는 "매월 15일 또는 매주 금요일 자정"입니다. 이는 많은 사람이 착각하는 함정입니다.
@단축어
자주 쓰이는 패턴을 위한 단축어입니다.
| 단축어 | 동등한 표현 | 설명 |
|---|---|---|
@reboot | (해당 없음) | 시스템 부팅 후 1회 실행 |
@yearly / @annually | 0 0 1 1 * | 매년 1월 1일 자정 |
@monthly | 0 0 1 * * | 매월 1일 자정 |
@weekly | 0 0 * * 0 | 매주 일요일 자정 |
@daily / @midnight | 0 0 * * * | 매일 자정 |
@hourly | 0 * * * * | 매시간 정각 |
# 부팅 시 VPN 클라이언트 시작
@reboot /usr/local/bin/start-vpn.sh
# 매일 자정에 백업
@daily /usr/local/bin/daily-backup.sh
# 매주 일요일 자정에 정리 작업
@weekly /usr/local/bin/weekly-cleanup.sh
crontab 명령어 사용법
실습 전 디렉토리와 예제 파일을 먼저 준비합니다.
# 실습 디렉토리 준비
mkdir -p /tmp/linux/part5/exam_222 && cd /tmp/linux/part5/exam_22
# 백업 스크립트 생성
cat > /tmp/linux/part5/exam_222/backup.sh << 'EOF'
#!/bin/bash
BACKUP_DIR="/tmp/linux/part5/exam_222/backups"
mkdir -p "$BACKUP_DIR"
DATE=$(date +%Y%m%d_%H%M%S)
tar -czf "$BACKUP_DIR/backup_$DATE.tar.gz" /tmp/linux/part5/exam_222/*.sh 2>/dev/null
echo "$(date): Backup completed → backup_$DATE.tar.gz" >> "$BACKUP_DIR/backup.log"
EOF
# 정리 스크립트 생성
cat > /tmp/linux/part5/exam_222/cleanup.sh << 'EOF'
#!/bin/bash
# 7일 이상 된 로그 파일 삭제
find /tmp/linux/part5/exam_222/backups -name "*.tar.gz" -mtime +7 -delete
echo "$(date): Cleanup done" >> /tmp/linux/part5/exam_222/backups/cleanup.log
EOF
chmod +x /tmp/linux/part5/exam_222/*.sh
이제 실습을 진행합니다.
crontab -e는 현재 사용자의 crontab을 편집기로 엽니다. 기본 편집기는 시스템 설정에 따라 다르지만, 보통 nano 또는 vi입니다.
# 현재 사용자의 crontab 편집
crontab -e
# 특정 사용자의 crontab 편집 (root만 가능)
sudo crontab -e -u www-data
sudo crontab -e -u deploy
편집기가 열리면 맨 아래에 새 줄을 추가합니다. 파일을 저장하고 닫으면 cron이 자동으로 새 설정을 로드합니다.
# 편집기 저장 후 확인 메시지
crontab: installing new crontab
편집기 변경 방법
# 기본 편집기를 nano로 변경
export VISUAL=nano
export EDITOR=nano
# 또는 영구 설정 (~/.bashrc에 추가)
echo 'export VISUAL=nano' >> ~/.bashrc
echo 'export EDITOR=nano' >> ~/.bashrc
source ~/.bashrc
현재 등록된 crontab 전체 내용을 출력합니다. 편집기를 열지 않고 확인만 할 때 사용합니다.
# 현재 사용자의 crontab 목록
crontab -l
# 특정 사용자의 crontab 목록 (root만 가능)
sudo crontab -l -u deploy
# crontab이 없을 경우 출력
# no crontab for username
출력 예시
# 매일 새벽 2시 DB 백업
0 2 * * * /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1
# 매주 일요일 로그 정리
0 3 * * 0 /usr/local/bin/log-cleanup.sh >> /var/log/cleanup.log 2>&1
# 15분마다 헬스 체크
*/15 * * * * /usr/local/bin/health-check.sh
crontab 백업 (중요!)
# 현재 crontab을 파일로 백업
crontab -l > ~/crontab-backup-$(date +%Y%m%d).txt
# 백업 파일 확인
cat ~/crontab-backup-20260326.txt
crontab을 수정하기 전에 항상 백업을 만들어 두는 습관을 들이세요.
crontab -r은 현재 사용자의 crontab 전체를 즉시, 확인 없이 삭제합니다. 실수로 실행하면 복구가 매우 어렵습니다.
# 경고: 이 명령은 crontab 전체를 삭제합니다!
crontab -r
# 삭제 전 확인을 요구하는 안전한 버전
crontab -ri
# remove crontab for user? (y/n)
-r과 -e는 키보드에서 인접해 있어 실수하기 쉽습니다. 항상 -ri를 사용하거나, 삭제 전에 백업을 만드세요.
# 안전한 삭제 방법 (확인 포함)
crontab -ri
# 또는 삭제 전 백업 후 삭제
crontab -l > /tmp/crontab-backup.txt && crontab -r
echo "백업 저장됨: /tmp/crontab-backup.txt"
실수로 삭제했을 때 복구 방법은 이 챕터 아래 트러블슈팅 섹션에서 다룹니다.
- crontab -l 을 실행했을 때 등록한 항목이 목록에 보인다
- crontab -e 저장 직후 'crontab: installing new crontab' 메시지가 출력된다
- grep CRON /var/log/syslog | tail -5 에서 실행 기록(CMD)이 나타난다
- crontab -ri 실행 시 삭제 확인 프롬프트가 나타난다
시스템 전체 crontab 구조
crontab 사용 보안 제어: /etc/cron.allow와 /etc/cron.deny
리눅스 시스템은 특정 사용자에게만 crontab을 생성할 수 있는 권한을 제한하는 강력한 보안 통제 메커니즘을 제공합니다.
- /etc/cron.allow 파일이 존재하는 경우 (화이트리스트):
- 오직 이 파일에 등록된 사용자만
crontab명령을 수행할 수 있습니다. - 이 경우
/etc/cron.deny파일은 완전히 무시됩니다.
- 오직 이 파일에 등록된 사용자만
- /etc/cron.allow 파일이 없고 /etc/cron.deny 파일만 존재하는 경우 (블랙리스트):
cron.deny파일에 등록된 사용자를 제외한 모든 사용자가crontab을 사용할 수 있습니다.
- 두 파일이 모두 존재하지 않는 경우:
- 시스템 환경(Ubuntu/CentOS 등)에 따라 root만 가능하게 제한되거나, 모든 사용자가 사용 가능하도록 기본 동작합니다.
사용자 crontab 외에도 시스템 전체에 적용되는 crontab 파일들이 있습니다.
/etc/crontab — 시스템 crontab
cat /etc/crontab
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# m h dom mon dow user command
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
/etc/crontab은 사용자 crontab과 달리 6번째 필드에 실행 사용자가 있습니다.
분 시 일 월 요일 사용자 명령어
0 2 * * * root /usr/local/bin/backup.sh
/etc/cron.d/ — 패키지별 crontab
애플리케이션 패키지들이 독립적인 cron 파일을 /etc/cron.d/에 설치합니다. /etc/crontab과 동일한 문법(사용자 필드 포함)을 사용합니다.
# cron.d 파일 목록 확인
ls /etc/cron.d/
# 예: unattended-upgrades가 설치한 cron 파일
cat /etc/cron.d/apt-compat
# 직접 패키지용 cron 파일 생성 예시
sudo vim /etc/cron.d/myapp
# 파일 내용
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# myapp 데이터 수집 - 5분마다
*/5 * * * * www-data /opt/myapp/bin/collect-metrics.sh >> /var/log/myapp/metrics.log 2>&1
세 가지 방식 비교
| 방식 | 사용자 필드 | 용도 | 편집 방법 |
|---|---|---|---|
crontab -e | 없음 (파일 소유자가 실행) | 개인/서비스 계정 작업 | crontab -e |
/etc/crontab | 있음 | 시스템 전체 작업 | sudo vim /etc/crontab |
/etc/cron.d/*.conf | 있음 | 패키지/앱별 작업 | sudo vim /etc/cron.d/파일명 |
시간 간격이 고정된 작업은 crontab 문법 없이 스크립트를 디렉토리에 넣기만 하면 됩니다. run-parts 명령이 해당 디렉토리의 모든 실행 가능한 파일을 실행합니다.
# 각 디렉토리 확인
ls /etc/cron.hourly/
ls /etc/cron.daily/
ls /etc/cron.weekly/
ls /etc/cron.monthly/
스크립트 배치 방법
# 매일 실행할 스크립트 작성
sudo vim /etc/cron.daily/cleanup-tmp
#!/bin/bash
# /tmp 파일 중 7일 이상 된 것 삭제
find /tmp -type f -mtime +7 -delete
find /tmp -type d -empty -delete
# 실행 권한 부여 (필수!)
sudo chmod +x /etc/cron.daily/cleanup-tmp
# 확인
ls -la /etc/cron.daily/cleanup-tmp
중요한 제약 사항
# 파일명에 점(.)이 있으면 run-parts가 실행하지 않습니다!
# 잘못된 예:
/etc/cron.daily/backup.sh # .sh 확장자 때문에 실행 안 됨!
# 올바른 예:
/etc/cron.daily/backup # 확장자 없음
/etc/cron.daily/db-backup # 하이픈은 허용
언제 실행되는지 확인
/etc/cron.daily가 정확히 언제 실행되는지는 /etc/crontab의 설정에 따라 다릅니다. anacron이 설치된 경우 정확한 시각보다는 "하루 이내에 한 번"이 보장됩니다.
# anacron 설정 확인 (지연 실행 설정)
cat /etc/anacrontab
cron 로그 확인
cron이 작업을 실행했는지, 오류가 있었는지 확인하는 방법입니다.
syslog에서 cron 로그 찾기
# Ubuntu/Debian: syslog에서 CRON 항목 찾기
grep CRON /var/log/syslog
# 실시간 모니터링
grep CRON /var/log/syslog | tail -f
# journald 사용 시스템 (RHEL/CentOS/Fedora)
journalctl -u crond
journalctl -u cron --since "today"
journalctl -u cron -f # 실시간
# 특정 시간대 로그
journalctl -u cron --since "2026-03-26 02:00" --until "2026-03-26 03:00"
로그 출력 예시
Mar 26 02:00:01 myserver CRON[12345]: (root) CMD (/usr/local/bin/db-backup.sh)
Mar 26 02:00:03 myserver CRON[12345]: (root) MAIL (mailed 0 byte of output to root)
Mar 26 03:00:01 myserver CRON[12389]: (deploy) CMD (/usr/local/bin/health-check.sh)
각 줄의 의미:
CRON[PID]: cron 데몬이 실행한 자식 프로세스 ID(user) CMD: 어떤 사용자가 어떤 명령을 실행했는지(user) MAIL: 출력 결과를 이메일로 전송했는지 (바이트가 0이면 출력 없음)
작업 출력을 직접 로그 파일로 저장
# crontab에 로그 리다이렉션 추가
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# 날짜 포함 로그
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup-$(date +\%Y\%m\%d).log 2>&1
%(퍼센트) 이스케이프: crontab 파일에서%는 줄 바꿈 문자로 해석됩니다. 명령어 안에서%를 사용하려면 반드시\%로 이스케이프해야 합니다.
# 잘못된 예 (% 때문에 오동작)
0 2 * * * echo $(date +%Y-%m-%d) >> /var/log/test.log
# 올바른 예 (\ 이스케이프)
0 2 * * * echo $(date +\%Y-\%m-\%d) >> /var/log/test.log
systemd timer
systemd timer: 현대적인 작업 스케줄링

cron으로 등록한 배치 작업이 실패했는데 로그가 없어서 원인을 모를 때, systemd timer를 썼다면 journalctl -u db-backup.service로 바로 확인할 수 있습니다. cron의 가장 큰 단점은 작업 출력이 기본적으로 메일로만 가고, 로그를 별도로 리다이렉션하지 않으면 흔적이 남지 않는다는 점입니다. systemd timer는 cron보다 설정이 조금 더 복잡하지만, journald와 통합된 로깅, 작업 간 의존성 설정, 실행 실패 시 재시도 정책 등 운영 환경에서 필요한 기능을 갖추고 있습니다.
systemd가 있는 시스템에서는 cron 대신 systemd timer를 사용할 수 있습니다. timer는 .service 유닛과 .timer 유닛 두 파일로 구성됩니다.
기본 구조
timer를 만들려면 두 개의 유닛 파일이 필요합니다:
- 실제 작업을 정의하는
.service파일 - 실행 스케줄을 정의하는
.timer파일
.service 유닛 예시
# /etc/systemd/system/db-backup.service
sudo vim /etc/systemd/system/db-backup.service
[Unit]
Description=데이터베이스 일일 백업
After=network.target
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/db-backup.sh
StandardOutput=journal
StandardError=journal
Type=oneshot: 한 번 실행하고 종료되는 작업에 사용합니다.
.timer 유닛 예시
# /etc/systemd/system/db-backup.timer
sudo vim /etc/systemd/system/db-backup.timer
[Unit]
Description=매일 새벽 2시 DB 백업 타이머
Requires=db-backup.service
[Timer]
OnCalendar=*-*-* 02:00:00
AccuracySec=1min
Persistent=true
[Install]
WantedBy=timers.target
주요 Timer 옵션
| 옵션 | 설명 | 예시 |
|---|---|---|
OnCalendar | 절대 시각 기반 | *-*-* 02:00:00, Mon *-*-* 09:00:00 |
OnBootSec | 부팅 후 N초/분/시간 후 | OnBootSec=5min |
OnUnitActiveSec | 마지막 실행 후 N초/분/시간 후 | OnUnitActiveSec=1h |
AccuracySec | 실행 시각의 허용 오차 | AccuracySec=1s (정확도 높임) |
Persistent=true | 시스템이 꺼진 동안 실행됐어야 할 작업을 재시작 시 실행 | — |
OnCalendar 표현식
# 매일 새벽 2시
OnCalendar=*-*-* 02:00:00
# 매주 일요일 오전 3시
OnCalendar=Sun *-*-* 03:00:00
# 평일 오전 9시
OnCalendar=Mon..Fri *-*-* 09:00:00
# 매월 1일 자정
OnCalendar=*-*-01 00:00:00
# 15분마다
OnCalendar=*:0/15
# 표현식 검증
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
timer 활성화 및 관리
# 데몬 재로드
sudo systemctl daemon-reload
# timer 활성화 (부팅 시 자동 시작)
sudo systemctl enable db-backup.timer
# timer 시작
sudo systemctl start db-backup.timer
# timer 상태 확인
systemctl status db-backup.timer
# 모든 timer 목록
systemctl list-timers --all
# timer 로그 확인
journalctl -u db-backup.service
systemctl list-timers 출력 예시
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2026-03-27 02:00:00 KST 21h left Wed 2026-03-26 02:00:01 KST 2h 59min ago db-backup.timer db-backup.service
Thu 2026-03-27 03:00:00 KST 22h left Wed 2026-03-26 03:00:01 KST 1h 59min ago log-cleanup.timer log-cleanup.service
cron vs systemd timer 비교
| 항목 | cron | systemd timer |
|---|---|---|
| 설정 방법 | 단일 crontab 줄 | .service + .timer 두 파일 |
| 로그 | syslog (제한적) | journald (풍부한 메타데이터) |
| 실패 처리 | 없음 | OnFailure=, Restart= 가능 |
| 의존성 | 없음 | After=, Requires= 등 지원 |
| 놓친 실행 처리 | anacron으로 별도 처리 | Persistent=true로 내장 지원 |
| 복잡도 | 낮음 | 높음 |
| 적합한 경우 | 간단한 반복 작업 | 복잡한 의존성, 상세 모니터링 필요 시 |
실무 예제
운영 데이터베이스를 매일 자동으로 백업하는 실무 예제입니다. PostgreSQL 기준이지만, MySQL도 유사하게 적용할 수 있습니다.
백업 스크립트 작성
sudo vim /usr/local/bin/db-backup.sh
#!/bin/bash
# PostgreSQL 일일 백업 스크립트
# /usr/local/bin/db-backup.sh
set -euo pipefail
# 설정
DB_USER="postgres"
DB_NAME="production_db"
BACKUP_DIR="/var/backups/postgresql"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${DATE}.sql.gz"
LOG_FILE="/var/log/db-backup.log"
# 로그 함수
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# 백업 디렉토리 생성
mkdir -p "$BACKUP_DIR"
log "백업 시작: ${DB_NAME}"
# pg_dump로 백업 (gzip 압축)
if pg_dump -U "$DB_USER" "$DB_NAME" | gzip > "$BACKUP_FILE"; then
BACKUP_SIZE=$(du -sh "$BACKUP_FILE" | cut -f1)
log "백업 성공: ${BACKUP_FILE} (${BACKUP_SIZE})"
else
log "오류: 백업 실패"
exit 1
fi
# 오래된 백업 삭제 (30일 이상)
DELETED_COUNT=$(find "$BACKUP_DIR" -name "*.sql.gz" -mtime +${RETENTION_DAYS} -print | wc -l)
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +${RETENTION_DAYS} -delete
log "오래된 백업 ${DELETED_COUNT}개 삭제 (${RETENTION_DAYS}일 초과)"
log "백업 완료"
# 스크립트 실행 권한 부여
sudo chmod +x /usr/local/bin/db-backup.sh
# 수동으로 먼저 테스트
sudo -u postgres /usr/local/bin/db-backup.sh
cat /var/log/db-backup.log
crontab 등록
# postgres 사용자로 crontab 편집
sudo crontab -e -u postgres
# 또는 root crontab에 사용자 지정
sudo crontab -e
# postgres 사용자 crontab에 추가
# 매일 새벽 2시 DB 백업
0 2 * * * /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1
또는 /etc/cron.d/에 파일로 추가:
sudo vim /etc/cron.d/postgresql-backup
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 매일 새벽 2시 PostgreSQL 백업
0 2 * * * postgres /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1
백업 확인
# 백업 파일 목록
ls -lh /var/backups/postgresql/
# 로그 확인
tail -20 /var/log/db-backup.log
# cron 실행 기록
grep "db-backup" /var/log/syslog
# 또는
journalctl -u cron --since "today" | grep db-backup
MySQL/MariaDB 버전
#!/bin/bash
# MySQL 백업 버전 핵심 부분
mysqldump -u root --password="$DB_PASS" \
--all-databases \
--single-transaction \
--routines \
--triggers \
| gzip > "$BACKUP_FILE"
서버 로그 파일이 쌓이면 디스크가 가득 찰 수 있습니다. 매주 자동으로 오래된 로그를 정리하는 스크립트를 만들어 봅니다.
참고:
/etc/logrotate.d/를 사용하는 logrotate가 일반적인 로그 로테이션 도구이지만, 커스텀 로그 디렉토리나 특수한 처리가 필요할 때는 직접 스크립트를 작성합니다.
로그 정리 스크립트
sudo vim /usr/local/bin/log-cleanup.sh
#!/bin/bash
# 주간 로그 정리 스크립트
# /usr/local/bin/log-cleanup.sh
set -euo pipefail
LOG_DIRS=(
"/var/log/myapp"
"/var/log/nginx"
"/opt/services/logs"
)
KEEP_DAYS=14 # 14일 이상된 로그 삭제
COMPRESS_DAYS=3 # 3일 이상된 로그 압축
REPORT_FILE="/var/log/log-cleanup-report.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$REPORT_FILE"
}
log "===== 주간 로그 정리 시작 ====="
log "실행 시각: $(date)"
TOTAL_FREED=0
for DIR in "${LOG_DIRS[@]}"; do
if [[ ! -d "$DIR" ]]; then
log "디렉토리 없음, 건너뜀: $DIR"
continue
fi
log "처리 중: $DIR"
# 3일 이상된 .log 파일 gzip 압축
COMPRESSED=0
while IFS= read -r -d '' file; do
gzip -f "$file" && COMPRESSED=$((COMPRESSED + 1))
done < <(find "$DIR" -name "*.log" -not -name "*.gz" -mtime +${COMPRESS_DAYS} -print0)
log " 압축: ${COMPRESSED}개 파일"
# 14일 이상된 파일 삭제 전 크기 계산
BEFORE=$(du -sb "$DIR" 2>/dev/null | cut -f1 || echo 0)
# 14일 이상된 압축 로그 삭제
DELETED=$(find "$DIR" -name "*.gz" -mtime +${KEEP_DAYS} -print | wc -l)
find "$DIR" -name "*.gz" -mtime +${KEEP_DAYS} -delete
# 빈 디렉토리 정리
find "$DIR" -type d -empty -not -path "$DIR" -delete 2>/dev/null || true
AFTER=$(du -sb "$DIR" 2>/dev/null | cut -f1 || echo 0)
FREED=$(( (BEFORE - AFTER) / 1024 / 1024 ))
TOTAL_FREED=$((TOTAL_FREED + FREED))
log " 삭제: ${DELETED}개 파일, ${FREED}MB 확보"
done
log "===== 정리 완료: 총 ${TOTAL_FREED}MB 확보 ====="
# 현재 디스크 사용량 리포트
log "현재 디스크 사용량:"
df -h / /var /opt 2>/dev/null | tee -a "$REPORT_FILE" || true
sudo chmod +x /usr/local/bin/log-cleanup.sh
# 수동 테스트
sudo /usr/local/bin/log-cleanup.sh
cat /var/log/log-cleanup-report.log
crontab 등록
sudo crontab -e
# 매주 일요일 새벽 3시 로그 정리 (DB 백업 완료 후)
0 3 * * 0 /usr/local/bin/log-cleanup.sh >> /var/log/log-cleanup.log 2>&1
디스크 사용량 모니터링과 함께 사용
# 디스크 90% 초과 시 알림 (5분마다 체크)
*/5 * * * * df -h / | awk 'NR==2 {gsub(/%/,"",$5); if($5>=90) print "경고: 디스크 사용량 "$5"%%"}' | logger -t disk-monitor
# 알림 확인
grep "disk-monitor" /var/log/syslog
트러블슈팅
cron에서 가장 흔한 문제입니다. 터미널에서는 잘 되는 스크립트가 cron에서는 "command not found" 오류와 함께 실패합니다.
원인
cron은 매우 제한된 환경에서 실행됩니다. 터미널에서 실행할 때의 PATH와 cron의 PATH가 다릅니다.
# 터미널에서의 PATH (풍부한 경로)
echo $PATH
# /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/snap/bin
# cron의 기본 PATH (매우 제한적)
# /usr/bin:/bin
/usr/local/bin에 설치된 node, python3, pg_dump 등을 cron에서 찾지 못하면 실패합니다.
진단 방법
# cron에서 환경 변수 덤프하기 (임시 디버깅)
* * * * * env > /tmp/cron-env.txt
# 1분 후 확인
cat /tmp/cron-env.txt
SHELL=/bin/sh
HOME=/root
LOGNAME=root
PATH=/usr/bin:/bin ← 이것이 cron의 PATH
해결 방법 1: crontab 파일 상단에 PATH 설정
crontab -e
# crontab 파일 상단에 추가
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 이제 아래 작업들은 위의 PATH를 사용
0 2 * * * /usr/local/bin/backup.sh
*/5 * * * * health-check.sh # PATH에 있으므로 전체 경로 불필요
해결 방법 2: 스크립트 내부에서 절대 경로 사용
#!/bin/bash
# 절대 경로 사용
/usr/bin/python3 /opt/myapp/script.py
/usr/local/bin/pg_dump -U postgres mydb > /tmp/backup.sql
/usr/bin/docker compose ps
해결 방법 3: 스크립트 첫 줄에서 PATH 확장
#!/bin/bash
# 스크립트 시작 시 PATH 명시적 설정
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 이후 일반 명령어 사용 가능
node --version
python3 script.py
기타 환경 변수 관련 문제
PATH 외에도 다음 환경 변수들이 문제가 될 수 있습니다:
# crontab에서 환경 변수 설정
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/root
LANG=ko_KR.UTF-8 # 한국어 로케일이 필요한 경우
DB_PASSWORD=secret # 환경 변수로 비밀번호 전달
0 2 * * * /usr/local/bin/backup.sh
cron은 기본적으로 작업의 표준 출력(stdout)이 있으면 시스템 메일로 전송하려 합니다. 메일 서버 설정에 따라 /var/mail/root에 쌓이거나, 메일 전송 실패 오류가 로그에 가득 차게 됩니다.
문제 확인
# 시스템 메일함 확인
cat /var/mail/root
# 또는
mail
# 로그에서 메일 관련 오류 확인
grep CRON /var/log/syslog | grep -i mail
해결 방법 1: 출력을 /dev/null로 버리기
# 표준 출력과 표준 에러 모두 버리기
0 2 * * * /usr/local/bin/backup.sh > /dev/null 2>&1
# 표준 에러만 버리기 (표준 출력은 메일로)
0 2 * * * /usr/local/bin/backup.sh 2>/dev/null
# 표준 출력은 로그 파일로, 에러는 버리기
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>/dev/null
해결 방법 2: MAILTO 환경 변수로 메일 비활성화
crontab -e
# 전체 crontab에 대해 메일 비활성화
MAILTO=""
0 2 * * * /usr/local/bin/backup.sh
*/15 * * * * /usr/local/bin/health-check.sh
MAILTO=""로 설정하면 모든 작업의 출력이 메일로 전송되지 않습니다. 하지만 스크립트 자체에서 출력이 없도록 설계하거나, 로그 파일로 리다이렉션하는 것이 더 좋은 방식입니다.
해결 방법 3: 특정 주소로 메일 받기
로그가 필요한 경우 MAILTO에 이메일 주소를 설정합니다:
MAILTO="admin@example.com"
0 2 * * * /usr/local/bin/backup.sh
권장 방법: 로그 파일로 리다이렉션
실무에서는 출력을 버리지 말고 로그 파일에 저장하는 것이 좋습니다:
MAILTO=""
# 로그 파일에 저장하되, 에러도 포함
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# 스크립트 내부에서 타임스탬프와 함께 로깅
0 2 * * * { echo "=== $(date) ==="; /usr/local/bin/backup.sh; } >> /var/log/backup.log 2>&1
crontab -r을 -e 대신 잘못 입력했거나, 습관적으로 엔터를 눌러버린 경우입니다. 확인 없이 즉시 모든 crontab을 삭제하는 매우 위험한 명령입니다.
즉시 할 일: 패닉하지 말고 체계적으로 접근
# 현재 상태 확인 (이미 삭제됐는지)
crontab -l
# no crontab for username ← 삭제된 상태
복구 방법 1: 백업 파일에서 복원
평소에 crontab을 백업해두었다면:
# 백업 파일에서 복원
crontab ~/crontab-backup-20260325.txt
# 또는 /tmp에 있는 경우
crontab /tmp/crontab-backup.txt
# 복원 확인
crontab -l
복구 방법 2: 시스템 메일에서 복원 단서 찾기
cron이 실행 결과를 메일로 보내는 설정이었다면:
# 시스템 메일에서 cron 관련 내용 확인
cat /var/mail/$(whoami) | grep -A5 "Cron"
mail
복구 방법 3: 로그에서 과거 실행 명령 재구성
# syslog에서 과거 cron 실행 기록 확인
grep CRON /var/log/syslog | grep CMD
grep CRON /var/log/syslog.1 | grep CMD # 이전 로그 파일
# journald에서 확인
journalctl -u cron --since "7 days ago" | grep CMD
Mar 25 02:00:01 myserver CRON[1234]: (root) CMD (/usr/local/bin/db-backup.sh)
Mar 25 03:00:01 myserver CRON[1256]: (root) CMD (/usr/local/bin/log-cleanup.sh)
이 로그에서 어떤 스크립트가 언제 실행됐는지 파악해 crontab을 재구성합니다.
복구 방법 4: /etc/cron.d/ 와 /etc/crontab 확인
사용자 crontab이 삭제된 경우, 시스템 crontab은 영향을 받지 않습니다:
# 시스템 crontab은 삭제 안 됨
cat /etc/crontab
ls /etc/cron.d/
앞으로 예방하기
# 1. alias로 -r를 항상 -ri로 교체
echo "alias crontab='crontab -i'" >> ~/.bashrc
source ~/.bashrc
# 2. crontab 편집 전후 자동 백업 스크립트
# ~/.local/bin/safe-crontab.sh
#!/bin/bash
BACKUP_DIR="$HOME/.crontab-backups"
mkdir -p "$BACKUP_DIR"
crontab -l > "$BACKUP_DIR/crontab-$(date +%Y%m%d-%H%M%S).txt" 2>/dev/null || true
crontab "$@"
# 3. crontab을 Git으로 관리
mkdir -p ~/dotfiles/cron
crontab -l > ~/dotfiles/cron/crontab
cd ~/dotfiles && git add -A && git commit -m "crontab 업데이트"
핵심 교훈: crontab -e를 사용하기 전에 항상 crontab -l > ~/crontab-backup.txt로 백업을 만드세요. 이 한 줄이 몇 시간의 재구성 작업을 막을 수 있습니다.
고급 활용 팁
중복 실행 방지 (flock 활용)
작업 실행 시간이 다음 실행 시각을 넘어버릴 경우, 중복으로 실행되어 문제가 생길 수 있습니다. flock으로 이를 방지합니다.
# flock으로 중복 실행 방지
*/10 * * * * /usr/bin/flock -n /tmp/myapp.lock /usr/local/bin/myapp.sh
# -n: 락을 획득하지 못하면 즉시 포기 (non-blocking)
# -w 30: 최대 30초 대기 후 포기
*/10 * * * * /usr/bin/flock -w 30 /tmp/myapp.lock /usr/local/bin/myapp.sh
여러 명령을 한 줄에
# &&로 순차 실행 (앞이 성공해야 뒤가 실행)
0 2 * * * /usr/local/bin/prepare.sh && /usr/local/bin/backup.sh
# ;로 순차 실행 (앞의 성공 여부 상관없이)
0 2 * * * /usr/local/bin/step1.sh ; /usr/local/bin/step2.sh
# 여러 명령을 그룹으로
0 2 * * * { /usr/local/bin/step1.sh; /usr/local/bin/step2.sh; } >> /var/log/combined.log 2>&1
조건부 실행
# 파일이 존재할 때만 실행
0 * * * * [ -f /var/run/myapp.pid ] && /usr/local/bin/monitor.sh
# 특정 프로세스가 실행 중일 때만 실행
*/5 * * * * pgrep -x nginx > /dev/null && /usr/local/bin/nginx-stats.sh
환경별 crontab 관리
# 프로덕션 서버에서만 실행
ENVIRONMENT=$(cat /etc/environment-name 2>/dev/null || echo "dev")
0 2 * * * [ "$ENVIRONMENT" = "production" ] && /usr/local/bin/prod-only.sh
실습 과제
기본 실습
실습 1: 다음 조건의 crontab 항목을 작성하세요.
- 매주 평일(월~금) 오전 8시 55분에
/usr/local/bin/morning-check.sh실행 - 실행 결과를
/var/log/morning-check.log에 append 저장
실습 2: 현재 시스템의 crontab 구성을 확인하세요.
# 다음 명령을 실행하고 결과를 분석하세요
crontab -l
cat /etc/crontab
ls /etc/cron.d/
ls /etc/cron.daily/
실습 3: 다음 crontab 표현식이 언제 실행되는지 설명하세요.
0 */6 * * *
30 9-17/2 * * 1-5
0 0 1,15 * *
@reboot
*/30 * * * 1,3,5
응용 실습
실습 4: 다음 시나리오의 crontab을 작성하고 테스트하세요.
- 매 5분마다
/tmp/heartbeat.txt에 현재 시각을 기록하는 스크립트 - 5분 후 파일이 생성됐는지 확인
# 힌트: 스크립트
#!/bin/bash
echo "$(date '+%Y-%m-%d %H:%M:%S') - alive" >> /tmp/heartbeat.txt
# crontab 등록 힌트
*/5 * * * * /usr/local/bin/heartbeat.sh
실습 5: systemd timer로 5분마다 헬스 체크를 실행하는 유닛 파일 두 개(.service, .timer)를 작성하고 활성화하세요.
요약
| 개념 | 핵심 내용 |
|---|---|
| cron 데몬 | 매 분마다 깨어나 스케줄 확인, 자식 프로세스로 작업 실행 |
| 5-필드 문법 | 분 시 일 월 요일 — 순서를 외워두세요 |
| 특수 문자 | *(모두), ,(목록), -(범위), /(간격) |
| @단축어 | @reboot, @daily, @weekly 등 |
| crontab -e | 편집 (저장 시 자동 로드) |
| crontab -l | 목록 확인 |
| crontab -r | 전체 삭제 (위험! 항상 -ri 사용) |
| /etc/crontab | 시스템 전체, 사용자 필드 포함 |
| /etc/cron.d/ | 패키지별 cron 파일, 사용자 필드 포함 |
| /etc/cron.daily/ | 파일 배치만으로 매일 실행, 확장자 금지 |
| PATH 문제 | cron의 PATH는 제한적 — 절대 경로 또는 명시적 PATH 설정 |
| MAILTO="" | cron 메일 출력 비활성화 |
| systemd timer | 풍부한 로그, 의존성 지원, 놓친 실행 처리 |
cron은 단순하지만 강력합니다. PATH 문제만 주의하면 대부분의 자동화 작업을 안정적으로 처리할 수 있습니다. 복잡한 워크플로우나 상세한 모니터링이 필요하다면 systemd timer를 검토하세요.
다음 모듈에서는 logrotate와 journald를 활용해 서버 로그를 자동으로 압축·분할하고 디스크 풀 장애를 예방하는 방법을 다룹니다.