전통적인 cron 대신 systemd 타이머를 사용해야 하는 이유와, 일정 표현부터 지속성·깨우기·무작위 지연 같은 강력한 기능까지 실용적으로 살펴본다.
내가 가장 좋아하는 환유적 기술 용어는 "cron job"이다. cron이 문자 그대로 일정에 따라 동작을 실행하는 데몬이 아닐 수도 있는데도, cron처럼 걷고 cron처럼 꽥꽥거리면 우리는 무엇이든 그 용어를 붙인다. Patrick McKenzie가 즐겨 지적하듯이, cron 작업은 가장 탁월하게 유용한 컴퓨팅 원시 요소 가운데 하나다. 거의 모두가 갖고 있는 수많은 사용 사례에 대해 그 효용이 즉시 분명하다. 이것 을 매일 하라. 저것 을 한 달에 한 번 하라.
그런데도 말이다. 예약 작업에는 아마도 문자 그대로의 cron(또는 그보다 현대적인 친척들)을 쓰지 않는 편이 좋다! 2026년에는 더 현대적인 선택지가 있고, 내가 가장 좋아하는 것은 소박한 systemd 타이머다. 나는 systemd 타이머를 사랑한다. 아직 사랑하지 않는다면, 왜 당신도 그것을 사랑해야 하는지 그 이유를 보여줄 수 있을지도 모른다.
cron? 익었나?systemd timer 는 특정 일정에 따라 다른 유닛(대개 서비스)을 예약하는 유닛의 한 종류다. (systemd service 유닛이 어떻게 동작하는지는 다른 글의 주제지만, systemd 타이머의 .service 대상을 논리적으로 스크립트라고 생각해도 된다.) 타이머는 전통적인 cron 데몬을 기능적으로 대체하며(물론 둘 다 함께 실행할 수도 있다), 타이머의 달력 설정은 전통적인 cron류 표현식과의 간극을 메우는 데 도움이 되는 몇 가지 유사성을 제공한다.
이쯤 되면 systemd 반대론자들이 systemd 프로젝트의 일부라는 이유와, 오래되고 성숙했지만 투박한 기술을 대체한다는 이유로 타이머를 격침시키기 위해 슬그머니 모습을 드러낸다. cron을 두고 논쟁하며 시간을 쓰고 싶지는 않으니, 왜 오랜 사후적 통찰의 혜택을 받은 systemd 타이머 같은 더 새로운 해법이 더 나은지 잠깐 생각해 보자.
$PATH 설정 때문에 cron 스크립트 실행은 예측하기 어렵다.stdout과 stderr 출력은 종종 블랙홀로 사라진다(그리고 흔히 호스트의 메일 시스템으로 보내지는데, 대개는 원하는 동작이 아니다.)01,31 04,05 1-15 1,6 * 는 사람이 읽기에 쉽지도 직관적이지도 않다.덧붙여 말하면, 타이머는 이런 문제를 모두(그리고 그 이상도) 해결한다.
기본기는 큰 의식 없이도 다룰 수 있다. 먼저 타이머가 실행할 대상이 필요하다. systemd가 동작 중인 Linux 호스트에서, 다음 유닛 내용을 /etc/systemd/system/roulette.service 에 두면 10분의 1 확률로 공짜가 되는(즉, 컴퓨터를 종료하는) 서비스가 설치된다.
Systemd
[Unit]
Description=1 in 10 chance to break your chains
[Service]
ExecStart=/usr/bin/env bash -c '[[ $(($RANDOM % 10)) == 0 ]] && systemctl poweroff || echo LIVE ANOTHER DAY'
업데이트: [2026-05-05 화]
Twitter 상호 팔로우인 HSVSphere가 지적하길, 서비스 옵션 ExecCondition= 은 조건부 실행을 다루는 네이티브한 방법을 제공한다. 이는 "계속 실행해야 하는가?"를 표현하는 더 긴밀하게 통합된 방식이며, 유닛 수준에서 의도를 더 명확하게 나타내는 방법이라고 나도 동의한다(여기서는 NixOS 시스템을 위해 절대 경로를 사용한다).
Systemd
[Unit]
Description=1 in 10 chance to break your chains
[Service]
ExecCondition=/run/current-system/sw/bin/bash -c '[[ $(($RANDOM % 10)) == 0 ]]'
ExecStart=/run/current-system/sw/bin/systemctl poweroff
이것은 앞선 bash 조건식과 같은 효과를 내며, 조건이 충족되었을 때 저널에 남는 문구가 달라져서 (내 생각에는) 상황을 더 분명하게 표현해 준다.
2026년 5월 5일 11:05:32 diesel systemd[3117]: Condition check resulted in 1 in 10 chance to break your chains being skipped.
일반적으로, 스스로 스크립팅하기보다 systemd가 제시하는 옵션을 활용하는 편이 더 좋은 경험이다. (다른 예로는 서비스 스크립트가 실패했을 때 반응하기 위해 OnFailure= 를 쓰거나, 일시적 실패에서 복구를 시도하기 위해 Restart= 를 사용하는 것이 있다.)
같은 파일 줄기(roulette)를 가진 파일을 /etc/systemd/system/roulette.timer 에 두어 그 service 를 timer 와 연결하자.
Systemd
[Unit]
Description=impending destruction
[Timer]
OnCalendar=10:00
[Install]
WantedBy=timers.target
여기서 연결한다 는 말의 뜻은, 기본적으로 타이머의 Unit= 설정이 같은 줄기에 .service 를 붙인 서비스 유닛을 고른다는 것이다. 이 경우에는 roulette.service 다. 다른 이름의 서비스 유닛을 실행하고 싶다면 언제든 이것을 바꿀 수 있다.
바로 몇 가지를 짚고 넘어가고 싶다.
ExecStart= 대상은 기본적으로 셸 명령으로 실행되지 않는다. 절대 경로 대상은 스크립트처럼, 또는 이 경우 문자열 인수로 스크립트를 기대하는 인터프리터처럼 다뤄야 한다. 예를 들어 ExecStart=/usr/bin/echo Hello | /usr/bin/awk 는 여기서는 맥락상 파이프가 아무 의미가 없으므로 전혀 동작하지 않는다.ExecStart= 인수는 기본적으로 어떤 환경 변수도 상속받지 않으므로(일부 시스템 관리자 기본값 제외), 처음에는 꽤 비어 있는 $PATH 로 시작한다. /usr/bin/env 를 실행하는 것은 systemctl 같은 것들이 사용 가능하도록 만드는 지름길이지만, 기본 상태 그대로면 깨끗한 상태에서 시작하게 된다. 만약 단순히 ExecStart=/usr/bin/bash 를 썼다면 $PATH 에 기본적인 것들은 있겠지만, 여기서 env 를 쓰는 것은 추가적인 안전장치다.타이머의 도움 없이도 주사위를 굴릴 수 있다.
shell
systemctl start roulette
다만 사용 가능한 [Install] 섹션이 없기 때문에 이 서비스는 enable 할 수 없다는 점 은 주의하자. 우리의 타이머가 서비스를 일관된 방식으로 실행하게 만드는 정석적 방법이다. 또 한 가지 유용한 점은 systemctl 이 명시적 접미사 없이도 기본적으로 roulette.service 에 대해 동작한다는 것이다.
.timer 유닛에 systemctl start 하위 명령을 적용하면, 말하자면 시계를 돌기 시작하게 만들지만 Unit= 대상을 실제로 실행하지는 않는다.
shell
systemctl start roulette.timer
이제 timer 는 활성화되었지만 service 는 아니다.
시점에 따라 status 는 타이머가 다음으로 언제 당신의 운명을 결정할지를 알려준다.
shell
systemctl status roulette.timer
status 페이지에서는 다음 발동 시각을 포함해 타이머에 관한 많은 정보를 볼 수 있다.
Trigger: Sat 2026-04-18 10:00:00 MDT; 35min left
이것이 가장 단순한 타이머 입문이다. 대상 하나를 만들고, 일정이 있는 타이머를 그 대상 서비스 파일 옆에 두고, 일정이 시작되도록 타이머를 시작한다(대상을 시작하는 것이 아니다). .timer 가 [Install] 안에 WantedBy= 를 정의하므로, start 했을 때뿐 아니라 부팅 시에도 타이머가 올라오도록 보장할 수 있다.
shell
systemctl enable roulette.timer
이제 기초를 지나 다음으로 가 보자.
타이머에 관한 정보 가운데 가장 중요한 것은 아마도 일정을 어떻게 표현하느냐일 것이다. 반복되는 시간 구간(매뉴얼에서는 보통 time span이라고 부른다)인지, 달력 이벤트(또는 타임스탬프)인지 말이다. 다행히 systemd.time(7) 의 man 페이지는 예제가 많고 실제로 아주 훌륭하다고 생각한다. 타이머를 작성할 때는 그것을 첫 번째 자료로 써야 한다. 음, 평범한 글쓴이가 쓰는 평범한 블로그 글보다 좋다, 아니 더 낫다.
systemd는 systemd-analyze 라는 명령행 도구도 함께 제공하는데, 여기에는 시간 표현식을 명령줄에서 직접 검증하고 설명하는 기능이 들어 있어서 이를 명령형으로 이해하는 데 도움이 된다. 고전적인 와일드카드 cron 표현식도 systemd-analyzer 가 파싱한 뒤 예상 실행 시각까지 포함해 설명해 줄 수 있다.
shell
systemd-analyze calendar '*-*-* *:*:*'
Normalized form: *-*-* *:*:*
Next elapse: Sat 2026-04-18 16:44:26 MDT
(in UTC): Sat 2026-04-18 22:44:26 UTC
From now: 431ms left
이 블로그 글은 systemd.time(7) 전체를 그대로 재현할 장소가 아니므로, 유익한 매뉴얼을 읽어보기를 권한다(RTHM). 작게 요약하자면, 꽤 간단하게 반복되는 벽시계 기준 기간을 정의할 수도 있고, 또는 낡은 cron 과 달리 어떤 이전 사건을 기준으로 한 반복 기간도 정의할 수 있다.
첫 번째 시간 표현식 범주는 상상하기 쉽다. 예를 들어, 완전한 형식에서 daily 는 다음을 뜻한다.
*-*-* 00:00:00
│ │ │ │ │ ╰── 초 00에
│ │ │ │ ╰───── 분 00에
│ │ │ ╰──────── 시 00에
│ │ ╰────────── 매일
│ ╰──────────── 매월
╰────────────── 매년
daily 같은 축약어를 쓸 수도 있고, 완전한 형식을 풀어 쓸 수도 있으며, systemd.time(7) 에 나열된 다른 지원 값도 사용할 수 있다. 그리고 나서 systemd-analyze 로 자신의 가정을 검증하면 된다.
두 번째 시간 표현식 범주는 "어떤 다른 사건을 기준으로 이것을 실행하라"에 해당한다. 이것은 "매일 같은 시각에 실행하라"와는 구별되며, 매우 자주 당신이 실제로 원하는 것이기도 하다. 예를 들어 임시 디렉터리를 비우는 작업을 생각해 보자. 부팅 직후 cron 표현식이 발동했다면 /tmp 에는 정리할 것이 거의 없을 가능성이 크다. 하지만 "컴퓨터가 시작한 뒤 한 시간 후에 실행하고, 그 뒤로도 매 시간마다 실행하라"고 인코딩하면, 일정 로직이 관련 서비스가 실제로 하는 일과 의미 있게 맞아떨어진다.
이것은 타이머에서 쉽게 할 수 있다.
Systemd
[Timer]
OnBootSec=1h
OnUnitActiveSec=1h
즉, "기계가 시작한 한 시간 뒤에 실행하라"(이것은 한 번 실행된다) 그리고 "내 Unit= 이 실행된 한 시간 뒤에도 실행하라"(이것이 암묵적으로 타이머를 무기한 반복하게 만든다)라는 뜻이다.
이런 주기적 시간 간격은 "이따금씩 한 번"이라는 사용 사례에 놀랄 만큼 자주 잘 맞으며, "매 시간의 이 분에 실행" 같은 표현보다 더 적합한 경우가 많다. 또 다른 좋은 예로, 나는 매년 12월에 친구들을 위해 만든 Slack 봇이 Advent of Code API를 폴링하도록 하는 타이머를 사용한다. */15 cron 표현식은 그들의 API 요청 정책인 "15분마다"를 충족하지만, cron 언어에서 그것이 가장 쉬운 표현 방식이기 때문에 모두가 함께 API를 폴링하며 트래픽 급증을 만들 것이라고 확신한다! 내가 코드 수정을 해서 타이머를 시작했을 때, 15분이 지날 때마다 실행되기만 하면 내가 신경 쓸 것은 그것뿐이며, 아마도 거대한 동시 몰림 문제도 덜 만들 것이다.
달력 단위와 시간 간격 단위의 구분은 아마 전통적인 cron 작업에서 가장 큰 개념적 도약일 테지만, 타이머는 그 밖에도 더 많은 것을 제공한다.
시스템의 타이머 상황을 한눈에 파악할 때 내가 가장 좋아하는 고수준 명령은 list-timers 하위 명령이다. 내 호스트의 요약은 이렇다.
shell
systemctl list-timers
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2026-04-20 15:15:00 MDT 1min 40s Mon 2026-04-20 15:00:05 MDT 13min ago zfs-snapshot-frequent.timer zfs-snapshot-frequent.service
Mon 2026-04-20 15:32:16 MDT 18min Mon 2026-04-20 14:22:15 MDT 51min ago fwupd-refresh.timer fwupd-refresh.service
Mon 2026-04-20 16:00:00 MDT 46min Mon 2026-04-20 15:00:05 MDT 13min ago logrotate.timer logrotate.service
Mon 2026-04-20 16:00:00 MDT 46min Mon 2026-04-20 15:00:05 MDT 13min ago zfs-snapshot-hourly.timer zfs-snapshot-hourly.service
Tue 2026-04-21 00:00:00 MDT 8h Mon 2026-04-20 09:43:22 MDT 5h 29min ago zfs-snapshot-daily.timer zfs-snapshot-daily.service
Tue 2026-04-21 07:31:28 MDT 16h Sun 2026-04-19 20:15:47 MDT 7h ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Mon 2026-04-27 00:00:00 MDT 6 days Mon 2026-04-20 09:43:22 MDT 5h 29min ago zfs-snapshot-weekly.timer zfs-snapshot-weekly.service
Mon 2026-04-27 01:09:27 MDT 6 days Mon 2026-04-20 09:43:22 MDT 5h 29min ago fstrim.timer fstrim.service
Mon 2026-04-27 04:28:38 MDT 6 days Mon 2026-04-20 09:43:22 MDT 5h 29min ago zpool-trim.timer zpool-trim.service
Fri 2026-05-01 00:00:00 MDT 1 week 3 days Wed 2026-04-01 10:07:51 MDT 1 week 1 day ago zfs-snapshot-monthly.timer zfs-snapshot-monthly.service
Fri 2026-05-01 03:17:17 MDT 1 week 3 days Wed 2026-04-01 10:07:51 MDT 1 week 1 day ago zfs-scrub.timer zfs-scrub.service
11 timers listed.
Pass --all to see loaded but inactive timers, too.
명령 하나로 타이머 일정에 따라 실행되는 모든 것의 전체 그림을 파악할 수 있다. 매우 유용하다.
list-timers 는 내가 꽤 자주 쓰는 systemd 하위 명령군의 일부다. 그 밖에 유용한 것으로는 list-units 와 list-paths 가 있다(list-paths 는 systemctl 에 비교적 최근 추가된 기능이다.)
중요한 스크립트를 실행하기 위해, 이를테면 노트북 뚜껑을 여는 물리적 행동을 당신이 직접 하지 않아도 일시 중단된 시스템을 깨운다는 것은 겉보기엔 벅찬 위업처럼 들리지만 WakeSystem= 을 발견하면 그렇지 않다.
WakeSystem=
Takes a boolean argument. If true, an elapsing timer will
cause the system to resume from suspend, should it be
suspended and if the system supports this.
...
이것이 얼마나 유용할지 상상할 수 있을 것이다. 사용하기 전에 패키지 업데이트를 다운로드해 둘 수 있는 배포판(예를 들어 Arch나 NixOS 같은)에서는, 밤늦게 업데이트 패키지를 미리 받아 두었다가 아침에 키보드 앞에 앉아 업데이트할 수 있다. 그리고 이것을 적용할 수 있는 다른 아이디어도 많다. 매뉴얼 페이지는 .service 가 끝난 뒤 다시 일시 중단되기를 의도한다면 수동으로 다시 일시 중단해야 한다는 점을 강조한다.
몇 문단 전에 살짝 언급한 거대한 동시 몰림 문제는, "여러 프로세스 집합이 동시에 깨어나면 어떻게 되는가?"라는 시스템 문제다. 세상의 모든 Debian 시스템이 00:00:00 에 apt update 하도록 하드코딩되어 있다면, 자정은 모두에게 나쁘고 뾰족한 시간대가 될 것이다.
FixedRandomDelay= 와 RandomizedOffsetSec= 라는 두 가지 타이머 옵션이 이를 도와준다.
FixedRandomDelay=
Takes a boolean argument. When enabled, the randomized delay
specified by RandomizedDelaySec= is chosen deterministically,
and remains stable between all firings of the same timer,
even if the manager is restarted. ...
RandomizedOffsetSec=
Offsets the timer by a stable, randomly-selected, and evenly
distributed amount of time between 0 and the specified time
value. ...
나는 실제로 소프트웨어 업데이트를 확인하는 시스템들에 이것을 사용해 왔다. 이것은 거대한 동시 몰림 문제를 돕는 것뿐 아니라, 균등 분포를 따라 실행을 퍼뜨림으로써 동작이 일관되게 유지되도록 하고 분산 서비스를 조율하고 있을지도 모르는 데몬 재시작 같은 방해성 활동을 피하게 해 준다.
전반적으로 시간 관련 옵션은 매우 세밀하게 설정할 수 있고, 엄청난 수준의 세분성을 노출한다(다시 말하지만, 이 모든 것은 man 페이지에 설명되어 있다.)
이 옵션은 일시 중단된 노트북 때문에 절대 건너뛰면 안 되지만 WakeSystem= 까지는 필요 없을 수도 있는 예약 스크립트에 특히 잘 맞는다.
Persistent=
Takes a boolean argument. If true, the time when the service
unit was last triggered is stored on disk. When the timer is
activated, the service unit is triggered immediately if it
would have been triggered at least once during the time when
the timer was inactive. ...
시스템이 구성 관리에 체크인하도록 예약해 두었는데 호스트가 다운타임을 겪었다면, .timer 에 Persistent= 를 붙이는 것은 온라인으로 돌아온 직후 곧바로 올바른 상태로 수렴하느냐, 아니면 타이머가 평소 발동할 때까지 기다리느냐(그게 꽤 오래일 수도 있다) 사이의 차이를 만들어낼 수 있다. 타이머가 놓친 활성화를 감지했을 때 기다리고 싶지 않은 서비스 활성화의 다른 좋은 예도 있다. 시스템 업데이트, 배치 작업 확인, 그런 종류의 것들이다.
타이머를 본격적으로 쓰기 시작한다면, 다음을 명심하자.
systemctl --user 로 상호작용하는 종류)는 완전히 유효하지만, [Install] 에 어떤 target 을 쓰는지 주의하라. 배포판에 따라서는 여기에 default.target 이 적절한 대상일 때도 있다.cron 때와 마찬가지로 그대로 적용된다. 동료 systemd 신도들은 timedatectl timesync-status 로 동기화 상태를 들여다볼 수 있다.