서비스 · 프로세스 관리 26 / 50 · Part 3. systemd와 서비스
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정 analyst)

1. 들어가며

재부팅 후 웹 앱이 "DB 연결 실패"로 죽어 있었다. unit 파일에는 분명 Requires=mariadb.service가 적혀 있었는데 왜 이런 일이 생겼을까? 답은 Requires는 "함께 띄워라"일 뿐 "먼저 띄워라"가 아니기 때문 이다. systemd는 가능한 한 모든 것을 병렬로 시작하므로, 순서를 원하면 After=를 따로 써야 한다.

이번 글에서는 의존성 지시어(Wants, Requires)와 순서 지시어(After, Before)를 구분하고, 실제 서비스로 의존 대상이 멈출 때의 동작 과 순서가 없을 때의 경쟁 상태 를 측정한다.


2. 핵심 개념

2-1. 의존성 지시어

지시어의미대상이 실패하거나 멈추면
Wants=가능하면 함께 시작 (약한 의존)나는 계속 동작
Requires=반드시 함께 시작 (강한 의존)나도 중지
BindsTo=Requires보다 강함대상이 사라지기만 해도 나도 중지
Requisite=이미 실행 중이어야 함 (시작해 주지 않음)시작 실패
PartOf=대상의 stop/restart를 따라감 (시작은 아님)—
Conflicts=동시에 실행 불가—

2-2. 순서 지시어

지시어의미
After=XX의 시작이 끝난 뒤에 나를 시작 (중지는 반대 순서)
Before=X내가 끝난 뒤에 X를 시작

순서 지시어는 함께 시작하게 만들지 않는다. After=X만 있고 Wants/Requires가 없으면, X가 다른 이유로 시작될 때만 순서가 적용된다.

2-3. 역방향 지시어

[Install]의 WantedBy=/RequiredBy=는 enable 시 상대편 unit에 Wants/Requires를 추가 하는 역방향 선언이다. 22편의 *.wants/ 디렉터리가 바로 이것이다.


3. 동작 원리

의존성(무엇을 함께)과 순서(누가 먼저)는 별개다

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=가 의미 있게 동작한다.


4. 명령어 실습

# 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

5. 실행 결과

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — Wants 와 Requires 의 차이

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — After 가 없으면 동시에 시작된다

텍스트 원본(실제 출력):

[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

6. 결과 해석

관찰의미
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 startsystemctl start가 DB 준비를 2초 기다린 뒤 앱을 시작했다
Requires=sysinit.target system.slice lab-db.service기본 의존성과 명시 의존성이 합쳐진 최종 값
After=... basic.target ... lab-db.service모든 일반 서비스는 기본적으로 basic.target 이후에 시작된다

7. 보안 관점

주제내용
보안 서비스의 순서방화벽 규칙·auditd·로그 전송기는 네트워크 서비스보다 먼저 시작되어야 부팅 직후 공백이 없다. 예: Before=network-pre.target
의존성으로 인한 연쇄 중지Requires=로 묶인 서비스는 하나가 멈추면 함께 멈춘다. 공격자가 하위 서비스를 멈춰 보안 서비스까지 연쇄 중지 시키지 않는지 의존 관계를 점검한다
악성 unit의 은닉악성 unit은 자신을 WantedBy=로 정상 서비스나 target에 붙여 정상 서비스가 시작될 때 함께 실행 되게 한다. systemctl list-dependencies --reverse로 역방향을 확인한다
Conflicts 악용Conflicts=auditd.service를 가진 unit이 시작되면 auditd가 중지된다. unit 점검 시 Conflicts도 본다

8. 보안관제 관점

[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 UNITUNIT이 필요로 하는 것
systemctl list-dependencies --reverse UNITUNIT을 필요로 하는 것
systemctl show UNIT -p Requires,Wants,After,Conflicts최종 계산된 관계
systemd-analyze critical-chain부팅 순서상 오래 걸린 경로

9. 실무에서 자주 발생하는 실수

실수결과예방
Requires=만 쓰고 After= 누락병렬 시작 → 준비 전 접속 실패의존성과 순서를 함께
모든 의존에 Requires=하나만 멈춰도 줄줄이 중지필수가 아니면 Wants=
After=network.target이면 네트워크 준비 완료로 오해인터페이스 설정 전일 수 있음필요하면 network-online.target (Wants+After)
Type=simple 서비스에 After만 믿음실제 준비 전 다음 서비스 시작준비 완료 알림(notify) 또는 앱 재시도 로직
순환 의존 작성systemd가 일부 job을 제거하며 경고systemd-analyze verify

10. 실습 체크리스트

[ ] Requires 대상이 멈추면 나도 멈추는 것을 확인했다
[ ] Wants 대상이 멈춰도 나는 유지되는 것을 확인했다
[ ] list-dependencies 로 기본 의존성(slice, sysinit)을 확인했다
[ ] After 가 없으면 준비 전에 시작되는 것을 로그 시각으로 확인했다
[ ] After 가 있으면 systemctl start 가 대기하는 것을 확인했다
[ ] --reverse 로 역방향 의존성을 조회할 수 있다

11. 핵심 정리

  • Wants/Requires는 "함께", After/Before는 "순서" 다. 둘은 독립이며 보통 함께 쓴다.
  • Requires 대상이 멈추면 나도 멈추고, Wants 대상은 영향을 주지 않는다.
  • "시작 완료" 시점은 Type에 따라 다르므로, 준비가 긴 서비스는 notify나 oneshot이 정확하다.
  • 모든 서비스에는 slice·sysinit·basic.target 같은 기본 의존성이 자동으로 붙는다.
  • --reverse와 Conflicts 점검으로 악성 unit의 연쇄 실행·중지를 찾는다.

12. 다음 편 예고

다음 글 「27. Target과 부팅 모드」 에서는 의존성의 묶음인 target을 다룬다. multi-user.target과 graphical.target, runlevel과의 대응, 기본 target 변경, 그리고 장애 복구에 쓰는 rescue·emergency 모드의 차이를 확인한다.


참고 자료


시리즈 이동

profile
코드에 숨겨진 위협을 읽고 AI로 보안의 미래를 설계합니다. 프론트엔드 개발 경험을 자산 삼아 더 견고하고 지능적인 보안 운영 시스템을 구축해 나가는 과정을 기록합니다

0개의 댓글