종료 코드 이해하기: $?와 exit를 활용한 효과적인 오류 처리

Bash 종료 코드, $?, exit, set -e, pipefail을 사용하여 스크립트 실패를 명확하고 통제된 방식으로 처리하세요.

종료 코드 이해하기: $?와 exit를 활용한 효과적인 오류 처리

Bash 스크립트가 실패하면 종료 코드는 호출자에게 다음에 무엇을 해야 하는지 알려줍니다: 계속, 재시도, 경고, 또는 중지. 종료 코드, $?, exit를 이해하는 것은 실패를 숨기는 자동화와 명확하게 보고하는 자동화의 차이를 만듭니다.

이 가이드는 Bash가 명령 상태를 추적하는 방법과 이 상태를 간단하고 신뢰할 수 있는 오류 처리에 사용하는 방법을 보여줍니다.

종료 상태의 개념

Unix 계열 셸 환경에서 실행되는 모든 명령이나 프로그램(내장 명령어 cd, 외부 유틸리티 grep, 다른 셸 스크립트 등)은 완료 시 정수 값을 반환합니다. 이 정수는 종료 코드로, 작업의 결과를 호출 프로세스에 알립니다.

표준 규칙

종료 코드에 대한 규칙은 보편적으로 인정됩니다:

  • 0 (영): 성공을 의미합니다. 명령이 정확히 예상대로 실행되었으며 오류가 발생하지 않았습니다.
  • 1 ~ 255: 실패 또는 특정 오류 조건을 의미합니다. 0이 아닌 값은 무언가 잘못되었음을 나타냅니다. 높은 숫자는 종종 특정 유형의 오류(예: 파일 없음, 권한 거부, 구문 오류)에 해당하지만, 정확한 의미는 특정 프로그램에 따라 다릅니다.

범위 참고: 종료 코드는 기술적으로 8비트 값(0-255)이지만, 셸 스크립트는 일반적으로 성공을 위한 0과 실패를 위한 0이 아닌 값에만 관심을 둡니다. 255보다 큰 종료 코드는 일반적으로 셸에 의해 잘리거나 모듈로 256으로 해석됩니다.

마지막 종료 코드 검사: $? 변수

특수 셸 변수 $?(달러 물음표)는 명령 상태를 모니터링하는 데 핵심적입니다. 명령이 실행된 직후, 셸은 종료 코드를 $?에 저장합니다.

$? 사용 방법

관심 있는 명령 직후$?를 확인해야 합니다. 이후의 명령(변수를 출력하는 것조차)은 그 값을 덮어쓰기 때문입니다.

예제 1: 성공과 실패 확인

# 1. 성공적인 명령
echo "Success test" > /dev/null
echo "성공 시 종료 코드: $?"

# 2. 실패하는 명령(예: 존재하지 않는 파일 나열)
ls /non/existent/path
echo "실패 시 종료 코드: $?"

예상 출력:

성공 시 종료 코드: 0
ls: cannot access '/non/existent/path': No such file or directory
실패 시 종료 코드: 2

조건부 오류 확인 구현

단순히 종료 코드를 아는 것만으로는 충분하지 않습니다; 이 정보를 사용하여 스크립트 흐름을 제어하는 데 힘이 있습니다. 이는 일반적으로 if 문이나 단락 연산자(&&||)를 사용하여 수행됩니다.

if 문 사용

이것은 오류를 처리하는 가장 명시적인 방법입니다:

if grep -q "important data" logfile.txt;
then
    echo "데이터를 성공적으로 찾았습니다."
else
    LAST_STATUS=$?
    echo "오류: Grep이 상태 $LAST_STATUS로 실패했습니다. 데이터를 찾을 수 없습니다."
    # 스크립트가 진행될 수 없으면 여기서 종료하는 것을 고려하세요
fi

위 예제에서 grep -q는 출력을 억제하고(-q) 일치 항목이 발견된 경우에만 0을 반환합니다. if 구조는 자동으로 종료 상태를 확인하지만, else 블록 내에서 $?를 명시적으로 캡처하는 것은 상세한 로깅에 유용합니다.

단락 논리 사용 (&&||)

간단한 순차적 확인의 경우, 단락 연산자는 간결한 오류 처리를 제공합니다:

  • && (AND): && 뒤의 명령은 앞의 명령이 성공(0 반환)한 경우에만 실행됩니다.
  • || (OR): || 뒤의 명령은 앞의 명령이 실패(0이 아닌 값 반환)한 경우에만 실행됩니다.

예제 2: 간결한 오류 처리

# 1. 'fetch_data'가 성공한 경우에만 'process_data' 실행
fetch_data.sh && ./process_data.sh

# 2. 기본 작업이 실패한 경우에만 'send_alert' 실행
rsync -a source/ dest/ || echo "RSync가 $(date)에 실패했습니다" >> /var/log/rsync_errors.log

exit로 스크립트 종료 제어

exit 명령은 현재 셸 스크립트나 함수를 즉시 종료하고 호출자(다른 스크립트나 사용자 터미널일 수 있음)에게 지정된 종료 상태를 반환하는 데 사용됩니다.

구문 및 사용법

구문은 간단히 exit [status_code]입니다.

상태가 제공되지 않으면 exit는 가장 최근에 실행된 포그라운드 명령의 상태를 기본값으로 사용합니다. 먼저 명령을 실행하지 않고 exit 0을 명시적으로 호출하면 0을 반환합니다.

예제 3: 사전 조건 실패 시 종료

이 스크립트는 진행하기 전에 필수 구성 파일이 존재하는지 확인합니다.

CONFIG_FILE="/etc/app/config.conf"

if [[ ! -f "$CONFIG_FILE" ]]; then
    echo "오류: $CONFIG_FILE에서 구성 파일을 찾을 수 없습니다."
    # 특정 오류 코드(예: 20)로 스크립트 즉시 종료
    exit 20 
fi

echo "구성 로드됨. 스크립트 계속 중..."
# ... 나머지 스크립트
exit 0

모범 사례: 의미 있는 종료 코드 사용

01이 대부분의 기본 사례를 다루지만, 다른 0이 아닌 코드를 사용하면 호출 스크립트가 정확한 문제를 진단하는 데 도움이 됩니다:

코드 의미 (예시)
0 성공
1 일반적인 포괄 오류
2-10 구문 오류, 인수 구문 분석 문제
20 필수 조건 누락(예: 파일 없음)
30 권한 문제

스크립트를 빠르게 실패하게 만들기: set 명령

복잡한 스크립트에서 최대한의 신뢰성을 위해, 스크립트 상단에서 set 명령 옵션을 사용하여 전역적으로 오류 검사를 활성화하는 것이 강력한 모범 사례입니다:

#!/bin/bash

# 명령이 0이 아닌 상태로 종료되면 즉시 종료합니다.
set -e

# 대체 시 설정되지 않은 변수를 오류로 처리합니다.
set -u

# Pipefail: 파이프라인의 반환 상태가 0이 아닌 상태로 종료된 가장 오른쪽 명령의 상태가 되도록 합니다.
set -o pipefail

# (선택 사항이지만 유용함) 디버깅을 위해 명령이 실행될 때 출력
# set -x 

# 아래 명령이 실패하면 스크립트가 즉시 중지됩니다.
ls /valid/path && grep pattern file.txt && ./next_step.sh

# 다음 줄은 앞의 모든 명령이 성공한 경우에만 실행됩니다.
echo "모든 단계 완료."

set -e가 활성화되면, 처리되지 않은 많은 0이 아닌 상태가 나쁜 가정 하에 이후 명령이 실행되기 전에 스크립트를 중지시킵니다. 조건문, 파이프라인 및 복합 명령에서는 예외가 있으므로 예상되는 실패는 여전히 명시적으로 처리해야 합니다.

예를 들어, grep은 일치 항목을 찾지 못하면 1을 반환합니다. 이는 치명적인 오류가 아닌 정상적인 결과일 수 있습니다:

if grep -q "READY" status.txt; then
    echo "서비스가 준비되었습니다."
else
    echo "서비스가 아직 준비되지 않았습니다."
fi

핵심 요점

중요한 명령이 실행되는 곳에서 확인하고, 오류를 stderr에 기록하며, 스크립트가 안전하게 계속될 수 없을 때 0이 아닌 상태로 종료하세요. 빠른 실패 스크립트를 위해 set -euo pipefail을 사용하되, 이것만이 유일한 오류 처리 전략이라고 의존하지 마세요.