LabHub

셸 스크립팅 · 견고한 스크립트 · 이론

set -euo pipefail, 그리고 그것만으로 부족한 이유

LabHub 에서 이어서 보기

한 줄 요약

set -euo pipefail 은 스크립트가 조용히 잘못된 결과를 내는 것을 막는 최소 장치다. 하지만 그것만으로는 임시 파일 정리도, 중복 실행 방지도 되지 않는다.

왜 이게 필요했나

백업 스크립트가 매일 새벽에 돌았고 매일 성공으로 보고했다. 복구가 필요한 날, 백업 파일이 0바이트라는 걸 알았다.

tar czf "$DEST" "$SRC_DIR"

SRC_DIR 이 오타로 비어 있었고, 셸은 빈 문자열을 그냥 넘겼고, tar 는 빈 아카이브를 만들었고, 종료 코드는 0이었다. 세 군데서 각각 "조용히" 넘어간 결과다.

set -u 하나면 첫 단추에서 멈췄을 것이다. 이 옵션들의 가치는 문제를 발견 지점에서 멈춰 세우는 것에 있다.

어떻게 동작하나

네 줄짜리 표준 헤더는 이렇다.

#!/usr/bin/env bashset -euo pipefailIFS=$'\n\t'

그런데 set -e 로 모든 게 해결되지 않는다. 오히려 함정이 있다.

그래서 trap 이 짝으로 필요하다.

cleanup() {  local rc=$?  rm -f "$TMPFILE"  exit "$rc"}trap cleanup EXIT

여기서 두 가지가 핵심이다. 첫째, EXIT 트랩은 정상 종료·오류 종료·시그널 종료 모두에서 돈다. 둘째, 맨 앞에서 $? 를 붙잡아 두고 마지막에 되돌려주지 않으면 트랩이 종료 코드를 삼켜 버린다.

임시 파일은 반드시 mktemp 로 만든다. 고정된 이름은 동시 실행 시 충돌하고, /tmp 에서는 심볼릭 링크 공격의 표적이 된다.

현장에서 만나는 모습

중복 실행 방지. 크론이 5분마다 도는 작업이 6분 걸리기 시작하면 두 개가 겹쳐 돈다. 잠금이 없으면 같은 파일을 동시에 쓰고 데이터가 깨진다. flock 이나 mkdir 기반 잠금으로 두 번째 실행은 즉시 종료시키되, 종료 코드로 "왜 안 돌았는지"를 구분해 주는 게 중요하다. 그래야 모니터링이 "실패"와 "이미 돌고 있어서 건너뜀"을 구분한다.

종료 코드 규약. 0 성공, 1 일반 실패까지는 모두가 안다. 그 이상은 프로젝트가 정한다. sysexits 관례를 빌리면 64 는 사용법 오류, 66 은 입력 파일 없음이다. 값 자체보다 일관되게 쓰고 문서에 적는 것이 중요하다.

재시도는 조건부로. 네트워크 호출은 실패해도 다시 하면 성공할 수 있다. 다만 재시도에는 세 가지 조건이 붙는다. 최대 횟수가 있어야 하고, 시도 사이에 쉬어야 하고(안 쉬면 상대를 더 밀어붙인다), 성공하면 즉시 멈춰야 한다. 그리고 재시도해도 되는 작업인지(멱등한지) 먼저 확인해야 한다 - 결제 요청을 재시도하면 두 번 결제된다.

shellcheck 는 무료 리뷰어다. 따옴표 누락, 사용하지 않는 변수, 위험한 패턴을 잡아 준다. CI 에 넣어 두면 리뷰 시간이 눈에 띄게 줄어든다.

다음 실습에서 할 것

엄격 모드가 실제로 동작하는지 행동으로 증명하고, pipefail 로 파이프 실패를 전파시키고, trap 으로 임시 파일을 정리하고, 잠금으로 중복 실행을 막고, 공백이 든 경로를 안전하게 처리하고, 종료 코드 규약을 지키는 백업 스크립트와 재시도 헬퍼를 만든 뒤, 마지막에는 다른 스크립트를 검사하는 감사 도구를 작성한다.