새로 입사한 첫 주, 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단계의 결과입니다.
[전원 인가 / 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 실행 → 포트 bind | enable 안 함 → 부팅에서 통째 누락(수동 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] 세 섹션의 역할을 이해하면 어떤 서비스든 운영 수준으로 등록할 수 있습니다.
확대
서비스를 nohup으로 띄우는 방식은 두 가지 문제가 있습니다. 재부팅 후 자동 기동이 안 되고, 죽었을 때 아무도 다시 살려주지 않습니다. systemd unit 파일은 이 두 가지를 모두 해결합니다.
unit 파일은 /etc/systemd/system/ 디렉터리에 {서비스명}.service 형태로 저장합니다. 세 개의 섹션으로 구성됩니다.
# /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가 기동 완료를 영원히 기다리는 상황이 생깁니다.
# 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을 씁니다.
확대
기동 순서 의존성
After=, Requires=, Wants= 차이
서버 부팅 시 systemd는 여러 서비스를 병렬로 시작합니다. 의존성을 지정하지 않으면 DB가 준비되기 전에 애플리케이션이 먼저 뜨면서 연결 실패로 기동에 실패할 수 있습니다.
확대
[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는 애플리케이션이 재시도 로직을 갖는 것이 더 견고한 설계입니다.
확대
자동 재시작 설정
Restart 정책과 재시작 제한
자동 재시작은 운영 서버의 핵심 안전망입니다. 하지만 무한 재시작 루프를 방지하는 제한도 함께 설정해야 합니다.
[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 파일이 자동 생성됩니다. 직접 설치한 경우 아래를 참고합니다.
# /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.local | systemd unit |
|---|---|---|
| 재부팅 후 자동 기동 | 수동 설정 필요 | enable로 간단히 등록 |
| 장애 시 자동 재시작 | 별도 감시 스크립트 필요 | Restart= 한 줄 |
| 로그 통합 | 파일마다 별도 관리 | journalctl -u 로 통합 |
| 기동 순서 제어 | 스크립트 순서에 의존 | After= / Requires= |
| 상태 확인 | ps aux / 별도 구현 | systemctl status |
이미 shell script로 관리 중인 서비스를 systemd로 전환할 때는 ExecStart에 그 스크립트를 그대로 넣어도 됩니다.
[Service]
Type=oneshot
ExecStart=/opt/scripts/start-myapp.sh
RemainAfterExit=yes
서비스 등록과 활성화
작성한 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 로 반드시 검증
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 순서 준수
기동 후 상태를 확인합니다. 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 으로 기동 로그 확인
기동 실패 시 로그 확인
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 After로 network-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-reload | unit 파일 변경 반영(필수) | 파일 수정 후 daemon-reload → start |
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-failed | StartLimit 초과 카운터 리셋 | reset-failed myapp → start |
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 |
관련 모듈로 더 깊이:
- 프로세스(ps), 포트(netstat), 리소스(top) 모니터링 실무 — 기동 실패 시 포트 충돌·리소스를 진단해 원인을 좁히는 법
- WAR/JAR/정적파일 배포와 배포 스크립트 작성 — systemd 서비스를 배포 구조와 함께 관리하는 법
- Heap/GC/Thread Dump 분석과 OOM 대응 실무 — Java 서비스 기동 시 JVM 옵션과 메모리를 설정하는 법
다음 모듈에서는 이 서비스를 실제로 변경하는 운영 작업계획서 작성 방법을 다룹니다.