trap을 활용한 에러 처리
PART 08에서 trap으로 시그널을 가로채고 클린업 패턴을 만들었습니다. 이번에는 한 발 더 나아갑니다. ERR 트랩을 활용해 에러가 발생한 위치, 호출 스택, 종료 코드를 자동으로 수집하고, 필요하다면 실행했던 작업을 되돌리는 롤백 패턴까지 구현합니다.
trap ERR — 에러 발생 시 실행
ERR은 시그널이 아니라 Bash의 특수 이벤트입니다. 명령어가 0이 아닌 종료 코드를 반환할 때마다 실행됩니다. set -e와 함께 쓰면 오류 발생 즉시 에러 핸들러가 실행되고 스크립트가 종료됩니다.
# 파일: trap_err_basic.sh#!/usr/bin/env bashset -euo pipefailon_error() { echo "오류가 발생했습니다!" >&2 echo "종료 코드: $?" >&2}trap on_error ERRecho "정상 명령어 실행"ls /없는경로 # 오류 발생 → on_error 실행echo "이 줄은 실행되지 않습니다."
실행 결과입니다.
정상 명령어 실행
ls: /없는경로: No such file or directory
오류가 발생했습니다!
종료 코드: 2
에러 정보 수집 함수
오류 메시지만으로는 부족합니다. 어떤 파일의 몇 번째 줄에서, 어떤 함수 안에서 오류가 났는지 알아야 합니다. $LINENO를 trap 표현식에서 전달하면 오류 발생 줄 번호를 정확히 잡을 수 있습니다.
# 파일: trap_err_info.sh#!/usr/bin/env bashset -euo pipefailerror_handler() { local exit_code=$? local line_number=$1 echo "" >&2 echo "=============================" >&2 echo " 오류 발생" >&2 echo "=============================" >&2 echo " 줄 번호 : ${line_number}" >&2 echo " 종료 코드: ${exit_code}" >&2 echo "=============================" >&2}# $LINENO를 인수로 전달 (trap 표현식에서 캡처)trap 'error_handler ${LINENO}' ERRecho "작업 1 시작"cp /없는파일 /tmp/ # 오류 발생echo "작업 2 시작"
실행 결과입니다.
작업 1 시작
cp: /없는파일: No such file or directory
=============================
오류 발생
=============================
줄 번호 : 20
종료 코드: 1
=============================
BASH_LINENO, FUNCNAME, BASH_SOURCE — 스택 트레이스
Bash는 함수 호출 스택을 추적할 수 있는 세 가지 배열 변수를 제공합니다.
| 변수 | 내용 |
|---|---|
BASH_LINENO |
함수가 호출된 줄 번호 배열 |
FUNCNAME |
함수 이름 배열 (0=현재, 1=호출자, ...) |
BASH_SOURCE |
각 함수가 정의된 파일 이름 배열 |
이 세 배열을 함께 사용하면 Python이나 Java의 스택 트레이스처럼 호출 경로를 출력할 수 있습니다.
# 파일: stack_trace.sh#!/usr/bin/env bashset -euo pipefailprint_stack_trace() { echo "" >&2 echo "=== 스택 트레이스 ===" >&2 local i for (( i=1; i<${#FUNCNAME[@]}; i++ )); do echo " #$((i-1)) ${BASH_SOURCE[$i]:-main}:${BASH_LINENO[$((i-1))]} in ${FUNCNAME[$i]:-main}()" >&2 done echo "===================" >&2}error_handler() { local exit_code=$? local line_number=$1 echo "" >&2 echo "[ERROR] 줄 ${line_number}에서 오류 발생 (종료 코드: ${exit_code})" >&2 print_stack_trace}trap 'error_handler ${LINENO}' ERR# 중첩 함수 호출로 스택 트레이스 테스트level3() { ls /없는경로 # 오류 발생 위치}level2() { level3}level1() { level2}echo "함수 호출 체인 시작"level1
실행 결과입니다.
함수 호출 체인 시작
ls: /없는경로: No such file or directory
[ERROR] 줄 24에서 오류 발생 (종료 코드: 2)
=== 스택 트레이스 ===
#0 stack_trace.sh:24 in level3()
#1 stack_trace.sh:28 in level2()
#2 stack_trace.sh:32 in level1()
#3 stack_trace.sh:36 in main()
===================
오류가 level3 함수의 24번째 줄에서 발생했고, level1 → level2 → level3 순으로 호출되었다는 것이 한눈에 보입니다.
에러 시 자동 롤백 패턴
배포 스크립트에서 중간에 오류가 생기면 이미 적용된 변경 사항을 되돌려야 합니다. 롤백 작업을 배열에 쌓아두고, 오류 발생 시 역순으로 실행하는 패턴입니다.
# 파일: rollback_pattern.sh#!/usr/bin/env bashset -euo pipefail# 롤백 명령어를 저장하는 배열ROLLBACK_STACK=()# 롤백 명령어 등록 함수push_rollback() { ROLLBACK_STACK+=("$1")}# 롤백 실행 함수 (역순)do_rollback() { echo "롤백 시작..." >&2 local i for (( i=${#ROLLBACK_STACK[@]}-1; i>=0; i-- )); do echo " 실행: ${ROLLBACK_STACK[$i]}" >&2 eval "${ROLLBACK_STACK[$i]}" || true done echo "롤백 완료" >&2}error_handler() { local exit_code=$? local line_number=$1 echo "[ERROR] 줄 ${line_number}에서 오류 발생 (코드: ${exit_code})" >&2 do_rollback}trap 'error_handler ${LINENO}' ERR# ---- 배포 시뮬레이션 ----echo "1. 설정 파일 백업"cp /etc/hosts /tmp/hosts.bakpush_rollback "cp /tmp/hosts.bak /etc/hosts"echo "2. 서비스 중지"# systemctl stop myapp (실제 환경)touch /tmp/myapp.stoppedpush_rollback "rm -f /tmp/myapp.stopped"echo "3. 파일 배포 (의도적 오류 발생)"cp /없는_배포파일 /tmp/ # 오류!echo "4. 서비스 시작 (여기까지 오지 않음)"
실행 결과입니다.
1. 설정 파일 백업
2. 서비스 중지
3. 파일 배포 (의도적 오류 발생)
cp: /없는_배포파일: No such file or directory
[ERROR] 줄 47에서 오류 발생 (코드: 1)
롤백 시작...
실행: rm -f /tmp/myapp.stopped
실행: cp /tmp/hosts.bak /etc/hosts
롤백 완료
성공한 작업들이 역순으로 되돌려집니다. 배포 중 오류가 생겨도 시스템이 일관된 상태를 유지합니다.
실습 — 에러 핸들러가 포함된 견고한 스크립트 템플릿
앞서 배운 내용을 종합한 실전 템플릿입니다. 이 템플릿을 기반으로 실제 스크립트를 빠르게 작성할 수 있습니다.
#!/usr/bin/env bash# 새 파일: robust_template.sh# ==========================================# 견고한 스크립트 템플릿# 사용법: ./robust_template.sh [인수]# ==========================================set -euo pipefail# ---- 상수 정의 ----readonly SCRIPT_NAME=$(basename "$0" .sh)readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)readonly TIMESTAMP=$(date '+%Y%m%d_%H%M%S')readonly LOG_FILE="/tmp/${SCRIPT_NAME}_${TIMESTAMP}.log"# ---- 로그 함수 ----log() { local level="$1" shift echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] $*" | tee -a "$LOG_FILE"}# ---- 스택 트레이스 ----print_stack_trace() { echo "" >&2 echo "=== 스택 트레이스 ===" >&2 local i for (( i=1; i<${#FUNCNAME[@]}; i++ )); do echo " #$((i-1)) ${BASH_SOURCE[$i]:-main}:${BASH_LINENO[$((i-1))]} in ${FUNCNAME[$i]:-main}()" >&2 done echo "===================" >&2}# ---- 롤백 스택 ----ROLLBACK_STACK=()push_rollback() { ROLLBACK_STACK+=("$1")}do_rollback() { if [[ ${#ROLLBACK_STACK[@]} -eq 0 ]]; then return fi log "WARN" "롤백 시작 (${#ROLLBACK_STACK[@]}개 작업)" local i for (( i=${#ROLLBACK_STACK[@]}-1; i>=0; i-- )); do log "WARN" "롤백: ${ROLLBACK_STACK[$i]}" eval "${ROLLBACK_STACK[$i]}" || true done log "WARN" "롤백 완료"}# ---- 에러 핸들러 ----error_handler() { local exit_code=$? local line_number=$1 log "ERROR" "오류 발생: 줄 ${line_number}, 종료 코드: ${exit_code}" print_stack_trace do_rollback log "ERROR" "로그 파일: ${LOG_FILE}" exit "${exit_code}"}# ---- EXIT 핸들러 ----on_exit() { local exit_code=$? if [[ $exit_code -eq 0 ]]; then log "INFO" "스크립트 정상 완료" fi}trap 'error_handler ${LINENO}' ERRtrap on_exit EXIT# ==========================================# 메인 로직# ==========================================log "INFO" "스크립트 시작: ${SCRIPT_NAME}"# 여기에 실제 작업 코드를 작성TEMP_DIR=$(mktemp -d)push_rollback "rm -rf ${TEMP_DIR}"log "INFO" "임시 디렉토리 생성: ${TEMP_DIR}"# 작업 예시echo "처리할 데이터" > "${TEMP_DIR}/input.txt"sort "${TEMP_DIR}/input.txt" > "${TEMP_DIR}/output.txt"log "INFO" "처리 완료"cat "${TEMP_DIR}/output.txt"
이 템플릿을 복사해서 메인 로직만 채워 넣으면 스택 트레이스, 자동 롤백, 로깅이 모두 갖춰진 스크립트를 빠르게 만들 수 있습니다.