Bash 스크립트에서 효과적인 오류 처리 전략

엄격 모드, 트랩, 종료 코드 및 명확한 stderr 메시지를 사용하여 Bash 스크립트가 안전하게 실패하고 스스로 정리하도록 만듭니다.

Bash 스크립트에서 효과적인 오류 처리 전략

Bash 스크립트 오류 처리는 중요합니다. 조용히 실패하는 스크립트는 부분 파일을 복사하거나, 깨진 코드를 배포하거나, 잘못된 경로를 삭제할 수 있기 때문입니다. 스크립트가 중요한 단계에서 실패할 때 중단되고, 무슨 일이 일어났는지 설명하며, 종료 전에 임시 파일을 정리하도록 해야 합니다.

아래 패턴들은 가장 자주 필요한 요소들을 다룹니다: 엄격 모드, 명시적 검사, trap, 그리고 간단한 오류 보고.

기본: 종료 상태 이해하기

Unix 세계에서 실행된 모든 명령은 종료 상태(또는 종료 코드)를 반환하며, 이는 작업 결과를 나타내는 정수 값입니다. 이 상태는 특수 변수 $?에 즉시 저장됩니다.

  • 종료 코드 0: 관례상 성공을 의미합니다('참' 또는 'true').
  • 종료 코드 1–255: 실패를 의미합니다('거짓' 또는 'false'). 특정 코드는 종종 특정 유형의 실패와 관련됩니다(예: 1은 일반 오류, 127은 명령을 찾을 수 없음).

신뢰할 수 있는 스크립트는 중요한 명령의 종료 상태를 확인하고, 스크립트가 실패할 경우 의미 있는 0이 아닌 코드를 반환해야 합니다.

핵심 전략 1: 방어적 스크립팅 삼중주

진지한 자동화 스크립트의 경우, shebang 라인(#!/bin/bash) 바로 다음에 세 가지 기본 옵션을 적용하는 것으로 시작해야 합니다. 이러한 옵션은 엄격하고 예측 가능한 동작을 강제합니다.

1. 실패 시 즉시 종료 (set -e)

set -e 옵션(또는 set -o errexit)은 명령이 실패하면(0이 아닌 종료 상태 반환) 스크립트가 즉시 종료되어야 한다고 지시합니다.

이는 종종 "빠른 실패(fail fast)" 원칙이라고 불리며, 불완전하거나 실패한 사전 조건 결과로 스크립트가 잠재적으로 파괴적인 작업을 진행하는 것을 방지합니다.

#!/bin/bash
set -e

echo "프로세스 시작..."
mkdir /tmp/test_dir
cp non_existent_file /tmp/test_dir/ # 이 명령은 실패합니다 (종료 코드 > 0)

echo "이 줄은 실행되지 않습니다." # 스크립트가 여기서 종료됩니다

경고: set -e 주의사항

set -eif 또는 while로 테스트된 명령, 대부분의 && 또는 || 목록의 명령, !로 상태가 반전된 명령 등 여러 일반적인 컨텍스트에서 종료를 트리거하지 않습니다. 예상되는 실패 주변의 명확한 검사를 대체하는 것이 아니라 안전망으로 취급하십시오.

2. 설정되지 않은 변수를 오류로 처리 (set -u)

set -u 옵션(또는 set -o nounset)은 설정되지 않은 변수의 사용을 오류로 처리하여 스크립트가 즉시 종료되도록 합니다(set -e와 유사). 이는 변수 이름의 오타가 중요한 명령에 빈 문자열을 전달하는 미묘한 버그를 방지합니다.

#!/bin/bash
set -u

# echo "변수는: $UNDEFINED_VAR" # 스크립트가 실패하고 여기서 종료됩니다

MY_VAR="defined"
echo "변수는: ${MY_VAR}"

3. 명령 파이프라인 처리 (set -o pipefail)

기본적으로 명령 파이프라인(command1 | command2 | command3)은 마지막 명령(command3)의 종료 상태만 보고합니다. command1이 실패했지만 command3이 성공하면 $?는 0이 되어 실패를 가립니다.

set -o pipefail은 이 동작을 변경하여 파이프라인의 어느 명령이든 실패하면 파이프라인이 0이 아닌 상태를 반환하도록 합니다. 이는 안정적인 데이터 처리에 중요합니다.

#!/bin/bash
set -o pipefail

# `false` 명령은 항상 1로 종료됩니다
# pipefail이 없으면 이 줄은 `cat`이 성공하므로 0을 반환합니다.
false | cat # pipefail 때문에 1을 반환합니다

if [ $? -ne 0 ]; then
    echo "파이프라인이 실패했습니다."
fi

모범 사례: 헤더

항상 결합된 방어 옵션으로 견고한 스크립트를 시작하십시오:

#!/bin/bash
set -euo pipefail

핵심 전략 2: 수동 검사 및 조건부 실행

set -e가 대부분의 실패를 처리하지만, 특히 실패가 예상되거나 특정 로깅이 필요할 때 명령 상태를 수동으로 확인해야 하는 경우가 많습니다.

if 문 검사

명령의 성공을 확인하는 표준 방법은 if 블록 내에서 종료 상태를 캡처하는 것입니다. 이 방법은 set -e 동작을 재정의하여 오류를 명시적으로 처리할 수 있게 합니다.

#!/bin/bash
set -euo pipefail

TEMP_FILE="/tmp/data_processing_$$/config.dat"

# 디렉토리 생성 시도; 실패를 명시적으로 처리
if ! mkdir -p "$(dirname "$TEMP_FILE")"; then
    echo "[ERROR] 임시 디렉토리를 생성할 수 없습니다." >&2
    exit 1
fi

# 데이터 가져오기 시도
if ! curl -sSf https://api.example.com/data > "$TEMP_FILE"; then
    echo "[ERROR] API에서 데이터를 가져오지 못했습니다." >&2
    exit 2
fi

echo "데이터를 성공적으로 검색했습니다."

팁: curl-sSf 플래그(조용함, 실패, 오류 표시)는 HTTP 오류 시 curl이 0이 아닌 종료 코드를 반환하도록 강제하여 오류 처리를 더 쉽게 만듭니다.

단축 연산자 사용 (&&||)

이러한 논리 연산자는 성공(&&) 또는 실패(||)에 따라 명령을 연결하는 간결한 방법을 제공합니다.

  • command1 && command2: command1이 성공한 경우에만 command2를 실행합니다.
  • command1 || command2: command1이 실패한 경우에만 command2를 실행합니다.
# 예: 디렉토리 생성 AND 파일 복사, 둘 중 하나라도 실패하면 실패
mkdir logs && cp /var/log/syslog logs/system.log

# 예: 백업 시도, OR 백업 실패 시 오류 기록 및 종료
pg_dump database > backup.sql || { echo "백업 실패!" >&2; exit 10; }

고급 전략 3: trap으로 보장된 정리

스크립트가 임시 파일, 잠금 파일 또는 설정된 네트워크 연결을 처리할 때, 갑작스러운 종료(성공 또는 오류로 인한)는 시스템을 일관되지 않은 상태로 남길 수 있습니다. trap 명령을 사용하면 스크립트가 특정 신호를 수신할 때 실행될 명령이나 함수를 정의할 수 있습니다.

EXIT 신호

EXIT 신호는 일반적인 정리에 가장 유용합니다. 트랩된 명령은 스크립트가 종료될 때마다 실행되며, 종료가 성공적이든, 수동 exit 호출이든, set -e에 의해 트리거된 종료이든 관계없이 실행됩니다.

#!/bin/bash

TEMP_DIR=$(mktemp -d)

# 정리 함수 정의
cleanup() {
    EXIT_CODE=$?
    echo "임시 디렉토리 정리 중: ${TEMP_DIR}"
    rm -rf "$TEMP_DIR"
    # 스크립트가 실패로 종료된 경우, 실패 코드 복원
    if [ $EXIT_CODE -ne 0 ]; then
        exit $EXIT_CODE
    fi
}

# 트랩 설정: 스크립트 종료 시 'cleanup' 함수 실행
trap cleanup EXIT

# --- 메인 스크립트 로직 ---

echo "${TEMP_DIR}에서 데이터 처리 중"

# 성공적인 작업 시뮬레이션...
# ... 스크립트 계속 ...

# set -e를 트리거하는 중요한 실패 시뮬레이션
false

# 이 줄은 도달할 수 없지만, 정리는 여전히 실행됩니다.
echo "완료."

특정 신호 처리 (TERM, INT)

사용자나 스케줄러가 작업을 취소할 때 정상 종료를 보장하기 위해 TERM(종료 요청) 또는 INT(인터럽트, 종종 Ctrl+C)와 같은 특정 종료 신호를 트랩할 수도 있습니다.

trap 'echo "사용자가 스크립트를 중단했습니다(Ctrl+C). 정리 중단." >&2; exit 130' INT

전략 4: 사용자 정의 오류 보고 및 로깅

전문적인 스크립트는 전용 오류 함수를 사용하여 보고를 중앙 집중화하고, 일관성과 적절한 출력 채널을 보장해야 합니다.

표준 오류로 오류 리디렉션 (>&2)

오류 메시지는 항상 표준 오류(stderr 또는 파일 디스크립터 2)로 출력되어야 하며, 표준 출력(stdout 또는 파일 디스크립터 1)은 데이터나 성공적인 결과를 위해 깨끗하게 유지되어야 합니다.

die 함수 패턴

종종 die 또는 error_exit라는 함수를 만들어 메시지 로깅, 정리(트랩을 사용하지 않는 경우), 지정된 코드로 종료를 처리합니다.

# 오류 메시지를 출력하고 종료하는 함수
die() {
    local msg=$1
    local code=${2:-1}
    echo "$(date +'%Y-%m-%d %H:%M:%S') [FATAL]: ${msg}" >&2
    exit "$code"
}

# 사용 예:

REQUIRED_VAR="$1"

if [ -z "$REQUIRED_VAR" ]; then
    die "필수 인수(데이터베이스 이름)가 누락되었습니다." 3
fi

# ... 스크립트 후반 ...

if ! validate_checksum "$FILE"; then
    die "$FILE의 체크섬 검증 실패." 5
fi

실패를 지루하게 만드세요

안정적인 Bash 스크립트 오류 처리를 위해, 각 중요하지 않은 스크립트를 set -euo pipefail로 시작하고, 명령이 실패할 것으로 예상되는 곳에서는 if ! command; then ...; fi를 사용하며, 오류를 stderr로 보내십시오. 스크립트가 임시 파일, 잠금 파일 또는 부분 출력을 생성하는 경우, 위험한 작업을 시작하기 전에 trap cleanup EXIT를 추가하십시오.

이 조합은 작은 자동화 작업을 예측 가능하게 유지하고 프로덕션 실패를 진단하기 쉽게 만듭니다.