본문 바로가기
C.W.K.
Lesson 05 of 06 · published

이야기하지 말고 재

~15 min · proof-and-fleet, epistemics, diagnostics, launchd, verification

Level 0번들 열어본 사람
0 XP0/84 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"맞는 관찰이든 그럴듯한 가정이든 결과는 똑같이 나와. 그러니 결과가 네 생각을 반박해줄 일은 없어."

일어난 적 없는 재시작

연습용 엔진을 치우던 세션이 pkill -f backend.serve를 돌렸어. 그러고는 launchctl list에서 진짜 엔진 이름 옆에 -15가 있는 걸 보고, 방금 보낸 SIGTERM이라고 읽었지. 포트가 여전히 응답하는 것까지 확인하고는, 주인의 엔진을 죽였더니 launchd가 다시 켰다고 보고했어. 비싸게 배운 교훈이라며 레포 함정 목록과 공유 학습 기록에 적어두기까지 했고. 실제론 아무 일도 없었어. 엔진은 다른 명령줄로 돌고 있어서 패턴에 걸린 프로세스가 없었고, 걸린 게 없으면 pkill은 아무것도 안 찍어. launchctl list의 두 번째 열은 언제 일어났든 마지막 종료 상태일 뿐이야. 포트가 응답한다는 것도 재시작이 있었든 없었든 똑같이 참이고. 답을 줄 수 있었던 값싼 관찰은 프로세스 시작 시각 하나였어. 확인해 보니 이틀 전에 시작된 프로세스였지. 함정 목록에 지어낸 사건이 들어 있으면 아예 기록이 없는 것보다 나빠. 대가를 치르고 얻은 교훈처럼 들리니까, 다음 사람이 틀린 모델을 그대로 물려받거든.

오래된 프로세스는 오래된 코드를 돌려

같은 관찰로 소리 없이 반쯤 망가지는 실패 여러 개를 설명할 수 있어. launchd로 도는 서비스는 시작할 때 코드를 딱 한 번만 불러와. 어떤 작업장 엔진은 일곱 시간 전에 고친 레지스트리를 무시하고 옛 레지스트리로 계속 작업을 준비했어. 다른 앱에선 다시 빌드한 웹 클라이언트가 묵은 엔진한테 그 엔진이 보내지도 않는 응답 필드를 달라고 했고, 결국 조용히 아무 일도 안 했어. 숫자 두 개면 답이 나와. 서비스 중인 프로세스의 시작 시각과, 그 프로세스가 돌려야 할 코드의 가장 최근 커밋 시각이야. 프로세스가 더 오래됐으면 코드가 아니라 프로세스 문제야. 이 레슨을 쓰는 동안 다시 재 보니, 이 Mac의 작업장 엔진 하나가 자기 레지스트리 커밋보다 30분 먼저 시작돼 있었어.

동작은 맞고 이유는 틀리고

가장 오래 살아남는 틀린 주장은 결과가 맞는 주장이야. 키트의 한 주석은, SwiftPM 실행 파일에선 Bundle.main.resourceURL이 nil이라서 패키징 안 된 실행이 안내 페이지를 띄운다고 설명했어. 안내 페이지는 진짜 떴어. 근데 그 속성은 nil이 아니라 빌드 디렉터리였고, 페이지가 뜬 건 기대한 하위 폴더가 없어서였어. 워치 모듈에는 받아쓰기 화면 하나가 언어를 바꾸는 "앱 안의 유일한 방법"이라고 적혀 있었어. 그 화면이 동작했으니까. 그런데 주인이 손목에서 직접 써보니, 평범한 화면에서 위로 쓸어올리기만 해도 전환이 됐어. 뭔가가 유일한 방법이라는 건 플랫폼 전체에 대한 주장이라서, 한 가지 방법이 되게 만든 것만으로는 성립시킬 수 없어. "경로 의존성에 새 파일을 넣으면 절대 안 잡힌다"는 메모도 있었는데, 5분짜리 검사에서 새 파일이 잡히자 쓴 사람이 직접 철회했어. 실제로 확인된 건 그 의존성을 쓰는 쪽 빌드가 묵은 상태일 수도 있다는 것, 그리고 모듈 시각과 새 파일 시각을 비교하면 답이 나온다는 거였어.

이 퀘스트도 같은 기준을 지켰어. 가족 메모에 있던 주장 두 개는 이 레슨들을 위해 재 보니 틀린 걸로 드러났어. 하나는 hardened runtime이 인증서로 서명한 앱의 designated requirement를 바꾼다는 주장이고, 다른 하나는 ssh 세션은 GUI 앱을 절대 못 띄운다는 주장이야. 레슨엔 측정이 보여준 대로 적었어.

Code

who.sh: 이 포트에서 누가 응답하고, 언제부터 돌았고, 자기 코드보다 오래됐나?·bash
#!/bin/zsh
# Which process is really answering on this port, since when, and is it older than the code it should run?
set -euo pipefail
port=${1:?port} code_path=${2:?path whose last commit the server must include}

pid=$(/usr/sbin/lsof -nP -iTCP:"$port" -sTCP:LISTEN -t | head -1)
[[ -n "$pid" ]] || { print "nothing listens on :$port"; exit 1; }
started=$(ps -o lstart= -p "$pid" | sed "s/ *$//")
command=$(ps -o command= -p "$pid" | cut -c1-80)
started_epoch=$(date -j -f '%a %b %d %T %Y' "$started" +%s)
commit_epoch=$(git log -1 --format=%ct -- "$code_path")

print "pid $pid on :$port, started $started"
print "command: $command"
print "last commit touching $code_path: $(git log -1 --format='%h %ci' -- "$code_path")"
if (( started_epoch < commit_epoch )); then
  print "STALE: the process predates that commit, so it is not running that code"
else
  print "fresh: the process started after that commit"
fi
읽기 세 개, 그리고 각각이 말해줄 수 있는 것과 없는 것·text
Run while this lesson was written, against a workshop engine on the same Mac (its label generalized):

  pid 70332 on :9500, started Mon Sep 14 21:38:18 2026
  last commit touching the registry: 2026-09-14 22:07:25
  STALE: the process predates that commit, so it is not running that code

  $ launchctl list | grep workshop
  70332   -15   com.example.workshop-serve   <- -15 is the LAST exit status, from some earlier
                                                stop. It says nothing about now.

  $ pkill -f 'a-pattern-that-matches-nothing'; echo $?
  1                                          <- no output at all; "|| true" makes it look done

External links

Exercise

네 Mac의 서비스 하나와 그 레포의 경로 하나로 who.sh를 돌려서, 프로세스가 최신 코드를 돌리고 있는지 적어. 이어서 거짓말하는 도구 두 개를 직접 증명해. 아무것도 안 맞는 패턴으로 pkill -f를 돌려서 출력과 종료 상태를 적어. 그리고 launchctl list에서 두 번째 열이 0이 아닌 작업을 찾아, ps -o lstart=로 그 작업이 최근에 종료된 적이 없다는 걸 보여줘. 마지막으로 네 코드에서 "유일한 방법"이라거나 "절대 안 일어난다"고 주장하는 주석을 하나 골라, 직접 재거나 관찰한 대로 고쳐 써.
Hint
lsof -t는 프로세스 id만 찍어서 변수에 담기 안전해. 작업이 멈춰 있다면 launchctl print gui/$(id -u)/<label>이 상태와 마지막 종료 이유를 글로 풀어 보여줘. 목록의 숫자 열보다 나은 정보야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.