환경  systemd 249 · Ubuntu 22.04 · nginx · Apache 2.4 · PHP-FPM 8.4 실측 · 2026-10-18 확인

설정 파일 한 줄을 고치고 습관처럼 systemctl restart 를 쳤다. 그 순간 업로드 중이던 요청이 끊기고, 오타가 하나 섞여 있었던 탓에 서비스가 아예 올라오지 않았다. 반대로 reload 만 쳐 두고 안심했는데 유닛 파일에 넣은 환경 변수가 전혀 먹지 않은 날도 있다. 두 명령 다 '설정을 반영한다'로 기억하기 쉽지만 하는 일이 다르다.

나란히 놓으면 차이는 다섯 줄로 갈린다

restart 는 서비스를 멈췄다가 다시 띄운다. 프로세스가 새로 뜨니 설정도, 바이너리도, 환경 변수도 처음부터 다시 읽는다. 대신 멈추는 동안 들어온 요청과 이미 붙어 있던 연결은 그대로 끊긴다.

reload 는 프로세스를 살려 둔 채 '설정만 다시 읽어 달라'고 서비스에 신호를 보낸다. 무엇을 다시 읽고 무엇을 그대로 두는지는 systemd 가 아니라 그 서비스가 정한다. 그래서 같은 reload 라도 nginx 와 PHP-FPM 과 sshd 가 하는 일이 제각각이다.

먼저 한눈에 본다.

                              systemctl restart               systemctl reload
----------------------------  ------------------------------  ------------------------------------
프로세스                        멈췄다가 새로 띄운다              그대로 두고 신호만 보낸다
붙어 있던 연결                   끊긴다                          대부분 이어진다 (서비스마다 다르다)
설정 문법이 틀렸을 때             서비스가 안 올라온다              nginx·apache 는 옛 설정으로 계속 돈다
유닛 파일(.service) 변경         daemon-reload 없으면 반영 안 됨   daemon-reload 해도 반영 안 됨
reload 를 못 받는 서비스          상관없다                        실패하고 종료 코드 3

같은 '설정 반영'이라도 끊기는 것과 남는 것이 다르다

restart 는 systemd 가 하는 일이고, reload 는 서비스에게 맡기는 일이다.

reload 가 실제로 무엇을 하는지는 유닛 파일의 ExecReload 한 줄이 정한다

systemctl reload 를 치면 systemd 는 유닛 파일의 ExecReload= 에 적힌 명령을 돌린다. 이 값이 서비스마다 다르다. Ubuntu 22.04 서버에서 몇 개를 꺼내 보면 바로 보인다.

Apache 는 apachectl graceful 을 부른다. graceful 은 지금 열린 연결을 끊지 않고 워커를 새 설정으로 갈아 끼우며, 시작 전에 configtest 와 같은 문법 검사를 먼저 돈다. PHP-FPM 은 마스터 프로세스에 USR2 신호를 보내는데, 이건 워커를 순서대로 갈아 끼우면서 fpm 설정까지 다시 읽는다. sshd 는 sshd -t 로 설정을 먼저 검사하고, 통과해야 HUP 을 보낸다.

nginx 는 HUP 을 받으면 마스터가 새 설정의 문법을 확인하고, 새 로그 파일과 listen 소켓을 열어 본 뒤 성공하면 새 워커를 띄운다. 옛 워커는 받던 요청을 끝까지 처리하고 나서야 내려간다. 업로드 중이던 요청이 끊기지 않는 이유가 여기 있다.

# 이 서비스가 reload 를 받는지, 받으면 무엇을 하는지
systemctl show apache2    -p CanReload -p ExecReload
systemctl show php8.4-fpm -p CanReload -p ExecReload

# Ubuntu 22.04 에서 읽힌 값
#   apache2     CanReload=yes  /usr/sbin/apachectl graceful
#   php8.4-fpm  CanReload=yes  /bin/kill -USR2 $MAINPID
#   ssh         CanReload=yes  /usr/sbin/sshd -t  →  /bin/kill -HUP $MAINPID
#   cron        CanReload=no
#   mariadb     CanReload=no

reload 를 치기 전에 그 서비스의 ExecReload 부터 읽어 둔다

설정에 오타가 있을 때 둘의 결과가 가장 크게 벌어진다

restart 는 일단 멈추고 다시 띄우는 순서라, 새 설정이 깨져 있으면 멈춘 서비스가 그대로 못 올라온다. 고치는 동안 사이트는 내려가 있다. 유닛에 ExecStartPre 로 문법 검사를 걸어 둔 배포판 패키지도 있지만, 그 검사가 실패해도 이미 멈춘 뒤라 돌이킬 수 없다.

reload 는 사정이 다르다. nginx 는 새 설정 적용에 실패하면 바뀐 것을 되돌리고 옛 설정으로 계속 돈다. Apache graceful 과 sshd 의 ExecReload 도 검사를 먼저 하니, 오타가 있으면 reload 명령만 실패하고 돌고 있던 프로세스는 건드리지 않는다. systemctl 은 실패를 알려 주고 서비스 상태는 active 그대로다.

그래도 reload 의 이 안전망에 기대지 말고, 어느 쪽을 치든 그 전에 문법 검사를 직접 돌리는 편이 낫다. 실패 메시지를 읽는 자리가 터미널이냐 장애 알림이냐의 차이다.

# 반영 전에 문법부터
sudo nginx -t          && sudo systemctl reload nginx
sudo apachectl -t      && sudo systemctl reload apache2
sudo php-fpm8.4 -t     && sudo systemctl reload php8.4-fpm
sudo sshd -t           && sudo systemctl reload ssh

검사가 통과해야 다음 명령으로 넘어가게 && 로 묶는다

깨진 설정으로 restart 하면 서비스가 내려가고, reload 하면 대개 명령만 실패한다.

reload 를 받지 않는 서비스에는 아무 일도 일어나지 않는다

ExecReload 가 없는 유닛에 reload 를 치면 systemd 는 아무것도 하지 않고 실패를 돌려준다. 같은 서버의 cron 에 쳐 보면 'Job type reload is not applicable for unit cron.service.' 라는 메시지와 함께 종료 코드 3 으로 끝난다. MariaDB 도 같은 식이다.

문제는 스크립트에서 이 실패를 흘려 넘길 때다. 배포 스크립트 마지막 줄의 reload 가 조용히 실패하면 새 설정은 반영되지 않았는데 배포는 끝난 것처럼 보인다.

받는지 못 받는지 매번 따지기 싫으면 reload-or-restart 를 쓴다. reload 를 받는 서비스면 reload 하고, 아니면 멈췄다 다시 띄운다. 서비스가 돌고 있지 않으면 새로 띄운다. 돌고 있을 때만 건드리고 싶다면 try-reload-or-restart 를 쓴다.

sudo systemctl reload cron; echo "exit=$?"
# Failed to reload cron.service: Job type reload is not applicable for unit cron.service.
# exit=3

# 되면 reload, 안 되면 restart
sudo systemctl reload-or-restart cron

reload 실패는 종료 코드로만 드러난다

유닛 파일을 고쳤다면 둘 다 먼저 daemon-reload 가 있어야 한다

헷갈리기 쉬운 지점이 하나 더 있다. systemctl reload 가 다시 읽는 건 서비스 자신의 설정, 예를 들어 nginx.conf 나 httpd.conf 다. /etc/systemd/system 아래의 .service 파일이나 드롭인은 건드리지 않는다. 유닛 파일을 systemd 가 다시 읽게 하는 명령은 따로 있고, 이름이 daemon-reload 다.

그래서 유닛에 Environment= 를 추가하거나 LimitNOFILE= 을 올렸다면 daemon-reload 를 먼저 치고, 그다음 restart 를 해야 새 프로세스가 그 값을 들고 뜬다. reload 로는 안 된다. 돌고 있는 프로세스의 환경 변수와 자원 한도는 처음 뜰 때 정해진 값이라, 신호를 보내 설정 파일을 다시 읽게 해도 바뀌지 않는다.

daemon-reload 를 빠뜨렸는지는 NeedDaemonReload 속성으로 확인한다. yes 로 나오면 디스크의 유닛 파일과 systemd 가 들고 있는 내용이 어긋나 있다는 뜻이고, systemctl status 에도 daemon-reload 를 하라는 경고가 뜬다.

sudo systemctl edit myapp            # 드롭인에 Environment=APP_ENV=prod 추가
systemctl show myapp -p NeedDaemonReload
#   NeedDaemonReload=yes

sudo systemctl daemon-reload         # 유닛 파일을 다시 읽는다
sudo systemctl restart myapp         # 새 값으로 프로세스를 다시 띄운다
systemctl show myapp -p Environment

유닛 변경은 daemon-reload 다음 restart, reload 로는 안 먹는다

언제 어느 쪽을 쓰나

서비스 자체 설정 파일만 고쳤고 그 서비스가 reload 를 받으면 reload 를 쓴다. 가상 호스트 추가, 프록시 경로 변경, PHP-FPM 풀 설정 같은 것들이다. 연결을 살려 둔 채 반영되고, 설정이 깨져 있어도 대개 옛 설정으로 버틴다. 단, 앞에 문법 검사를 붙인다.

restart 가 맞는 경우도 분명히 있다. 패키지를 올려 바이너리가 바뀌었을 때, 유닛 파일의 환경 변수나 자원 한도를 바꿨을 때(daemon-reload 다음), 서비스가 reload 를 받지 않을 때, 그리고 reload 로는 다시 읽지 않는 항목을 고쳤을 때다. 어떤 항목이 reload 로 반영되는지는 서비스 문서가 정하니, 처음 바꾸는 설정이면 한 번은 확인해 둔다.

restart 에도 함정이 하나 있다. restart 는 작업이 걸려 있는 동안 유닛 자원을 다 비우지 않는다. 파일 디스크립터 저장소(FileDescriptorStoreMax=)가 대표적인데, 이것까지 깨끗이 비우고 싶다면 restart 대신 stop 과 start 를 따로 쳐야 한다.

서비스 설정만 바꿨다면 검사 후 reload, 유닛이나 바이너리가 바뀌었다면 restart.

조치 후 확인할 것

  • reload 를 치기 전에 systemctl show <서비스> -p CanReload -p ExecReload 로 그 서비스가 reload 를 받는지, 무엇을 하는지 읽어 둔다.
  • 반영 명령 앞에는 nginx -t, apachectl -t, php-fpm -t, sshd -t 같은 문법 검사를 && 로 묶는다.
  • 배포 스크립트의 reload 가 실패해도 넘어가지 않는지 확인한다. 종료 코드 3 을 흘리면 설정은 옛날 그대로다.
  • 유닛 파일이나 드롭인을 고쳤다면 NeedDaemonReload 가 no 인지 보고, daemon-reload 다음 restart 로 반영한다.
  • 반영 뒤 systemctl status 로 active 와 메인 PID 를 확인하고, restart 했다면 PID 가 바뀌었는지까지 본다.

restart 와 reload 는 둘 다 손에 익은 명령이라 그날 기분대로 고르기 쉽다. 그런데 하나는 프로세스를 갈아 끼우고 하나는 서비스에게 부탁만 한다. 치기 전에 그 서비스의 ExecReload 를 한 번 읽어 두고, 문법 검사를 앞에 붙이고, 유닛을 고쳤다면 daemon-reload 를 잊지 않는다. 그 세 가지면 설정 한 줄 고치다 사이트를 내리는 일은 거의 없다.