iBetter Books
수정

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번째 줄에서 발생했고, level1level2level3 순으로 호출되었다는 것이 한눈에 보입니다.

에러 시 자동 롤백 패턴

배포 스크립트에서 중간에 오류가 생기면 이미 적용된 변경 사항을 되돌려야 합니다. 롤백 작업을 배열에 쌓아두고, 오류 발생 시 역순으로 실행하는 패턴입니다.

# 파일: 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"

이 템플릿을 복사해서 메인 로직만 채워 넣으면 스택 트레이스, 자동 롤백, 로깅이 모두 갖춰진 스크립트를 빠르게 만들 수 있습니다.