과외 자료를 자동으로 정리하는 스크립트를 만들다 보니, 저는 그걸 매일 정해진 시각에 스스로 돌게 예약 작업으로 걸어뒀어요. 그런데 문득 이런 질문이 떠올랐습니다. “이 예약 작업은 지금 내 코드의 어느 버전을 돌리고 있는 거지?” 답을 바로 못 하겠더라고요. 이건 사고가 나서 쓴 글이 아니라, 사고가 나기 전에 막은 이야기입니다. 그런데 왜 미리 막아야 했는지, 구조를 보면 납득이 되실 거예요.
예약 작업은 ‘커밋한 버전’이 아니라 ‘지금 폴더 상태’를 돌린다
먼저 용어부터 풀게요. 예약 작업(scheduled task): 정해진 시각에 사람이 없어도 알아서 실행되는 작업이에요. 저는 상태 집계 스크립트를 매일 밤 도는 예약 작업으로 걸어뒀습니다.
여기서 많은 사람이 착각하는 게 있어요. 예약 작업이 “내가 어제 완성해서 저장(커밋)한 그 버전”을 돌린다고 생각하기 쉬운데, 아니에요. 예약 작업은 그 폴더의 바로 지금 디스크 상태를 그대로 실행합니다. 무슨 뜻이냐면요.
- 브랜치(branch): 코드의 평행 세계 같은 거예요. 안정된 본선을
main이라고 하고, 새 기능을 실험할 땐feature라는 곁가지를 따로 만들어 거기서 작업해요. - 체크아웃(checkout): 폴더를 특정 브랜치 상태로 “갈아 끼우는” 것. feature 브랜치를 체크아웃하면 그 폴더의 파일들이 실험 중인 코드로 바뀝니다.
문제가 보이시나요? 제가 밤에 새 기능을 실험하려고 feature 브랜치를 체크아웃해둔 채 잠들면, 그 폴더는 지금 실험 코드 상태예요. 그리고 아침에 예약 작업이 깨어나면, 그 실험 중인 코드를 그대로 프로덕션(실제 운영)으로 돌립니다. 제가 “이건 아직 테스트 중인데”라고 생각하는 코드가, 사람 없는 새벽에 진짜로 실행돼버리는 거죠.
왜 미리 막아야 했나 — 터지고 나서 알기엔 비싼 위험
솔직하게 밝힐게요. 이것 때문에 실제로 사고가 난 적은 없어요. 저는 관측된 오작동을 보고 고친 게 아니라, 구조를 보고 “이건 언젠가 반드시 터진다”고 판단해서 미리 막았습니다.
그 판단의 근거는 이야기가 아니라 구조예요. 예약 작업이 디스크의 현재 상태를 실행한다는 사실은 변하지 않아요. 그리고 그 폴더에서 개발도 하는 사람(1인 개발자라면 거의 전부죠)은 언젠가 반드시 feature 브랜치를 체크아웃해둔 채 밤을 넘깁니다. 두 개가 겹치는 날, 예약 작업은 조용히 엉뚱한 코드를 돌려요.
이 위험이 고약한 이유: 티가 안 나요. 예약 작업은 사람이 없을 때 도니까, 잘못된 코드가 돌아도 그 자리에서 아무도 못 봐요. 나중에 결과가 이상해진 뒤에야 “어, 왜 이러지?” 하고 거슬러 올라가게 되죠. 터지고 나서 원인을 찾는 비용이 큰 종류라, 터지기 전에 막는 게 남는 장사입니다.
가드의 형태 — ‘무인일 때만’ 막는 플래그
그래서 브랜치 가드를 붙였어요. 핵심 아이디어는 “예약 작업이 실행되는 순간 현재 브랜치가 main이 아니면, 그냥 실행하지 말고 멈춰라”입니다.
그런데 여기서 제일 중요한 설계 결정이 있어요. 이 가드를 무조건 켜지 않았다는 것. 만약 “항상 main이 아니면 무조건 중단”으로 만들면, 제가 feature 브랜치에서 손으로 직접 스크립트를 테스트할 때도 막혀버려요. 그건 개발을 방해하죠.
그래서 가드를 플래그(flag, 실행할 때 붙이는 옵션 스위치) 뒤에 숨겼어요. 예약 작업이 부르는 무인 진입점에서만 그 플래그(--require-main)를 붙이게 했습니다. 정리하면 이래요.
- 예약 작업(무인 실행): 플래그가 붙음 → main이 아니면 중단. 엉뚱한 코드가 새벽에 도는 걸 막음.
- 내가 손으로 실행(대화형 개발): 플래그 없음 → 가드가 발동 안 함. feature 브랜치에서 자유롭게 테스트.
즉 이건 항상 main만 실행하는 게 아니라, 무인 진입점에서만 main을 강제하는 것이에요. 사람이 개입하는 자리와 개입하지 않는 자리를 구분한 게 이 설계의 핵심입니다.
그리고 하나 더. 가드가 걸려서 멈출 때 조용히 죽지 않게 했어요. 명시적인 종료 코드(exit code, 프로그램이 끝날 때 성공·실패를 알리는 숫자)로 중단해서, 나중에 로그를 보면 “아, 브랜치가 main이 아니라서 안 돌았구나”를 알 수 있게요. 조용히 넘어가면 안 돌아간 이유조차 못 찾거든요.
검증 — ‘발동할 때’와 ‘발동하지 말아야 할 때’ 둘 다 확인
가드는 만들었다고 끝이 아니에요. 두 방향을 다 확인해야 진짜 검증입니다. 한쪽만 보면 반쪽짜리예요.
첫째, 발동해야 할 때 발동하는가. feature 브랜치로 갈아 끼운 상태에서 무인 플래그를 붙여 실행해봤어요. 결과는 곧바로 중단(종료 코드 2), 평소 같으면 나올 실행 로그도 없고 네트워크 호출도 없었습니다. 일이 시작되기 전에 딱 끊긴 거죠. 가드가 제대로 문을 막았다는 뜻이에요.
둘째, 그리고 이게 자주 빠뜨리는 부분인데, 발동하지 말아야 할 때 얌전히 있는가. 플래그 없이(손으로 개발하듯) 실행하면 가드가 발동하지 않아야 해요. 안 그러면 개발을 막으니까요. 이것도 확인했습니다 — 플래그가 없으면 가드는 조용히 비켜줬어요.
이 “양쪽 검증”은 습관으로 만들면 좋아요. 안전장치를 넣을 땐 막아야 할 걸 막는지뿐 아니라 막지 말아야 할 걸 안 막는지까지 봐야, 안전장치가 오히려 일을 방해하는 사태를 피할 수 있거든요. 실제로 이 브랜치 문제는 비슷한 예약 스크립트들에서도 똑같이 생길 수 있어서, 같은 가드를 그쪽에도 옮겨 붙이며 양쪽 경로를 다시 확인했어요.
정리 — 자동화에 물어볼 질문
| 점검 질문 | 왜 중요한가 | 액션 |
|---|---|---|
| 예약 작업이 어느 브랜치를 도는가 | 커밋 버전이 아니라 ‘지금 디스크’를 실행함 | 🔴 지금: main일 때만 무인 실행하게 가드 |
| 가드가 손 개발까지 막지 않나 | 무조건 강제는 개발을 방해함 | 🔴 지금: 플래그로 무인 진입점만 강제 |
| 멈출 때 티가 나는가 | 조용히 죽으면 원인을 못 찾음 | 🟡 백로그: 명시적 종료 코드로 중단 |
| 검증을 양쪽 다 했나 | 발동/미발동 둘 다 봐야 반쪽이 아님 | 🟢 계속: 안전장치는 양방향 테스트 |
한마디로, 자동화에게 “지금 어느 코드를 돌리고 있니?”라고 물어보라는 거예요. 예약 작업은 내가 커밋한 안정 버전이 아니라 폴더의 현재 상태를 돌리기 때문에, 개발과 자동화가 같은 폴더를 쓰면 언젠가 부딪힙니다. 사고가 난 뒤에 알기엔 비싼 위험이라, 구조를 이해했다면 터지기 전에 문 하나 달아두는 게 낫습니다.