[Swift 입문] 34편 — LaunchAgent와 백그라운드 데몬

🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [33편] PTY와 가상 터미널

launchd — macOS의 프로세스 관리자

macOS는 cron이나 init.d 같은 전통적인 유닉스 도구 대신 launchd 하나로 부팅 프로세스부터 사용자 로그인, 백그라운드 서비스, 주기적 작업까지 전부 관리합니다. launchd에게 “이 프로그램을 이런 조건에서 실행해줘”라고 알려주는 방법이 plist(property list) 파일입니다.

이 plist를 어디에 두느냐에 따라 역할이 갈립니다.

  • LaunchAgent: ~/Library/LaunchAgents/ — 특정 사용자가 로그인했을 때 그 사용자 권한으로 실행. GUI 접근 가능
  • LaunchDaemon: /Library/LaunchDaemons/ — 로그인 여부와 무관하게 시스템 부팅 시 root 권한으로 실행. GUI 접근 불가

메뉴바 앱이나 백그라운드 브리지처럼 “사용자가 로그인해 있는 동안 계속 떠 있어야 하는” 도구는 대부분 LaunchAgent를 씁니다.


plist 기본 구조

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.mybridge</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Users/me/bin/mybridge</string>
        <string>--daemon</string>
    </array>

    <key>RunAtLoad</key>
    <true/>

    <key>KeepAlive</key>
    <true/>

    <key>StandardOutPath</key>
    <string>/tmp/mybridge.log</string>

    <key>StandardErrorPath</key>
    <string>/tmp/mybridge.error.log</string>
</dict>
</plist>

핵심 키들을 짚어보면:

  • Label: 이 작업의 고유 식별자. 역도메인 표기(com.example.xxx)가 관례이며, launchctl 명령에서 이 이름으로 작업을 지칭합니다.
  • ProgramArguments: 실행할 프로그램과 인자. 배열의 첫 항목이 실행 파일 경로입니다.
  • RunAtLoad: launchd가 이 plist를 로드하는 즉시(보통 로그인 시점) 한 번 실행할지 여부.
  • KeepAlive: 프로세스가 죽으면 launchd가 자동으로 재시작할지 여부. true로 두면 크래시나 강제 종료 시에도 계속 되살아납니다.
  • StandardOutPath / StandardErrorPath: 표준 출력/에러를 리다이렉트할 파일. 백그라운드로 뜨는 프로세스는 터미널이 없으므로, 로그를 보려면 반드시 지정해야 합니다.

launchctl로 제어하기

# 등록 (최신 문법, macOS 10.11+)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.mybridge.plist

# 상태 확인
launchctl print gui/$(id -u)/com.example.mybridge

# 해제
launchctl bootout gui/$(id -u)/com.example.mybridge

# 예전 문법 (여전히 널리 쓰임)
launchctl load ~/Library/LaunchAgents/com.example.mybridge.plist
launchctl unload ~/Library/LaunchAgents/com.example.mybridge.plist

load/unload는 오래된 문법이지만 여전히 동작하고 스크립트에서 흔히 볼 수 있습니다. bootstrap/bootout은 launchd가 관리하는 여러 도메인(시스템 전체, 특정 사용자 GUI 세션 등)을 명시적으로 지정할 수 있어 더 정확합니다. gui/$(id -u)는 “현재 로그인한 사용자의 GUI 세션”을 가리킵니다.


설치 스크립트 자동화하기

사용자가 매번 plist를 손으로 복사하게 만들 필요는 없습니다. 셸 스크립트 한 번으로 plist를 생성하고 등록까지 끝내는 편이 좋습니다.

#!/bin/bash
set -euo pipefail

LABEL="com.example.mybridge"
PLIST_PATH="$HOME/Library/LaunchAgents/${LABEL}.plist"
BIN_PATH="$HOME/bin/mybridge"

mkdir -p "$HOME/Library/LaunchAgents"

cat > "$PLIST_PATH" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>${LABEL}</string>
    <key>ProgramArguments</key>
    <array>
        <string>${BIN_PATH}</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>
EOF

# 이미 등록되어 있으면 먼저 내리고 다시 올린다 (idempotent하게)
launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$PLIST_PATH"

echo "설치 완료: ${PLIST_PATH}"

bootout을 실패해도 무시하는 || true 처리가 중요합니다. 처음 설치하는 경우에는 아직 등록된 게 없어서 bootout이 실패하는 게 정상이기 때문입니다. 스크립트를 몇 번 실행해도 같은 결과가 나오는 idempotent한 설치 스크립트를 만드는 습관을 들이세요.


흔한 문제들

  • 경로 문제: LaunchAgent로 실행되는 프로세스는 셸 로그인 과정을 거치지 않으므로 .zshrcPATH 설정이 적용되지 않습니다. ProgramArguments에는 항상 절대 경로를 쓰세요.
  • plist 문법 오류: plutil -lint 파일.plist로 미리 검증하면 launchd가 조용히 무시해버리는 상황을 피할 수 있습니다.
  • 재시작 폭주: KeepAlive가 켜진 상태에서 프로그램이 시작하자마자 계속 크래시하면, launchd가 짧은 간격으로 재시작을 반복하다가 결국 “너무 빨리 재시작한다”며 포기합니다. ThrottleInterval 키로 최소 재시작 간격을 지정할 수 있습니다.
  • 변경 사항 미반영: plist를 수정한 뒤에는 bootoutbootstrap으로 다시 로드해야 합니다. launchd는 실행 중인 작업의 plist가 바뀌었다고 자동으로 알아채지 않습니다.

핵심 요약

  • launchd가 macOS의 모든 백그라운드 프로세스를 관리 — cron/init.d를 대체
  • LaunchAgent(사용자 세션, GUI 가능) vs LaunchDaemon(시스템 전체, root)
  • plist의 Label·ProgramArguments·RunAtLoad·KeepAlive가 핵심 설정
  • StandardOutPath/StandardErrorPath로 로그를 리다이렉트해야 백그라운드 프로세스 상태를 확인할 수 있다
  • 설치 스크립트는 bootout || true 패턴으로 idempotent하게 작성
  • PATH에 의존하지 말고 절대 경로 사용, plutil -lint로 plist 검증

다음 편에서는 7부의 마지막 주제인 AVFoundation과 사운드 재생을 다룹니다. 이벤트별로 다른 소리를 매핑하는 사운드팩 구조를 함께 설계해봅니다.

🤖 Generated with Claude Code

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다