배포 스크립트가 Ctrl+C나 SIGTERM으로 중간에 끊기면 임시 파일이 /tmp에 남고, lock 파일이 안 지워져 다음 실행이 죽고, 쓰다 만 결과 파일이 완료된 것처럼 남기도 한다. Bash의 trap은 시그널을 받거나 종료될 때 명령을 실행하도록 예약하는 내장 기능이다. 이 글에서는 trap 기본 문법부터 EXIT/ERR/INT/TERM 활용, set -euo pipefail 조합, 함수/서브셸 스코프 주의사항까지 정리한다.
trap 기본 문법
trap '명령' 시그널이름 [시그널이름 ...]
# 예시
trap 'echo "인터럽트 받음"' SIGINT
trap 'echo "스크립트 종료"' EXIT| 시그널 | 발생 상황 |
|---|---|
| EXIT | 실제 시그널 아님 — 스크립트가 어떤 이유로든(정상 종료, exit, 에러) 끝날 때 발생하는 Bash 의사 이벤트 |
| ERR | set -e 상태에서 명령이 0이 아닌 종료 코드를 반환할 때 |
| SIGINT | 터미널에서 Ctrl+C |
| SIGTERM | kill의 기본 시그널, 정상 종료 요청 |
| SIGHUP | 터미널 연결 종료(SSH 끊김 등) |
트랩 명령은 작은따옴표로 감싸서 등록 시점이 아니라 실행 시점에 변수가 평가되게 한다. 큰따옴표는 등록 순간 바로 치환돼버려 의도와 다르게 동작할 수 있다.
임시 파일 자동 정리 — mktemp + trap EXIT
EXIT 트랩은 종료 경로와 무관하게 마지막에 한 번 실행돼 finally처럼 쓸 수 있다.
#!/usr/bin/env bash
set -euo pipefail
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
echo "작업 디렉터리: $TMPDIR"
echo "some data" > "$TMPDIR/work.txt"
# 여기서 에러가 나도, Ctrl+C가 눌려도 EXIT 트랩이 실행되어 TMPDIR이 지워진다
sleep 3
cat "$TMPDIR/work.txt"$ ./cleanup.sh
작업 디렉터리: /tmp/tmp.Xk3nP9wLqe
^C
$ ls /tmp/tmp.Xk3nP9wLqe
ls: cannot access '/tmp/tmp.Xk3nP9wLqe': No such file or directory
EXIT 트랩 하나로 SIGINT/SIGTERM 비정상 종료와 정상 종료를 모두 커버한다.
여러 시그널 동시 처리
#!/usr/bin/env bash
set -euo pipefail
LOCKFILE="/tmp/myjob.lock"
cleanup_and_exit() {
local sig="$1"
echo "[$sig] 수신, lock 해제 후 종료" >&2
rm -f "$LOCKFILE"
# 130 = 128+SIGINT(2), 143 = 128+SIGTERM(15) 관례를 따른다
case "$sig" in
INT) exit 130 ;;
TERM) exit 143 ;;
*) exit 1 ;;
esac
}
touch "$LOCKFILE"
trap 'cleanup_and_exit INT' SIGINT
trap 'cleanup_and_exit TERM' SIGTERM
echo "작업 중... (PID: $$)"
sleep 60같은 명령을 여러 시그널에 한 번에 걸 수도 있다.
trap 'echo "종료 신호 수신"; rm -f "$LOCKFILE"; exit 1' SIGINT SIGTERM SIGHUPtrap -p로 현재 등록된 트랩 확인
$ trap -p
trap -- 'rm -rf "$TMPDIR"' EXIT
trap -- 'cleanup_and_exit INT' SIGINT
trap -- 'cleanup_and_exit TERM' SIGTERM
$ trap -p EXIT
trap -- 'rm -rf "$TMPDIR"' EXIT
trap -p SIGINT처럼 시그널을 지정하면 그 하나만 볼 수 있다. 완전히 해제하려면 trap - EXIT.
set -euo pipefail과 ERR 트랩 조합
set -e는 실패 시 즉시 중단만 시킬 뿐 “어디서” 실패했는지는 안 남긴다. ERR 트랩으로 라인 번호와 명령을 기록한다.
#!/usr/bin/env bash
set -euo pipefail
on_error() {
local exit_code=$?
local line_no="$1"
echo "에러 발생: line ${line_no}, exit code ${exit_code}" >&2
echo "실행 중이던 명령: ${BASH_COMMAND}" >&2
}
trap 'on_error $LINENO' ERR
echo "1단계 시작"
false # 여기서 실패
echo "2단계 (실행되지 않음)"$ ./err_trap.sh
1단계 시작
에러 발생: line 13, exit code 1
실행 중이던 명령: false
$LINENO/$BASH_COMMAND로 실패 위치를 잡는다. ERR 트랩이 EXIT 트랩보다 먼저 실행되므로 “기록 → 정리” 순서가 보장된다. 단 if cmd; then, cmd && other처럼 set -e가 무시하는 문맥에서는 ERR 트랩도 발생하지 않는다.
종합 예시 — 안전한 배포 스크립트
#!/usr/bin/env bash
set -euo pipefail
LOCKFILE="/tmp/deploy.lock"
WORKDIR="$(mktemp -d)"
EXIT_CODE=0
log() { echo "[$(date '+%H:%M:%S')] $*"; }
on_error() {
log "에러: line $1에서 실패 (명령: ${BASH_COMMAND})"
EXIT_CODE=1
}
cleanup() {
log "정리 시작 (exit code: ${EXIT_CODE})"
rm -rf "$WORKDIR"
rm -f "$LOCKFILE"
log "정리 완료"
exit "$EXIT_CODE"
}
if [[ -e "$LOCKFILE" ]]; then
log "이미 실행 중입니다 (lock: $LOCKFILE)"
exit 1
fi
touch "$LOCKFILE"
trap 'on_error $LINENO' ERR
trap cleanup EXIT
trap 'log "인터럽트 수신"; exit 130' SIGINT SIGTERM
log "작업 디렉터리: $WORKDIR"
log "배포 시작"
cp -r ./build/* "$WORKDIR/"
rsync -a "$WORKDIR/" ./deploy-target/
log "배포 완료"SIGINT/SIGTERM 트랩의 exit 130이 다시 EXIT 트랩(cleanup)을 발동시키므로, lock 해제와 임시 디렉터리 삭제는 cleanup 한 곳에만 몰아둬도 어떤 종료 경로든 실행된다.
주의사항
- 함수 안 트랩도 전역이다. 함수 스코프가 아니라 셸 전체에 적용된다 — 함수마다 지역 정리가 필요하면 트랩을 저장/복원하거나 스크립트 레벨에서 한 번만 등록.
- 서브셸에는 트랩이 안 넘어간다.
( ... ),$(...), 파이프라인 각 단계는 별도 서브셸 — 필요하면 서브셸 내부에서 다시 등록. - 재등록은 덮어쓴다. 같은 시그널에
trap을 두 번 걸면 나중 것이 이긴다. - SIGKILL(9)/SIGSTOP은 못 잡는다.
kill -9는 EXIT 트랩도 실행 안 됨 — 락/임시파일은 PID 생존 확인 같은 별도 방어 수단도 고려. - ERR 트랩 예외:
if cmd; then,cmd && other,while cmd; do문맥에서는set -e가 무시되므로 ERR 트랩도 안 걸림.
마무리
임시 자원 정리는 EXIT 트랩 하나로 몰고, 에러 위치는 ERR 트랩+$LINENO/$BASH_COMMAND로 남기고, 함수/서브셸 스코프와 SIGKILL 한계만 미리 알아두면 된다. 실행 중 끊길 가능성이 있는 스크립트라면 trap 하나로 사고 대부분을 예방할 수 있다.