콘텐츠로 건너뛰기
Codex

macOS에서 launchd 작업이 실행되지 않을 때 점검 순서

TECH NOTE · AUTOMATION OPS

macOS에서 launchd 작업이 실행되지 않을 때 등록 상태, plist 위치와 문법, 실행 경로, 로그, 권한을 어떤 순서로 점검하는지 정리했습니다.

먼저 보는 순서

macOS에서 launchd 작업이 안 돌면, 나는 보통 네 군데를 순서대로 본다. 먼저 launchctl에 작업이 올라와 있는지 보고, 그다음 plist가 맞는 위치에 있는지 본다. 그다음 로그가 실제로 어디로 떨어지는지 확인하고, 마지막으로 파일 권한이나 실행 권한에서 막힌 건 아닌지 본다.

처음엔 plist 내용부터 뒤지기 쉬운데, 내 경우에는 그 전에 “launchd가 이 작업을 알고 있나?”부터 보는 게 덜 헷갈렸다.

launchctl 상태부터 본다

사용자 로그인 세션에서 도는 작업이면 보통 gui/내 uid 쪽에 올라간다. 내 uid는 이렇게 본다.

id -u

그 uid 기준으로 launchd 쪽 상태를 훑는다.

launchctl print "gui/$(id -u)" | head -n 40

여기서 봐야 하는 건 대단한 해석이 아니다. 내가 등록했다고 생각한 label이 보이는지, 서비스 목록 자체가 정상적으로 찍히는지부터 보면 된다.

비활성화된 항목도 따로 본다. plist는 맞게 넣었는데 예전에 disable 해둔 상태면, 파일만 봐서는 잘 안 보인다.

launchctl print-disabled "gui/$(id -u)" | head -n 40

여기서 내 작업 label이 disabled 쪽으로 보이면, plist를 고치기 전에 enable 상태부터 풀어야 한다. 반대로 목록에 아예 없다면 등록이 안 된 쪽으로 생각하는 게 맞다.

plist 위치가 맞는지 본다

launchd는 위치가 꽤 중요하다. 대충 아무 데나 plist를 두고 “왜 안 돌지” 하면 안 된다.

위치 용도
~/Library/LaunchAgents 현재 사용자 로그인 세션에서 도는 작업
/Library/LaunchAgents 모든 사용자에게 적용되는 사용자 에이전트
/Library/LaunchDaemons 로그인과 상관없이 시스템 쪽에서 도는 작업

내가 자주 헷갈린 건 LaunchAgentLaunchDaemon의 차이다. 메뉴바 앱이나 사용자 홈 디렉터리, GUI 세션이 필요한 작업이면 보통 LaunchAgent 쪽이다. 백그라운드 시스템 작업이면 LaunchDaemon 쪽을 본다.

현재 사용자 LaunchAgents 폴더가 있는지도 먼저 본다.

ls -ld ~/Library/LaunchAgents

그리고 plist 문법이 깨지지 않았는지 확인한다.

find ~/Library/LaunchAgents -maxdepth 1 -name "*.plist" -print -exec plutil -lint {} \;

OK가 나오면 적어도 plist XML 문법은 살아 있다는 뜻이다. 이게 성공했다고 작업이 돈다는 뜻은 아니다. 그냥 “파일을 launchd가 읽을 수 있는 형태인가”까지만 통과한 것이다.

label과 파일명이 꼭 같은 문제는 아니다

plist 파일명과 Label 값은 보통 맞춰 두는 게 편하다. 예를 들어 파일이 com.me.backup.plist라면 label도 com.me.backup처럼 둔다.

launchctl에서 작업을 찾을 때는 파일명이 아니라 Label을 기준으로 본다. 여기서 처음에 꽤 헷갈렸다. 파일은 있는데 상태 조회에서 안 보인다면, plist 안의 Label이 내가 생각한 이름과 다른지 먼저 확인한다.

로그 위치를 plist에 박아두면 덜 헤맨다

launchd 작업은 터미널에서 직접 실행하는 게 아니라서, 출력이 눈앞에 안 보인다. 그래서 StandardOutPath, StandardErrorPath를 넣어두는 편이 낫다.

<key>StandardOutPath</key>
<string>/tmp/my-launchd.out.log</string>
<key>StandardErrorPath</key>
<string>/tmp/my-launchd.err.log</string>

이걸 안 넣어두면 “실행이 안 된 건지, 실행됐는데 바로 죽은 건지”가 잘 안 갈린다. 내 경우에는 stderr 파일을 보고 나서야 경로 하나가 잘못된 걸 알았다.

특히 launchd에서 실행되는 작업은 내가 터미널에서 쓰는 셸 환경과 다르다. PATH도 다르고, 현재 디렉터리도 내가 생각한 프로젝트 폴더가 아닐 수 있다. 그래서 스크립트나 바이너리 경로는 가능하면 절대경로로 쓰는 게 낫다.

권한 문제는 마지막에 보지만 자주 걸린다

plist가 맞고 label도 맞고 로그도 봤는데 여전히 안 돌면 권한을 본다. 실행할 스크립트에 실행 권한이 없는 경우가 꽤 흔하다.

사용자 LaunchAgent라면 plist 파일은 내 계정이 읽을 수 있어야 한다. 스크립트도 실행 가능해야 한다. Daemon 쪽이면 소유자와 권한 규칙이 더 빡빡해진다.

이때도 무작정 sudo를 붙이면 헷갈릴 수 있다. launchctl은 내가 어느 도메인에 등록하는지에 따라 결과가 달라진다. 일반 사용자 작업을 보려는 건데 sudo launchctl로 보면 시스템 도메인을 보고 있을 수 있다.

내가 실제로 보는 순서

  1. launchctl print "gui/$(id -u)"로 사용자 도메인이 정상인지 본다.
  2. launchctl print-disabled "gui/$(id -u)"로 꺼진 상태가 아닌지 본다.
  3. plist가 ~/Library/LaunchAgents에 있는지 확인한다.
  4. plutil -lint로 plist 문법을 본다.
  5. plist 안의 Label이 내가 찾는 이름과 같은지 본다.
  6. StandardOutPath, StandardErrorPath로 로그를 남긴다.
  7. 스크립트 경로와 실행 권한을 확인한다.
  8. 그래도 안 되면 GUI 작업인지 Daemon 작업인지 다시 나눈다.

같이 나오는 질문

터미널에서 직접 실행하면 되는데 launchd에서는 왜 실패하나

대부분 환경 차이다. 터미널에서 잡혀 있던 PATH, 현재 디렉터리, 로그인 셸 설정이 launchd에는 그대로 들어가지 않는다. 그래서 launchd에 넣을 명령은 절대경로 기준으로 생각하는 게 편하다.

plist 파일명과 Label이 다르면 안 되나

반드시 같아야 하는 건 아니다. 그래도 맞춰 두는 게 좋다. 나중에 launchctl에서 찾을 때 기준은 Label이라, 둘이 다르면 내가 만든 작업을 내가 못 찾는 일이 생긴다.

로그 파일이 안 생기면 실행이 안 된 건가

그럴 수도 있고, 로그 파일을 만들 권한이 없는 걸 수도 있다. /tmp처럼 쓰기 쉬운 위치로 먼저 빼서 확인한 다음, 원하는 위치로 옮기는 편이 덜 헷갈린다.

다음에 볼 건 plist 안의 ProgramArguments다. launchd에서는 셸이 알아서 명령줄을 해석해준다고 생각하면 자주 꼬인다.

이 블로그 더 보기

이 블로그는 실제 프로젝트를 AI와 함께 굴리면서 남긴 기록입니다.

  • 새 글은 RSS로 받아볼 수 있습니다.
  • 시리즈 전체는 여기에 정리해 두었습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다