
서비스 · 프로세스 관리 26 / 50 · Part 3. systemd와 서비스
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
재부팅 후 웹 앱이 "DB 연결 실패"로 죽어 있었다. unit 파일에는 분명 Requires=mariadb.service가 적혀 있었는데 왜 이런 일이 생겼을까? 답은 Requires는 "함께 띄워라"일 뿐 "먼저 띄워라"가 아니기 때문 이다. systemd는 가능한 한 모든 것을 병렬로 시작하므로, 순서를 원하면 After=를 따로 써야 한다.
이번 글에서는 의존성 지시어(Wants, Requires)와 순서 지시어(After, Before)를 구분하고, 실제 서비스로 의존 대상이 멈출 때의 동작 과 순서가 없을 때의 경쟁 상태 를 측정한다.
| 지시어 | 의미 | 대상이 실패하거나 멈추면 |
|---|---|---|
Wants= | 가능하면 함께 시작 (약한 의존) | 나는 계속 동작 |
Requires= | 반드시 함께 시작 (강한 의존) | 나도 중지 |
BindsTo= | Requires보다 강함 | 대상이 사라지기만 해도 나도 중지 |
Requisite= | 이미 실행 중이어야 함 (시작해 주지 않음) | 시작 실패 |
PartOf= | 대상의 stop/restart를 따라감 (시작은 아님) | — |
Conflicts= | 동시에 실행 불가 | — |
| 지시어 | 의미 |
|---|---|
After=X | X의 시작이 끝난 뒤에 나를 시작 (중지는 반대 순서) |
Before=X | 내가 끝난 뒤에 X를 시작 |
순서 지시어는 함께 시작하게 만들지 않는다. After=X만 있고 Wants/Requires가 없으면, X가 다른 이유로 시작될 때만 순서가 적용된다.
[Install]의 WantedBy=/RequiredBy=는 enable 시 상대편 unit에 Wants/Requires를 추가 하는 역방향 선언이다. 22편의 *.wants/ 디렉터리가 바로 이것이다.

systemctl start lab-web
→ 의존성 계산: lab-web + (Requires) lab-db + (Wants) lab-cache → 하나의 트랜잭션
→ 순서 계산: After=lab-db lab-cache → db·cache 시작 완료 후 web
→ 실행
"시작 완료"의 기준은 Type 에 따라 다르다 (25편)
Type=simple : fork 직후 → After 가 있어도 실제 준비 전일 수 있다
Type=oneshot : 프로세스 종료 후 → 실습의 slowdb 처럼 확실히 기다린다
Type=notify : 프로그램이 READY=1 을 보낸 후 → 가장 정확
그래서 DB처럼 준비 시간이 긴 서비스는 Type=notify로 준비 완료를 알려야 After=가 의미 있게 동작한다.
# 1) Wants 와 Requires — web 은 db 를 Requires, cache 를 Wants
for n in db cache; do printf '[Unit]\nDescription=Lab %s\n[Service]\nExecStart=/bin/sleep infinity\n' $n > /etc/systemd/system/lab-$n.service; done
printf '[Unit]\nDescription=Lab web\nRequires=lab-db.service\nWants=lab-cache.service\nAfter=lab-db.service lab-cache.service\n[Service]\nExecStart=/bin/sleep infinity\n' > /etc/systemd/system/lab-web.service
systemctl daemon-reload; systemctl start lab-web
systemctl is-active lab-web lab-db lab-cache # web 만 시작해도 셋 다
systemctl list-dependencies lab-web --no-pager | head -5
systemctl stop lab-cache; systemctl is-active lab-web lab-cache # Wants 대상 중지
systemctl start lab-cache; systemctl stop lab-db; systemctl is-active lab-web lab-db # Requires 대상 중지
# 2) After 유무 비교 — slowdb 는 준비에 2초 걸리는 oneshot
# app1: Wants=lab-slowdb (After 없음) / app2: Wants + After=lab-slowdb
systemctl start lab-app1 --no-block; sleep 3; journalctl ... | grep -E 'app start|db ready'
systemctl stop lab-slowdb lab-app1; time systemctl start lab-app2; journalctl ... | grep -E 'app start|db ready'
systemctl show lab-web -p Requires,Wants,After --no-pager


텍스트 원본(실제 출력):
[root@rocky9-lab ~]# for n in db cache; do printf '[Unit]\nDescription=Lab %s\n[Service]\nExecStart=/bin/sleep infinity\n' $n > /etc/systemd/system/lab-$n.service; done
[root@rocky9-lab ~]# printf '[Unit]\nDescription=Lab web\nRequires=lab-db.service\nWants=lab-cache.service\nAfter=lab-db.service lab-cache.service\n[Service]\nExecStart=/bin/sleep infinity\n' > /etc/systemd/system/lab-web.service
[root@rocky9-lab ~]# systemctl daemon-reload; systemctl start lab-web
[root@rocky9-lab ~]# systemctl is-active lab-web lab-db lab-cache
active
active
active
[root@rocky9-lab ~]# systemctl list-dependencies lab-web --no-pager | head -5
lab-web.service
● ├─lab-cache.service
● ├─lab-db.service
● ├─system.slice
● └─sysinit.target
[root@rocky9-lab ~]# systemctl stop lab-cache; systemctl is-active lab-web lab-cache
active
inactive
[root@rocky9-lab ~]# systemctl start lab-cache; systemctl stop lab-db; systemctl is-active lab-web lab-db
inactive
inactive
[root@rocky9-lab ~]# journalctl -u lab-web -u lab-db --no-pager -o short-precise | tail -4
Sep 24 12:12:25.475189 rocky9-lab systemd[1]: Stopped Lab web.
Sep 24 12:12:25.475874 rocky9-lab systemd[1]: Stopping Lab db...
Sep 24 12:12:25.476367 rocky9-lab systemd[1]: lab-db.service: Deactivated successfully.
Sep 24 12:12:25.476576 rocky9-lab systemd[1]: Stopped Lab db.
[root@rocky9-lab ~]# printf '[Unit]\nDescription=Lab slow db\n[Service]\nType=oneshot\nRemainAfterExit=yes\nExecStart=/bin/bash -c "sleep 2; echo db ready"\n' > /etc/systemd/system/lab-slowdb.service
[root@rocky9-lab ~]# printf '[Unit]\nDescription=Lab app no-after\nWants=lab-slowdb.service\n[Service]\nType=oneshot\nRemainAfterExit=yes\nExecStart=/bin/echo app start\n' > /etc/systemd/system/lab-app1.service
[root@rocky9-lab ~]# printf '[Unit]\nDescription=Lab app with-after\nWants=lab-slowdb.service\nAfter=lab-slowdb.service\n[Service]\nType=oneshot\nRemainAfterExit=yes\nExecStart=/bin/echo app start\n' > /etc/systemd/system/lab-app2.service
[root@rocky9-lab ~]# systemctl daemon-reload; S=$(date '+%F %T'); systemctl start lab-app1 --no-block; sleep 3; journalctl -u lab-app1 -u lab-slowdb --since "$S" --no-pager -o short-precise | grep -E 'app start|db ready'
Sep 24 12:12:25.918146 rocky9-lab echo[3999]: app start
Sep 24 12:12:27.924626 rocky9-lab bash[4000]: db ready
[root@rocky9-lab ~]# systemctl stop lab-slowdb lab-app1; S=$(date '+%F %T'); time systemctl start lab-app2; journalctl -u lab-app2 -u lab-slowdb --since "$S" --no-pager -o short-precise | grep -E 'app start|db ready'
real 0m2.038s
user 0m0.005s
sys 0m0.006s
Sep 24 12:12:30.965278 rocky9-lab bash[4010]: db ready
Sep 24 12:12:30.979153 rocky9-lab echo[4012]: app start
[root@rocky9-lab ~]# systemctl show lab-web -p Requires,Wants,After --no-pager
Requires=sysinit.target system.slice lab-db.service
Wants=lab-cache.service
After=lab-cache.service lab-db.service sysinit.target systemd-journald.socket system.slice basic.target
| 관찰 | 의미 |
|---|---|
| lab-web만 start → web·db·cache 모두 active | 의존성에 따라 함께 시작 되었다 |
list-dependencies에 cache, db, system.slice, sysinit.target | 명시한 의존성 외에 기본 의존성 (slice, sysinit)이 자동으로 붙는다 |
| cache(Wants) 중지 → web active 유지 | 약한 의존. 캐시가 없어도 웹은 돈다 |
| db(Requires) 중지 → web inactive | 강한 의존. DB가 멈추면 웹도 함께 멈춘다 |
로그: Stopped Lab web. → Stopping Lab db... | db 중지 요청이 web을 먼저 멈추게 했다. After의 역순으로 중지된다 |
app1(After 없음): app start 25.918 → db ready 27.924 | 앱이 DB보다 2초 먼저 시작했다. 실제 서비스라면 연결 실패다 |
app2(After 있음): real 0m2.038s, db ready → app start | systemctl start가 DB 준비를 2초 기다린 뒤 앱을 시작했다 |
Requires=sysinit.target system.slice lab-db.service | 기본 의존성과 명시 의존성이 합쳐진 최종 값 |
After=... basic.target ... lab-db.service | 모든 일반 서비스는 기본적으로 basic.target 이후에 시작된다 |
| 주제 | 내용 |
|---|---|
| 보안 서비스의 순서 | 방화벽 규칙·auditd·로그 전송기는 네트워크 서비스보다 먼저 시작되어야 부팅 직후 공백이 없다. 예: Before=network-pre.target |
| 의존성으로 인한 연쇄 중지 | Requires=로 묶인 서비스는 하나가 멈추면 함께 멈춘다. 공격자가 하위 서비스를 멈춰 보안 서비스까지 연쇄 중지 시키지 않는지 의존 관계를 점검한다 |
| 악성 unit의 은닉 | 악성 unit은 자신을 WantedBy=로 정상 서비스나 target에 붙여 정상 서비스가 시작될 때 함께 실행 되게 한다. systemctl list-dependencies --reverse로 역방향을 확인한다 |
| Conflicts 악용 | Conflicts=auditd.service를 가진 unit이 시작되면 auditd가 중지된다. unit 점검 시 Conflicts도 본다 |
[Detection] 부팅 후 auditd 가 매번 inactive
↓
[확인] systemctl list-dependencies --reverse auditd.service ← 누가 auditd 를 끌어오나
grep -rl 'Conflicts=.*auditd' /etc/systemd/system /usr/lib/systemd/system
↓
[발견] /etc/systemd/system/sys-update.service : Conflicts=auditd.service, WantedBy=multi-user.target
↓
[판단] 부팅 시 자동 시작되어 auditd 를 중지시키는 unit → 방어 회피
↓
[Response] unit 격리·삭제, 생성 시각·생성 주체 조사, auditd 복구
| 명령 | 용도 |
|---|---|
systemctl list-dependencies UNIT | UNIT이 필요로 하는 것 |
systemctl list-dependencies --reverse UNIT | UNIT을 필요로 하는 것 |
systemctl show UNIT -p Requires,Wants,After,Conflicts | 최종 계산된 관계 |
systemd-analyze critical-chain | 부팅 순서상 오래 걸린 경로 |
| 실수 | 결과 | 예방 |
|---|---|---|
Requires=만 쓰고 After= 누락 | 병렬 시작 → 준비 전 접속 실패 | 의존성과 순서를 함께 |
모든 의존에 Requires= | 하나만 멈춰도 줄줄이 중지 | 필수가 아니면 Wants= |
After=network.target이면 네트워크 준비 완료로 오해 | 인터페이스 설정 전일 수 있음 | 필요하면 network-online.target (Wants+After) |
| Type=simple 서비스에 After만 믿음 | 실제 준비 전 다음 서비스 시작 | 준비 완료 알림(notify) 또는 앱 재시도 로직 |
| 순환 의존 작성 | systemd가 일부 job을 제거하며 경고 | systemd-analyze verify |
[ ] Requires 대상이 멈추면 나도 멈추는 것을 확인했다
[ ] Wants 대상이 멈춰도 나는 유지되는 것을 확인했다
[ ] list-dependencies 로 기본 의존성(slice, sysinit)을 확인했다
[ ] After 가 없으면 준비 전에 시작되는 것을 로그 시각으로 확인했다
[ ] After 가 있으면 systemctl start 가 대기하는 것을 확인했다
[ ] --reverse 로 역방향 의존성을 조회할 수 있다
--reverse와 Conflicts 점검으로 악성 unit의 연쇄 실행·중지를 찾는다.다음 글 「27. Target과 부팅 모드」 에서는 의존성의 묶음인 target을 다룬다. multi-user.target과 graphical.target, runlevel과의 대응, 기본 target 변경, 그리고 장애 복구에 쓰는 rescue·emergency 모드의 차이를 확인한다.