"맞는 관찰이든 그럴듯한 가정이든 결과는 똑같이 나와. 그러니 결과가 네 생각을 반박해줄 일은 없어."
일어난 적 없는 재시작
연습용 엔진을 치우던 세션이 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 앱을 절대 못 띄운다는 주장이야. 레슨엔 측정이 보여준 대로 적었어.