systemd Type= 정리|simple·exec·notify와 서비스 파일 핵심 키워드
Answer First: systemd Type= 지시어는 서비스가 기동(startup)을 언제 완료했는지 매니저에게 알리는 방식입니다. 현재 공식 man 페이지 기준 설정값은 simple, exec, forking, oneshot, dbus, notify, notify-reload, idle입니다. 이 글은 freedesktop 공식 man 페이지(systemd.service(5))를 기준으로 각 타입의 선택 기준과 반드시 알아야 할 핵심 키워드를 정리합니다.
이 글은 systemd 공식 문서(systemd.service(5) 등)를 기준으로 한 일반 안내이며, 배포판에 패키지된 systemd 버전에 따라 옵션·기본값·추가 Type 동작은 달라질 수 있습니다. 작성·적용 전 해당 시스템의 man systemd.service를 확인하시기 바랍니다.
systemd [Service] Type=은 무엇을 의미하나?
한 줄 답: Type=은 .service 파일의 [Service] 섹션에서 systemd 매니저가 해당 서비스의 기동 완료 시점을 어떻게 판단할지를 지정하는 지시어입니다.
systemd가 서비스를 시작하면, 그 서비스가 실제로 “준비 완료” 상태인지 어떻게 알 수 있습니까? 프로세스를 단순히 fork한 직후인지, 바이너리 실행에 성공한 순간인지, 아니면 서비스 자체가 준비 신호를 보낼 때까지 기다려야 하는지 — 이 기준을 결정하는 것이 바로 Type=입니다.
이 지시어는 단순히 데몬 유형을 분류하는 레이블이 아닙니다. Type=의 선택은 다음 세 가지에 직접적인 영향을 줍니다.
- 후속 유닛의 기동 시점:
After=my.service로 선언된 유닛들은my.service의 기동 완료 판정이 내려진 후에야 시작됩니다.Type=이 잘못되면 아직 초기화가 끝나지 않은 상태에서 후속 유닛이 실행될 수 있습니다. systemctl start의 반환 시점:Type=simple은fork()직후 성공을 반환하기 때문에, 바이너리 경로가 잘못됐거나User=에 지정한 사용자가 없더라도 명령이 성공한 것처럼 보일 수 있습니다.- 실패 감지 시점:
Type=exec는execve()가 실제로 성공한 후에야 기동 완료로 판정하므로, 바이너리 누락이나 잘못된User=설정 같은 오류를systemctl start단계에서 바로 잡아낼 수 있습니다.
따라서 Type=을 기본값(simple)으로 방치하면 실제 오류가 숨겨질 수 있습니다. 서비스 파일을 작성할 때 가장 먼저 결정해야 할 항목이 바로 이 지시어입니다.
Type= 종류는 무엇이 있고 언제 고르나?
한 줄 답: 현재 공식 systemd.service(5) 기준으로 Type=에는 8가지 값이 있으며, 각각 기동 완료 판정 기준이 다릅니다.
아래는 각 타입의 동작 방식과 권장 사용 시나리오를 정리한 것입니다.
simple
ExecStart=로 지정한 프로세스를 fork()한 직후 기동 완료로 판정합니다. ExecStart=가 설정되어 있고 Type=과 BusName= 중 어느 것도 명시되지 않은 경우의 기본값입니다(단, 크리덴셜을 사용하지 않을 때). execve()가 성공했는지는 확인하지 않으므로, 바이너리 경로 오류나 User= 설정 오류가 systemctl start 시점에 드러나지 않을 수 있습니다. 장기 실행 서비스에는 exec를 권장합니다.
exec
main 프로세스가 execve()에 성공한 후 기동 완료로 판정합니다. fork()와 exec 모두 성공해야 완료로 인정됩니다. 크리덴셜(LoadCredential= 등)을 사용하는 경우 자동으로 이 타입이 적용됩니다. 바이너리 누락이나 잘못된 User= 설정을 기동 단계에서 바로 잡아낼 수 있어, 장기 실행 서비스에 공식적으로 권장되는 방식입니다.
forking
매니저가 시작한 부모 프로세스가 전통적인 double-fork 이후 종료되고, 자식 프로세스가 계속 실행되는 방식입니다. 매니저는 부모 프로세스가 종료된 시점을 기동 완료로 판정합니다. 이 타입은 전통적인 유닉스 데몬 방식에 해당하지만, 현대적 프로젝트에서는 사용을 지양하고 notify, notify-reload, 또는 dbus로 대체할 것을 공식 man 페이지가 권장합니다. 사용 시 PIDFile=을 함께 지정해야 매니저가 메인 프로세스를 정확히 추적할 수 있습니다.
oneshot
메인 프로세스가 실행을 완전히 마치고 종료된 시점을 기동 완료로 판정합니다. ExecStart=도 Type=도 설정되지 않은 경우의 기본값이기도 합니다. exec와 유사하지만, 프로세스 종료 후 유닛 상태가 “active”로 남습니다. 하나의 유닛 파일에 여러 개의 ExecStart=를 나열할 수 있는 유일한 타입입니다. 장기 실행 데몬에는 사용하지 않습니다. RemainAfterExit=yes와 조합하면, 종료 후에도 유닛 상태를 “active”로 유지할 수 있어 후속 유닛의 의존 관계에 활용됩니다.
notify
서비스가 sd_notify(3)를 통해 READY=1 신호를 보냈을 때 기동 완료로 판정합니다. 그 외 동작 방식은 exec와 유사합니다. IPC 소켓이나 데이터베이스 연결 등 초기화가 완료된 후에야 클라이언트를 받을 수 있는 데몬에 적합합니다. NotifyAccess= 지시어가 알림 소켓 접근을 허용하도록 설정되어야 하며, 설정되지 않은 경우 main 프로세스로 강제됩니다.
notify-reload
notify와 유사하지만, 리로드 시 추가 프로토콜을 지원합니다. 매니저가 리로드를 요청하면 SIGHUP(또는 ReloadSignal=에 지정한 시그널)을 보내고, 서비스는 RELOADING=1과 MONOTONIC_USEC=…을 알린 뒤 리로드 완료 시 다시 READY=1을 전송합니다. 시그널을 통해 리로드하는 데몬에 권장되며, 비동기 방식의 ExecReload=kill -HUP를 대체하는 현대적 방식입니다.
dbus
BusName=에 지정한 이름을 D-Bus에서 획득했을 때 기동 완료로 판정합니다. 그 외 동작 방식은 simple과 유사합니다. BusName=이 설정된 경우의 기본 타입이기도 합니다. dbus.socket에 대한 의존성이 암묵적으로 추가되며, BusName= 지정은 필수입니다.
idle
simple과 유사하지만, 다른 모든 작업이 디스패치될 때까지 실행을 지연합니다. 콘솔 출력이 다른 서비스 메시지와 뒤섞이는 것을 방지하는 용도로 설계되었으며, 최대 5초 후에는 지연 없이 실행됩니다. 일반적인 유닛 순서 제어 도구로 사용해서는 안 됩니다. 순서 제어에는 After=/Before=를 사용해야 합니다.
타입 선택 요약
| Type | 기동 완료 판정 기준 | 권장 상황 |
|---|---|---|
simple | fork() 직후 | 레거시 또는 socket activation + simple 조합 |
exec | execve() 성공 후 | 장기 실행 서비스 — 권장 |
forking | 부모 프로세스 종료 후 | 전통적 Unix 데몬; 가능하면 기피 |
oneshot | 메인 프로세스 종료 후 | 일회성 초기화 스크립트 |
notify | READY=1 수신 후 | IPC 준비 후 알림이 필요한 데몬 |
notify-reload | READY=1 수신 후 (리로드 프로토콜 포함) | 시그널 리로드를 지원하는 데몬 |
dbus | D-Bus BusName= 획득 후 | D-Bus 서비스 |
idle | 다른 작업 디스패치 후 (최대 5초) | 콘솔 출력 겹침 방지 (순서 제어 아님) |
ExecStart·ExecStop·Restart=는 어떻게 쓰나?
한 줄 답: ExecStart=는 서비스를 실행할 명령을, ExecStop=은 중지 명령을, Restart=는 비정상 종료 시 재시작 정책을 지정합니다.
ExecStart=
서비스 기동 시 실행할 명령(및 인자)을 지정합니다. 절대 경로를 사용해야 합니다. Type=oneshot을 제외한 모든 타입에서는 단 하나의 ExecStart=만 허용됩니다. Type=oneshot에서는 여러 개를 나열할 수 있으며, 순서대로 실행됩니다. 기동 전후에 추가 명령이 필요하다면 ExecStartPre=와 ExecStartPost=를 활용합니다.
ExecStop=
서비스 중지 시 실행할 명령을 지정합니다. 지정하지 않으면 systemd는 KillMode=와 KillSignal=에 따라 프로세스에 시그널을 보내 중지합니다. 중지 후 정리 작업이 필요한 경우 ExecStopPost=를 사용합니다.
ExecReload=
systemctl reload 시 실행할 명령을 지정합니다. Type=notify-reload를 사용하는 경우, 리로드 프로토콜이 이 지시어를 대체하는 역할을 합니다.
Restart=
서비스가 종료·실패했을 때 자동 재시작 여부와 조건을 설정합니다. 기본값은 no입니다. 장기 실행 서비스에서 가장 흔히 사용하는 값과 그 의미는 다음과 같습니다.
no: 재시작하지 않습니다(기본값).on-failure: 종료 코드가 0이 아니거나 시그널에 의해 비정상 종료된 경우에만 재시작합니다. 장기 실행 서비스에 일반적으로 권장됩니다.always: 종료 이유에 관계없이 항상 재시작합니다.Type=oneshot과는 함께 사용할 수 없습니다.on-abnormal: 시그널·타임아웃·watchdog 등 비정상 종료 시에만 재시작합니다.
재시작 전 대기 시간은 RestartSec=으로 조절하며 기본값은 100ms입니다.
WantedBy=·After=·User=·Environment= 등 필수 키워드는?
한 줄 답: 서비스 파일의 핵심 키워드는 크게 세 그룹으로 나뉩니다 — 실행 환경 설정([Service]/systemd.exec), 의존성 및 순서 제어([Unit]), 서비스 활성화([Install]).
실행 환경 설정
다음 지시어들은 주로 [Service] 섹션 또는 systemd.exec(5) 스펙에서 정의되며, 프로세스가 실행될 환경을 구성합니다.
User=/Group=: 서비스를 실행할 사용자와 그룹을 지정합니다. 지정하지 않으면root로 실행됩니다. 보안을 위해 최소 권한 사용자를 별도로 생성하여 사용하는 것이 좋습니다.WorkingDirectory=: 서비스 프로세스의 작업 디렉터리를 지정합니다. 기본값은root의 홈 디렉터리입니다.Environment=: 서비스에 전달할 환경 변수를KEY=VALUE형식으로 지정합니다. 한 줄에 여러 변수를 공백으로 구분하여 나열할 수 있습니다.EnvironmentFile=: 환경 변수를 파일에서 읽어옵니다. 시크릿이나 배포판별 경로 등을 유닛 파일 외부에서 관리할 때 유용합니다.
의존성 및 실행 순서
다음 지시어들은 [Unit] 섹션에 작성하며, systemd.unit(5) 스펙에서 정의됩니다. 순서 제어와 의존성 선언은 개념이 다릅니다.
After=/Before=: 유닛의 시작 순서만 제어합니다. 의존성을 강제하지 않습니다.After=network.target은 “network.target이 먼저 활성화된 후 이 유닛을 시작한다”는 의미이며,network.target이 실패해도 이 유닛의 시작을 막지 않습니다.Requires=: 강한 의존성입니다. 나열된 유닛이 활성화에 실패하거나 중지되면 이 유닛도 함께 중지됩니다.Wants=: 약한 의존성입니다. 나열된 유닛이 있으면 함께 활성화를 시도하지만, 해당 유닛이 실패해도 이 유닛의 실행을 막지 않습니다. 일반적으로Requires=보다Wants=사용을 권장합니다.
서비스 활성화 — [Install] 섹션
WantedBy=:systemctl enable실행 시 어느 타겟의.wants/디렉터리에 심볼릭 링크를 생성할지 지정합니다. 일반 서비스는 대부분WantedBy=multi-user.target을 사용합니다. 이 심볼릭 링크가 있어야 부팅 시 자동으로 서비스가 시작됩니다.
주의사항: daemon-reload
유닛 파일을 생성하거나 수정한 후에는 반드시 systemctl daemon-reload를 실행해야 합니다. systemd는 런타임에 유닛 파일을 자동으로 재읽지 않습니다. daemon-reload를 실행하지 않으면 이전 설정이 그대로 유지됩니다.
# 유닛 파일 수정 후 반드시 실행
systemctl daemon-reload
systemctl enable --now my-service.service
forking의 PIDFile=과 oneshot의 RemainAfterExit=는 왜 중요한가?
한 줄 답: PIDFile=은 Type=forking에서 systemd가 메인 프로세스를 추적하기 위해 필요하고, RemainAfterExit=는 Type=oneshot 서비스가 종료 후에도 “active” 상태를 유지하도록 합니다.
PIDFile=과 Type=forking
전통적인 데몬은 double-fork 기법으로 자신을 백그라운드에 detach합니다. 이 경우 systemd가 처음 fork한 부모 프로세스는 종료되고, 그 자식 프로세스(실제 데몬)가 이어서 실행됩니다. 문제는 systemd가 어느 PID가 실제 메인 프로세스인지 알 방법이 없다는 점입니다.
PIDFile=은 데몬이 기록하는 PID 파일의 경로를 systemd에게 알려주어, 매니저가 올바른 프로세스를 추적하고 모니터링할 수 있도록 합니다. 경로는 절대 경로로 지정하며, 일반적으로 /run/ 하위 디렉터리를 사용합니다.
그러나 공식 man 페이지는 현대적 프로젝트에서는 Type=forking과 PIDFile= 조합을 지양하고, notify나 notify-reload로 대체할 것을 권장합니다. PID 파일 방식은 레이스 컨디션이 발생할 수 있고, 알림 소켓 기반 방식에 비해 신뢰성이 낮습니다.
[Service]
Type=forking
PIDFile=/run/my-legacy-daemon.pid
ExecStart=/usr/sbin/my-legacy-daemon --daemonize
RemainAfterExit=와 Type=oneshot
Type=oneshot 서비스는 정의상 실행 후 바로 종료됩니다. 이 경우 RemainAfterExit=를 지정하지 않으면, 서비스 상태는 “activating → dead”로 전환됩니다. 즉, 스크립트가 성공적으로 완료된 후에도 상태가 “inactive(dead)“로 보입니다.
RemainAfterExit=yes를 설정하면, 모든 ExecStart= 명령이 종료된 후에도 유닛 상태가 “active(exited)“로 유지됩니다. 이 설정은 다음 두 가지 경우에 특히 유용합니다.
- 후속 유닛의
After=의존성: 초기화 스크립트가 완료되었음을 나타내는 가상의 “완료 표지”로 활용합니다.After=setup.service로 선언된 후속 서비스는 이 상태를 보고 안전하게 실행될 수 있습니다. systemctl is-active조회: 스크립트 완료 후에도 활성 상태임을 확인할 수 있습니다.
[Unit]
Description=One-time system initialization
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/setup-step1.sh
ExecStart=/usr/bin/setup-step2.sh
[Install]
WantedBy=multi-user.target
위 예시에서 setup-step1.sh와 setup-step2.sh가 순서대로 모두 완료되면 유닛은 “active(exited)” 상태가 되며, 이 유닛에 After=로 의존하는 다른 서비스들이 안전하게 이어서 실행됩니다.
FAQ
한 줄 답: 자주 묻는 질문과 공식 man 기준의 간결한 답변을 정리합니다.
| 질문 | 답변 |
|---|---|
daemon-reload는 언제 필요합니까? | 유닛 파일(.service, .target, .socket 등)을 생성하거나 수정한 직후에 필요합니다. systemd는 디스크의 유닛 파일을 자동으로 감시하지 않으며, daemon-reload를 실행해야 변경된 내용을 메모리에 반영합니다. 이미 실행 중인 서비스 프로세스 자체에는 영향을 주지 않습니다. |
Type=simple에서 바이너리가 누락되어도 systemctl start가 성공처럼 보이는 이유는 무엇입니까? | Type=simple은 fork()가 성공한 시점을 기동 완료로 판정하기 때문입니다. 바이너리 실행(execve())이 실패하더라도 그 오류는 자식 프로세스 내부에서 발생하며, 매니저는 이미 성공으로 판정한 후입니다. 이 문제를 방지하려면 Type=exec를 사용해야 합니다. exec는 execve() 성공 후에야 완료로 판정합니다. |
socket activation과 Type=의 관계는 무엇입니까? | socket activation은 소켓 유닛(.socket)이 연결 요청을 받을 때까지 서비스 실행을 지연하는 방식입니다. 이 경우 서비스의 Type=은 여전히 서비스 자체의 기동 완료 판정에 사용됩니다. socket activation을 사용하면 Type=simple이나 notify와의 조합이 일반적이며, forking은 피하는 것이 권장됩니다. |
Type=idle을 일반적인 유닛 순서 제어에 사용해도 됩니까? | 사용해서는 안 됩니다. idle은 콘솔 출력이 다른 서비스 메시지와 뒤섞이는 것을 방지하는 좁은 용도로 설계되었습니다. 최대 5초만 지연되며 그 이후에는 다른 작업 완료를 기다리지 않고 실행됩니다. 유닛 간 순서 제어가 필요하다면 After=/Before=와 Requires=/Wants=를 사용해야 합니다. |
출처
한 줄 답: 본문 사실은 2026-09-14 기준으로 확인한 systemd 공식 문서를 기반으로 합니다.
- systemd.service(5) — freedesktop.org —
Type=전 항목,ExecStart=,Restart=,PIDFile=,RemainAfterExit=,NotifyAccess=사실 확인에 사용했습니다. - systemd.unit(5) — freedesktop.org —
After=,Before=,Requires=,Wants=,WantedBy=사실 확인에 사용했습니다. - systemd.exec(5) — freedesktop.org —
User=,Group=,WorkingDirectory=,Environment=,EnvironmentFile=사실 확인에 사용했습니다. - sd_notify(3) — freedesktop.org —
READY=1,RELOADING=1신호 프로토콜 사실 확인에 사용했습니다. - OPTIMIZATIONS — systemd.io —
forking기피 권장 및notify/socket activation 선호 권장 사실 확인에 사용했습니다. - FAQ — systemd.io —
daemon-reload필요 시점 관련 사실 확인에 사용했습니다.
면책 조항(Disclaimer): 이 글은 systemd 공식 문서(systemd.service(5) 등)를 기준으로 한 일반 안내이며, 배포판에 패키지된 systemd 버전에 따라 옵션·기본값·추가 Type 동작은 달라질 수 있습니다. 작성·적용 전 해당 시스템의 man systemd.service를 확인하시기 바랍니다.