infra
Platform

모듈 맵

[Infra Ops] systemd 서비스 등록과 자동 재시작 설정

0 / 52 완료

펼치기
0 / 52 완료0%

인프라 운영 & SRE · 39 / 52

[Infra Ops] systemd 서비스 등록과 자동 재시작 설정

systemd unit 파일 작성, 기동 순서 의존성, auto restart 설정, 서비스 기동 전 점검 루틴까지

🚨INCIDENT ALERT
HIGH

새로 입사한 첫 주, Tomcat 기반 Java 애플리케이션을 서버에 배포하는 작업을 맡았습니다. 개발팀은 JAR 파일을 넘겼고, 실행 명령어도 알려줬습니다. 직접 실행해보니 잘 뜹니다. 그런데 다음날 아침 서버에 재접속해보니 서비스가 죽어있습니다. 누군가 서버를 재부팅했기 때문입니다. 또 nohup java -jar myapp.jar &로 띄웠지만, 서버 재시작 후마다 수동으로 올려야 하는 게 맞는 건지 의심이 듭니다.

운영 서버에서 서비스는 systemd가 관리해야 합니다. 재부팅 후 자동 기동, 장애 시 자동 재시작, 로그 통합 관리까지 — 이 모든 것이 unit 파일 하나로 해결됩니다.

이번 챕터에서 배울 것
  • 1systemd unit 파일의 [Unit], [Service], [Install] 섹션 구조를 설명할 수 있다
  • 2Type=simple/forking/oneshot/notify의 차이를 구분하고 상황에 따라 선택할 수 있다
  • 3After=, Requires=, Wants= 의존성 지시어로 기동 순서를 제어할 수 있다
  • 4Restart=on-failure, RestartSec, StartLimitInterval로 자동 재시작을 설정할 수 있다
  • 5기동 실패 시 systemctl status → journalctl 순서로 원인을 추적할 수 있다

부팅에서 서비스 기동까지 — 전체 흐름

💡개념

서버가 부팅되면 서비스는 어떻게 저절로 뜨나 — 전원 인가부터 헬스 확인까지 6단계

nohup java -jar app.jar &로 띄운 프로세스는 재부팅하면 사라지지만, systemctl enable로 등록한 서비스는 전원만 넣으면 사람이 없어도 알아서 올라옵니다. 그 "알아서"의 실체는 전원 인가 → systemd 기동 → 타깃 도달 → 의존성 순서로 유닛 활성화 → 각 서비스 기동 → 헬스 확인이라는 정해진 순서입니다. 이 흐름을 알면 "재부팅했더니 서비스가 없다", "부팅 때만 기동에 실패한다" 같은 문제를 어느 단계에서 끊겼는지로 좁혀 진단할 수 있습니다. 서비스 자동 기동은 마법이 아니라 아래 6단계의 결과입니다.

TEXT
[전원 인가 / reboot]
   │
   ① 부트로더 → 커널 → systemd(PID 1) 기동      (모든 서비스의 부모 프로세스)
   │
   ② 기본 타깃까지 타깃 트리 전개                (default.target → multi-user.target)
   │
   ③ 의존성 그래프 계산 → 유닛 병렬 활성화        (After=/Requires=/Wants= 순서 반영)
   │
   ④ 각 서비스 유닛 기동                          (ExecStart 실행 → 포트 바인드 → 의존 서비스 대기)
   │    → enable된 유닛만 .wants/ 링크로 자동 포함
   │
   ⑤ Type별 '기동 완료' 판정                      (simple=실행 즉시 / forking=부모 종료 / notify=신호)
   │
   ⑥ 헬스 확인 + 후속 유닛 기동
   ▼
[서비스 active (running) → 포트 LISTEN → 요청 수신]

각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:

단계하는 일여기서 막히면
① systemd 기동커널이 systemd를 PID 1로 실행 → 이후 모든 서비스의 부모가 됨거의 안 막힘. 초기화 실패 시 emergency.target으로 떨어져 부팅 자체가 멈춤
② 타깃 도달default.target(보통 multi-user.target)까지 필요한 타깃·유닛을 끌어옴default.target이 잘못 설정(GUI 없는데 graphical.target)되면 부팅이 지연·정지
③ 의존성 정렬After=/Before=로 순서를, Requires=/Wants=로 동반 기동을 정한 뒤 병렬 실행의존성 순서 오류 → DB보다 앱이 먼저 떠 연결 실패(After= 누락이 전형)
④ 유닛 활성화enable로 걸린 .wants 링크의 유닛만 자동 포함 → ExecStart 실행 → 포트 bindenable 안 함 → 부팅에서 통째 누락(수동 start만 됨) / 포트 선점 → Address already in use로 기동 실패
⑤ 완료 판정Type에 맞춰 메인 프로세스와 기동 완료 시점을 판단Type 불일치 → forking 데몬을 simple로 두면 죽은 걸로 오판해 재시작 반복
⑥ 헬스 확인프로세스는 떴어도 포트 LISTEN·앱 초기화가 끝나야 실제 요청을 받음active인데 헬스 실패 → 포트 바인드 전이거나 앱 초기화 미완(기동은 됐으나 준비 안 됨)

"재부팅 후 서비스가 저절로 떠 있다"는 세 가지가 모두 참이라는 뜻입니다 — 유닛이 enable돼 부팅 목록에 포함됐고(④), 의존성 순서대로 정상 기동됐으며(③⑤), 포트를 잡고 헬스가 통과했다(⑥). 서비스가 부팅 후 없을 때는 이 셋 중 어디서 끊겼는지를 좁히는 것이 진단의 핵심입니다 — systemctl is-enabled로 ④의 등록 여부를, journalctl -u <서비스> -b로 ③④⑤의 부팅 기동 실패를, ss -tlnp로 ⑥의 포트 바인드를 확인합니다. 예를 들어 수동 start는 되는데 부팅 때만 실패한다면 ③의 순서 경합(네트워크·DB 준비 전 기동)을 먼저 의심합니다.

systemd unit 파일 구조

💡개념

unit 파일의 세 가지 섹션

운영 서버에서 nohup java -jar app.jar &로 서비스를 띄우는 것은 두 가지 점에서 불완전합니다. 서버가 재부팅되면 아무도 다시 올려주지 않고, 프로세스가 죽어도 아무도 감지하지 못합니다. systemd unit 파일은 이 두 가지를 한 번에 해결하며, [Unit], [Service], [Install] 세 섹션의 역할을 이해하면 어떤 서비스든 운영 수준으로 등록할 수 있습니다.

systemd unit 파일 구조와 생명주기 — unit 파일은 Unit·Service·[Install](부팅 자동시작 타깃)로 구성. enable로 부팅 시 자동 시작, Restart=on-failure로 죽으면 자동 복구. nohup &와 달리 재부팅 생존·장애 감지·로그 통합(journalctl)을 한 번에 해결확대

서비스를 nohup으로 띄우는 방식은 두 가지 문제가 있습니다. 재부팅 후 자동 기동이 안 되고, 죽었을 때 아무도 다시 살려주지 않습니다. systemd unit 파일은 이 두 가지를 모두 해결합니다.

unit 파일은 /etc/systemd/system/ 디렉터리에 {서비스명}.service 형태로 저장합니다. 세 개의 섹션으로 구성됩니다.

INI
# /etc/systemd/system/myapp.service

[Unit]
Description=My Application
After=network.target

[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/app
ExecStart=/usr/bin/java -jar /opt/app/myapp.jar
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

각 섹션의 역할:

섹션역할주요 지시어
[Unit]서비스 메타데이터와 의존성Description, After, Requires, Wants
[Service]실행 방법, 사용자, 재시작 정책Type, ExecStart, Restart, User
[Install]어느 타겟에서 활성화할지WantedBy

WantedBy=multi-user.target: 일반 서버 서비스의 표준입니다. 네트워크가 올라오고 멀티유저 환경이 준비된 후 서비스가 시작됩니다. GUI가 필요한 서비스는 graphical.target을 쓰지만, 서버 애플리케이션은 대부분 multi-user.target입니다.

💡개념

Type — 프로세스 기동 방식 구분

systemd가 서비스 기동 완료를 어떻게 판단할지를 Type이 결정합니다. 잘못 지정하면 의존 서비스가 너무 일찍 뜨거나, systemd가 기동 완료를 영원히 기다리는 상황이 생깁니다.

INI
# Type=simple (가장 일반적)
# ExecStart로 지정한 프로세스가 메인 프로세스
# 해당 프로세스가 실행되면 즉시 기동 완료로 간주
Type=simple
ExecStart=/usr/bin/java -jar /opt/app/myapp.jar

# Type=forking
# ExecStart 프로세스가 fork 후 부모가 종료하는 구조 (전통적인 데몬 방식)
# Nginx, Apache 같은 오래된 서비스에 사용
Type=forking
PIDFile=/var/run/myapp.pid

# Type=oneshot
# 한 번 실행하고 종료하는 작업 (스크립트, 초기화 작업 등)
# RemainAfterExit=yes를 함께 쓰면 종료 후에도 active 상태 유지
Type=oneshot
RemainAfterExit=yes
ExecStart=/opt/scripts/init-db.sh

# Type=notify
# 서비스가 준비 완료 시 sd_notify()로 systemd에 직접 신호를 보냄
# 기동 시간이 오래 걸리는 서비스에서 정확한 준비 완료 감지
Type=notify
ExecStart=/usr/bin/myapp --systemd

실무에서 선택 기준:

Java/Python/Node.js 애플리케이션은 대부분 Type=simple입니다. 직접 실행되는 단일 프로세스이기 때문입니다. Nginx처럼 fork 구조를 쓰는 서비스는 Type=forking, DB 초기화나 배치 스크립트는 Type=oneshot을 씁니다.

systemd Type 4가지 — 서비스 "기동 완료"를 systemd가 어떻게 판단하나. Type=simple은 ExecStart 프로세스가 곧 메인이라 실행되면 즉시 기동 완료로 간주(Java·Python·Node 단일 프로세스 앱에 가장 일반적), Type=forking은 ExecStart가 fork 후 부모가 종료하는 전통 데몬 방식으로 PIDFile로 메인을 추적(Nginx·Apache 같은 fork 구조), Type=oneshot은 한 번 실행하고 종료하며 RemainAfterExit=yes면 종료 후에도 active 유지(DB 초기화·배치 스크립트), Type=notify는 서비스가 준비 완료 시 sd_notify()로 systemd에 직접 신호를 보내 기동이 오래 걸려도 정확히 감지한다. Type을 잘못 지정하면 사고가 나는데, forking 서비스에 simple을 쓰면 부모가 종료돼 systemd가 죽었다고 오판해 재시작을 반복하고, 준비가 오래 걸리는 서비스를 simple로 두면 아직 준비 안 됐는데 완료로 간주돼 After= 의존 서비스가 너무 일찍 뜬다확대

기동 순서 의존성

💡개념

After=, Requires=, Wants= 차이

서버 부팅 시 systemd는 여러 서비스를 병렬로 시작합니다. 의존성을 지정하지 않으면 DB가 준비되기 전에 애플리케이션이 먼저 뜨면서 연결 실패로 기동에 실패할 수 있습니다.

systemd 의존성 지시어 비교 — myapp.service에 After=network.target(순서만, network 준비 후 시작)·Requires=postgresql.service(강한 의존성, postgres 실패 시 myapp도 함께 중지)·Wants=redis.service(약한 의존성, redis 실패해도 계속 기동)를 걸었을 때의 동작. After는 순서만 보장하고 Requires는 의존성만 보장하므로, "먼저 뜨고 + 죽으면 나도 중지"를 원하면 Requires + After를 함께 쓴다. 외부 서버 DB는 systemd 대신 앱의 연결 재시도 로직이 더 견고하다확대

INI
[Unit]
Description=My Web Application
# 순서(After): network.target이 완료된 후에 시작
After=network.target

# 강한 의존성(Requires): postgresql.service가 없으면 이 서비스도 중지
# postgresql.service가 실패하면 myapp도 함께 실패
Requires=postgresql.service

# 약한 의존성(Wants): redis.service가 있으면 좋지만 없어도 기동
# redis가 실패해도 myapp은 계속 기동 시도
Wants=redis.service

# Requires + After 조합이 가장 일반적
# (After만 쓰면 순서는 보장하지만 의존성은 보장 안 함)
After=network.target postgresql.service
Requires=postgresql.service

세 가지 지시어 비교:

지시어의미종속 서비스 실패 시
After=순서만 보장 (이것보다 나중에)내 서비스는 계속 기동
Requires=강한 의존성내 서비스도 중지
Wants=약한 의존성내 서비스는 계속 기동

Tomcat + PostgreSQL 조합이라면 After=network.target 정도가 현실적입니다. DB가 같은 서버에 있을 때는 After=postgresql.service를 추가합니다. 외부 서버 DB는 애플리케이션이 재시도 로직을 갖는 것이 더 견고한 설계입니다.

systemd 서비스 상태 전환 — 장애 시 확인 흐름확대

자동 재시작 설정

💡개념

Restart 정책과 재시작 제한

자동 재시작은 운영 서버의 핵심 안전망입니다. 하지만 무한 재시작 루프를 방지하는 제한도 함께 설정해야 합니다.

INI
[Service]
ExecStart=/usr/bin/java -jar /opt/app/myapp.jar

# 비정상 종료 시에만 재시작 (권장)
Restart=on-failure

# 재시작 전 대기 시간(초) — 너무 짧으면 로그가 폭주
RestartSec=10

# 재시작 횟수 제한: StartLimitInterval초 안에 StartLimitBurst번 초과 시 중단
StartLimitInterval=60
StartLimitBurst=3
# → 60초 안에 3번 재시작하면 더 이상 시도하지 않음

Restart 옵션 비교:

재시작 조건사용 상황
no재시작 안 함수동 관리 서비스
on-failure비정상 종료 시일반 애플리케이션 (권장)
always종료 원인 무관항상 떠있어야 하는 서비스
on-abnormal시그널/타임아웃 종료 시-

재시작 제한(StartLimitBurst)을 초과해 서비스가 멈춘 경우:

서버 터미널
# 제한 카운터 리셋 후 재시작
sudo systemctl reset-failed myapp
sudo systemctl start myapp

실제 예시 — Tomcat과 Nginx

💡개념

Tomcat systemd unit 파일

패키지 매니저로 설치한 Tomcat은 unit 파일이 자동 생성됩니다. 직접 설치한 경우 아래를 참고합니다.

INI
# /etc/systemd/system/tomcat.service
[Unit]
Description=Apache Tomcat Web Application Container
After=network.target

[Service]
Type=forking
User=tomcat
Group=tomcat

Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk
Environment=CATALINA_HOME=/opt/tomcat
Environment=CATALINA_BASE=/opt/tomcat
Environment=CATALINA_PID=/opt/tomcat/temp/tomcat.pid

ExecStart=/opt/tomcat/bin/startup.sh
ExecStop=/opt/tomcat/bin/shutdown.sh

Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

Tomcat은 startup.sh가 자식 프로세스를 fork하고 종료하는 구조이므로 Type=forking을 씁니다. CATALINA_PID를 지정하면 systemd가 실제 Tomcat 프로세스 PID를 추적할 수 있습니다.

💡개념

shell script 기반 기동 방식과 비교

많은 레거시 환경에서 /etc/rc.local이나 cron @reboot로 서비스를 관리합니다. systemd와 비교하면 차이가 명확합니다.

항목shell script / rc.localsystemd unit
재부팅 후 자동 기동수동 설정 필요enable로 간단히 등록
장애 시 자동 재시작별도 감시 스크립트 필요Restart= 한 줄
로그 통합파일마다 별도 관리journalctl -u 로 통합
기동 순서 제어스크립트 순서에 의존After= / Requires=
상태 확인ps aux / 별도 구현systemctl status

이미 shell script로 관리 중인 서비스를 systemd로 전환할 때는 ExecStart에 그 스크립트를 그대로 넣어도 됩니다.

INI
[Service]
Type=oneshot
ExecStart=/opt/scripts/start-myapp.sh
RemainAfterExit=yes

서비스 등록과 활성화

1unit 파일 작성 및 등록

작성한 unit 파일을 /etc/systemd/system/에 복사합니다. 파일을 직접 이 경로에서 vim으로 작성해도 됩니다. /usr/lib/systemd/system/은 패키지 매니저가 관리하는 경로이므로 커스텀 서비스는 반드시 /etc/systemd/system/에 넣습니다.

로컬 터미널
# unit 파일 직접 작성
sudo vim /etc/systemd/system/myapp.service

# 또는 기존 파일 복사
sudo cp myapp.service /etc/systemd/system/
sudo cp myapp.service /etc/systemd/system/
🔍실행 후 확인할 것
  • ls /etc/systemd/system/myapp.service 로 파일 존재 먼저 확인, 그 다음 systemctl cat myapp 으로 내용 확인 — 파일이 없으면 cat 명령이 "not found" 반환하므로 순서가 중요
  • unit 파일 기준: [Unit], [Service], [Install] 세 섹션이 모두 있어야 정상 — [Install] 섹션이 없으면 systemctl enable 을 해도 재부팅 시 자동 시작 안 됨
  • systemctl cat myapp 이 내용을 보여주는데 ExecStart 경로가 /opt/app/myapp.jar 형태이면 → 실제 파일이 해당 경로에 있는지 ls -la /opt/app/myapp.jar 로 반드시 검증
2daemon-reload → enable → start 순서 실행

unit 파일을 추가하거나 수정한 후에는 반드시 daemon-reload를 먼저 실행합니다. systemd가 파일 변경을 감지해 내부 설정을 갱신합니다. 이 단계를 건너뛰면 이전 내용 그대로 동작합니다.

서버 터미널
# 1. systemd에 unit 파일 변경 알림
sudo systemctl daemon-reload

# 2. 부팅 시 자동 시작 등록
sudo systemctl enable myapp

# 3. 지금 즉시 기동
sudo systemctl start myapp

# enable과 start를 한 번에: --now 플래그
sudo systemctl enable --now myapp
sudo systemctl daemon-reload && sudo systemctl enable myapp && sudo systemctl start myapp
🔍실행 후 확인할 것
  • daemon-reload 후 systemctl is-enabled myapp 확인, 그 다음 systemctl status myapp 으로 실행 상태 확인 — is-enabled 결과가 enabled여야 재부팅 자동 시작 보장
  • is-enabled 기준: enabled=자동 시작 등록됨(정상), disabled=enable 미실행, static=수동 기동만 가능 — disabled 상태에서 서비스가 떠 있어도 재부팅 후 죽음
  • daemon-reload 없이 start 실행하면 → 수정된 unit 파일이 반영 안 되어 이전 설정으로 기동 — 파일 수정 후에는 반드시 daemon-reload → start 순서 준수
3서비스 상태 확인

기동 후 상태를 확인합니다. active (running) 이 정상입니다. 출력 하단에 최근 로그 10줄이 함께 표시됩니다.

서버 터미널
# 서비스 상태 확인
sudo systemctl status myapp

# 출력 예시:
# ● myapp.service - My Application
#    Loaded: loaded (/etc/systemd/system/myapp.service; enabled; ...)
#    Active: active (running) since Fri 2026-05-30 10:00:00 KST; 5s ago
#  Main PID: 12345 (java)
#    CGroup: /system.slice/myapp.service
#            └─12345 /usr/bin/java -jar /opt/app/myapp.jar
sudo systemctl status myapp
🔍실행 후 확인할 것
  • systemctl status myapp 에서 먼저 Active 상태를 보고, 그 다음 Loaded 줄의 enabled 여부 확인 — active (running)이어도 disabled이면 재부팅 후 서비스가 사라짐
  • Active 상태 기준: active (running)=정상, activating=기동 진행 중(30초 이상이면 문제), failed=시작 실패 — Main PID가 없으면 ExecStart 경로 또는 권한 문제
  • status가 active (running)인데 curl 헬스체크에 실패하면 → 서비스는 떴지만 포트 바인딩 또는 애플리케이션 초기화 미완료 — journalctl -u myapp -n 20 으로 기동 로그 확인

기동 실패 시 로그 확인

4기동 실패 원인 추적

systemctl status로 짧은 에러를 먼저 확인하고, 충분하지 않으면 journalctl로 상세 로그를 봅니다.

서버 터미널
# 1단계: 현재 상태와 최근 로그 확인
sudo systemctl status myapp

# 2단계: 더 많은 로그 확인 (최근 50줄)
sudo journalctl -u myapp -n 50 --no-pager

# 3단계: 실시간 팔로우 (기동 시도하면서 로그 확인)
sudo journalctl -u myapp -f

# 특정 시간대 로그
sudo journalctl -u myapp --since "2026-05-30 09:00:00" --no-pager
sudo systemctl status myapp
🔍실행 후 확인할 것
  • systemctl status myapp 하단 에러 메시지를 먼저 보고, 짧은 메시지로 원인 불명이면 journalctl -u myapp -n 50 으로 상세 로그 확인 — status가 1단계, journalctl이 2단계
  • exit code 기준: code=exited status=1 이면 애플리케이션 오류(코드/설정 문제), code=killed signal=SIGTERM 이면 타임아웃 또는 외부 종료, code=exited status=2 이면 ExecStart 경로 오류
  • journalctl에 "Permission denied" 가 보이면 → ExecStart 경로 파일의 소유자/권한 확인, "Address already in use" 가 보이면 → ss -tlnp | grep <포트> 로 충돌 프로세스 확인

트러블슈팅

원인: unit 파일의 ExecStart 경로가 잘못됐거나, 지정한 User로는 해당 파일을 실행할 권한이 없습니다. systemd는 ExecStart에 절대 경로를 요구합니다.

서버 터미널
# 에러 확인
sudo systemctl status myapp
# 예시: myapp.service: ExecStart=/opt/app/myapp.jar failed (errno=ENOENT)

sudo journalctl -u myapp -n 20 --no-pager
# 예시: Failed to execute command: No such file or directory

# ExecStart 경로 존재 여부 확인
ls -la /opt/app/myapp.jar
# → 없으면 실제 경로 확인 후 unit 파일 수정

# 실행 파일 권한 확인
ls -la /usr/bin/java
# User=appuser로 실행 가능한지 확인
sudo -u appuser /usr/bin/java -version

# 수정 후 적용 순서
sudo systemctl daemon-reload
sudo systemctl start myapp

ExecStart에 쉘 문법(~, 환경변수 직접 사용)은 동작하지 않습니다. 절대 경로를 써야 합니다. 환경변수가 필요하면 Environment= 또는 EnvironmentFile=을 unit 파일에 명시합니다.

원인: systemctl start는 지금 즉시 서비스를 시작하지만, 부팅 시 자동 시작은 등록하지 않습니다. enable 없이 start만 한 경우 재부팅하면 서비스가 꺼진 상태로 시작됩니다.

서버 터미널
# enable 상태 확인
sudo systemctl is-enabled myapp
# 출력: disabled  ← 이것이 원인

# 활성화 (다음 재부팅부터 자동 기동)
sudo systemctl enable myapp

# 확인
sudo systemctl is-enabled myapp
# 출력: enabled

# 현재도 즉시 기동 (이미 꺼진 경우)
sudo systemctl start myapp

# 한 번에 처리
sudo systemctl enable --now myapp

enable은 실제로 심볼릭 링크를 생성하는 작업입니다. /etc/systemd/system/multi-user.target.wants/myapp.service -> /etc/systemd/system/myapp.service 링크가 생성됩니다. disable은 이 링크를 제거합니다.

심화 — '순서'와 '준비됨'은 다르다

💡개념

심화: systemd가 보장하는 것과 못 하는 것 — 부팅 순서·Type·재시작의 경계

"After=로 순서를 걸었으니 의존 서비스는 준비돼 있겠지"라는 가정이 부팅 때만 나는 장애의 근원입니다. systemd의 순서·Type·재시작이 각각 무엇을 보장하고 무엇은 보장하지 못하는지를 나눠 보면 함정이 드러납니다.

  • network.target vs network-online.target: network.target은 '네트워킹 스택을 시작했다'는 동기화 지점일 뿐, 인터페이스에 IP가 붙고 라우팅이 완성됐음을 뜻하지 않습니다. 부팅 시 특정 IP 바인딩이나 외부 호스트 연결이 필요한 서비스는 Wants=network-online.target + After=network-online.target을 걸고, systemd-networkd-wait-online(또는 NetworkManager-wait-online)이 켜져 있어야 '진짜 온라인'을 기다립니다. 다만 대부분의 앱은 애초에 연결 재시도 로직을 갖는 편이 더 견고합니다.
  • 순서(After)는 '준비됨'을 보장하지 않는다: After=postgresql.service는 postgres 유닛이 '기동을 시작한' 뒤에 앱을 띄울 뿐입니다. Type=simple인 DB는 프로세스가 뜨자마자 준비됐다고 간주되지만, 실제로는 아직 소켓을 리스닝하기 전일 수 있습니다. 그래서 순서를 걸어도 앱의 커넥션 재시도가 필요합니다(Type=notify를 쓰는 서비스만 '진짜 준비 완료'를 systemd에 알립니다).
  • Restart 폭주와 StartLimit의 균형: Restart=on-failure로 크래시를 복구하되, RestartSec이 너무 짧으면 StartLimitBurst 한도에 금방 걸려 systemd가 'start request repeated too quickly'로 아예 포기하고 서비스를 죽입니다. 반대로 너무 길면 복구가 느립니다. 재시작은 증상 완화일 뿐, 근본 원인을 못 고치면 폭주만 반복합니다.
  • 한계: systemd의 순서·재시작은 '프로세스 수준' 안전망입니다. '의존 서비스가 실제로 요청을 받을 준비(readiness)가 됐는지'까지는 보장하지 못하며, 그건 앱의 재시도·헬스체크의 몫입니다.

상황: myapp이 서버 재부팅 직후에는 failed 상태입니다. 그런데 사람이 접속해 systemctl start myapp을 치면 곧바로 정상 기동됩니다. 재현이 부팅 시점에만 일어나 원인을 잡기 까다롭습니다.

원인: unit에 After=network.target만 걸려 있었습니다. network.target은 네트워킹 설정을 시작한 지점일 뿐, 인터페이스에 IP가 붙고 라우팅이 완성된 상태를 보장하지 않습니다. 앱이 부팅 시 특정 IP 바인딩(또는 외부 DB 호스트명 DNS 조회)을 하는데 아직 주소가 없어 실패했고, 짧은 RestartSec 탓에 StartLimitBurst까지 소진돼 systemd가 포기했습니다. 수동 start 때는 이미 네트워크가 완비돼 있어 성공한 것입니다.

진단: journalctl -u myapp -b로 부팅 로그를 보면 Cannot assign requested address·Name or service not known·Network is unreachable 같은 에러가 네트워크 완비 전에 찍혀 있습니다. systemctl show myapp -p Afternetwork-online.target 누락을 확인하고, systemd-analyze critical-chain myapp.service로 앱이 네트워크보다 이른 시점에 떴는지 대조합니다.

해결: 외부 네트워크가 필요한 서비스는 Wants=network-online.target + After=network-online.target을 걸고, systemctl enable systemd-networkd-wait-online(또는 NetworkManager-wait-online)으로 그 타깃이 실제로 대기하게 합니다. 더 견고한 근본 해법은 앱에 연결 재시도·백오프를 넣어 '부팅 시점 경합'에 아예 의존하지 않는 것이고, RestartSec을 늘려 부팅 초기 폭주로 StartLimit에 걸리는 것도 함께 완화합니다.

💼
실무 맥락
현업 패턴

실제 업무에서 이 지식이 쓰이는 상황:

주니어 엔지니어가 처음 서비스를 서버에 올릴 때 가장 자주 하는 실수가 두 가지입니다. nohup java -jar app.jar &로 띄우고 끝냈거나, systemctl start만 하고 enable을 빠뜨린 경우입니다. 둘 다 재부팅 후 서비스가 꺼져 있는 결과로 이어집니다.

신규 서비스 등록 표준 절차:

로컬 터미널
# 1. unit 파일 작성
sudo vim /etc/systemd/system/myapp.service

# 2. systemd에 알림
sudo systemctl daemon-reload

# 3. 활성화 + 즉시 기동
sudo systemctl enable --now myapp

# 4. 상태 확인
sudo systemctl status myapp

# 5. 로그 확인 (기동 직후 에러 없는지)
sudo journalctl -u myapp -n 20 --no-pager

기동 전 10분 점검 루틴:

서비스를 처음 등록하거나 설정을 변경한 뒤에는 이 순서로 확인합니다.

로컬 터미널
# ExecStart 경로 존재 확인
ls -la /opt/app/myapp.jar

# 실행 유저 확인 (unit 파일의 User=와 일치해야 함)
id appuser

# 포트 충돌 확인 (서비스가 사용할 포트)
sudo ss -tlnp | grep :8080

# 의존 서비스 상태 확인 (DB 등)
sudo systemctl status postgresql

현업에서는 배포 스크립트에 이 절차가 포함되는 경우가 많습니다. unit 파일을 배포 아티팩트와 함께 관리하고, 배포 시 daemon-reload → enable → restart → status 확인 순서를 자동화합니다.

명령어·단축키 빠른 참조

이 모듈에서 다룬 systemd 서비스 등록·진단 명령을 표준 순서대로 모았습니다. 신규 등록은 daemon-reload → enable → start, 실패 진단은 status → journalctl 순서입니다.

명령어/단축키용도자주 쓰는 예
systemctl daemon-reloadunit 파일 변경 반영(필수)파일 수정 후 daemon-reloadstart
systemctl enable --now부팅 자동시작 등록 + 즉시 기동systemctl enable --now myapp
systemctl start/stop/restart서비스 기동·중지·재시작systemctl restart myapp
systemctl status상태·최근 로그 확인systemctl status myapp --no-pager | head -20
systemctl is-enabled부팅 자동시작 여부 확인결과가 enabled여야 재부팅 후 생존
systemctl reset-failedStartLimit 초과 카운터 리셋reset-failed myappstart
systemctl show -p After의존·순서 설정 점검systemctl show myapp -p After (network-online 누락 확인)
systemctl cat등록된 unit 내용 확인systemctl cat myapp
journalctl -u서비스 상세 로그journalctl -u myapp -n 50 --no-pager
journalctl -u -b이번 부팅 로그만journalctl -u myapp -b (부팅 시점 경합 진단)
systemd-analyze critical-chain기동 순서·시점 분석systemd-analyze critical-chain myapp.service
ss -tlnp포트 충돌 확인ss -tlnp | grep :8080

관련 모듈로 더 깊이:

다음 모듈에서는 이 서비스를 실제로 변경하는 운영 작업계획서 작성 방법을 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

Java 앱 서비스의 unit 파일에 Restart=always를 설정했더니, systemctl stop myapp으로 서비스를 멈춰도 곧바로 다시 올라옵니다. 배포 작업 중 서비스를 안정적으로 중지할 수 있으면서 크래시 시 자동 복구도 되게 하려면 어떻게 설정해야 하는가?

Q2

신규 서버에 Tomcat을 설치하고 systemctl start tomcat으로 잘 뜨는 것을 확인했습니다. 다음날 서버 재부팅 후 확인하니 Tomcat이 내려가 있습니다. 서버 재부팅 시 자동으로 올라오게 하려면 어떤 명령어가 필요한가?

Q3

After=network.target 설정이 필요한 이유는?

Q4

서비스 기동 실패 시 원인을 확인하는 올바른 절차는?

Q5

백그라운드로 fork 하며 부모 프로세스가 종료되는 데몬을 systemd unit의 Type=simple로 등록했더니, systemctl이 '실행 중'을 잘못 판단하거나 곧 죽은 것으로 본다. 올바른 설정은?

Q6

systemd unit의 의존 지시자 After=, Requires=, Wants=는 무엇이 다른가?

Q7

[심화] systemd unit에 After=network.target만 걸어 뒀는데, 서버 부팅 직후에만 외부 DB 연결이나 특정 IP 바인딩이 간헐적으로 실패합니다. 근본 원인으로 가장 정확한 것은?

Q8

[심화] 부팅 직후엔 myapp이 failed인데, 로그인해서 수동으로 systemctl start myapp 하면 정상 기동됩니다. 원인을 확정하는 진단으로 가장 적절한 것은?

0 / 8 답변

🧪 실습으로 확인하기

systemd 서비스 부팅 실패 — 자동 기동·의존성·순서 진단

중급

재부팅 후 서비스가 자동으로 안 뜬다. 수동 start는 되는데. 원인은 (1) enable 누락(자동 시작 미등록), (2) 의존성/순서 문제(DB·네트워크·마운트가 준비되기 전에 기동), (3) 환경/권한 차이다. enable·journalctl·의존성으로 진단하고, After/Requires와 재시작 정책으로 부팅 후 안정 기동을 보장한다.

40📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요