이미지 로딩 중...

Bash 디버깅과 에러 핸들링 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 18. · 3 Views

Bash 디버깅과 에러 핸들링 완벽 가이드

Bash 스크립트의 디버깅 기법과 에러 핸들링 전략을 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. set 옵션부터 trap, ShellCheck까지 실무에서 바로 활용할 수 있는 기법들을 단계별로 배워보세요.


목차

  1. set -e, set -u, set -x 옵션
  2. trap을 이용한 시그널 처리
  3. 에러 핸들링 패턴
  4. 로깅 전략
  5. ShellCheck 도구 활용
  6. 디버깅 모드 (bash -x)

1. set -e, set -u, set -x 옵션

시작하며

여러분이 Bash 스크립트를 작성할 때 이런 상황을 겪어본 적 있나요? 스크립트 중간에 에러가 발생했는데도 계속 실행되어 결국 시스템이 엉망이 되거나, 정의하지 않은 변수를 사용해서 예상치 못한 결과가 나온 적이요.

이런 문제는 실제 개발 현장에서 정말 자주 발생합니다. 특히 자동화 스크립트나 배포 스크립트에서 이런 일이 생기면 큰 사고로 이어질 수 있죠.

에러가 발생했는데도 계속 실행되면 데이터가 손실되거나 잘못된 설정이 적용될 수 있습니다. 바로 이럴 때 필요한 것이 set 옵션들입니다.

set -e, set -u, set -x는 마치 스크립트에 안전벨트를 매워주는 것과 같아요. 에러가 발생하면 즉시 멈추고, 변수 실수를 방지하며, 무슨 일이 일어나는지 눈으로 확인할 수 있게 해줍니다.

개요

간단히 말해서, 이 옵션들은 Bash 스크립트를 더 안전하고 디버깅하기 쉽게 만들어주는 강력한 도구입니다. 왜 이 옵션들이 필요한지 실무 관점에서 설명하자면, 스크립트가 예상치 못한 방식으로 동작하는 것을 미리 방지할 수 있기 때문입니다.

예를 들어, 데이터베이스 백업 스크립트에서 백업 파일 생성이 실패했는데도 계속 실행되어 이전 백업을 삭제해버리는 경우를 막을 수 있습니다. 기존에는 모든 명령어마다 일일이 에러 체크 코드를 작성해야 했다면, 이제는 스크립트 시작 부분에 몇 줄만 추가하면 자동으로 안전장치가 작동합니다.

set -e는 명령어가 실패하면 즉시 스크립트를 중단시킵니다. set -u는 정의되지 않은 변수를 사용하면 에러를 발생시킵니다.

set -x는 실행되는 모든 명령어를 화면에 출력하여 디버깅을 쉽게 해줍니다. 이러한 특징들이 스크립트의 안정성과 유지보수성을 크게 향상시킵니다.

코드 예제

#!/bin/bash

# 에러가 발생하면 즉시 스크립트 중단
set -e
# 정의되지 않은 변수 사용 시 에러 발생
set -u
# 실행되는 명령어를 출력 (디버깅용)
set -x

# 또는 한 줄로 작성 가능
# set -eux

# 예제: 파일 백업 스크립트
SOURCE_FILE="/data/important.txt"
BACKUP_DIR="/backup"

# 백업 디렉토리가 없으면 생성 (실패하면 여기서 중단)
mkdir -p "$BACKUP_DIR"

# 파일 복사 (실패하면 여기서 중단)
cp "$SOURCE_FILE" "$BACKUP_DIR/"

echo "백업 완료!"

설명

이것이 하는 일: set 옵션들은 Bash 쉘의 동작 방식을 변경하여 스크립트를 더 안전하게 만들어줍니다. 첫 번째로, set -e는 "exit on error"의 약자로 어떤 명령어가 0이 아닌 종료 코드를 반환하면 (즉, 실패하면) 스크립트를 즉시 종료시킵니다.

왜 이렇게 하는지 이해하려면, 일상생활의 예를 들어볼게요. 케이크를 만드는데 밀가루가 없다면 계속 진행하지 않고 멈추는 것이 맞겠죠?

마찬가지로 스크립트도 중요한 단계가 실패하면 멈춰야 합니다. 그 다음으로, set -u는 "unset variable error"를 의미하며, 정의되지 않은 변수를 사용하려고 하면 에러를 발생시킵니다.

예를 들어 $USRNAME이라고 오타를 냈는데 (정확한 변수명은 $USERNAME) 이것을 그냥 빈 문자열로 처리하면 예상치 못한 동작이 발생할 수 있습니다. set -u를 사용하면 이런 실수를 즉시 잡아낼 수 있습니다.

마지막으로, set -x는 "xtrace" 모드로, 실행되는 모든 명령어 앞에 + 기호와 함께 출력합니다. 스크립트가 어떤 순서로 실행되는지, 변수가 어떤 값으로 치환되는지 눈으로 확인할 수 있어서 디버깅할 때 정말 유용합니다.

마치 요리할 때 각 단계를 소리내어 말하면서 하는 것과 비슷하죠. 여러분이 이 옵션들을 사용하면 스크립트 에러를 조기에 발견할 수 있고, 예상치 못한 동작을 방지하며, 문제가 발생했을 때 원인을 빠르게 파악할 수 있습니다.

실무에서는 특히 자동화된 배포 스크립트나 크론 작업에서 이러한 안전장치가 매우 중요합니다.

실전 팁

💡 프로덕션 스크립트에는 항상 set -eu를 사용하되, set -x는 개발/디버깅 시에만 사용하세요. set -x는 민감한 정보(비밀번호 등)도 로그에 출력되므로 주의가 필요합니다.

💡 특정 명령어가 실패해도 계속 진행하고 싶다면 || true를 붙이세요. 예: grep "pattern" file.txt || true

💡 set +e로 일시적으로 에러 체크를 끄고, 작업 후 다시 set -e로 켤 수 있습니다. 에러가 예상되는 구간에서 유용합니다.

💡 set -o pipefail을 추가하면 파이프라인 중 하나라도 실패하면 전체가 실패로 처리됩니다. 예: set -euo pipefail

💡 ${변수:-기본값} 문법을 사용하면 set -u 환경에서도 안전하게 기본값을 설정할 수 있습니다.


2. trap을 이용한 시그널 처리

시작하며

여러분이 스크립트를 실행하다가 Ctrl+C를 눌러서 중단했을 때, 임시 파일이 그대로 남아있거나 잠금 파일이 삭제되지 않아서 다음 실행이 안 되는 경험을 해보셨나요? 이런 문제는 스크립트가 갑자기 종료될 때 정리(cleanup) 작업을 하지 못해서 발생합니다.

특히 장시간 실행되는 스크립트나 중요한 자원을 사용하는 스크립트에서는 이런 상황이 심각한 문제를 일으킬 수 있습니다. 데이터베이스 연결이 끊기지 않거나, 임시 파일이 디스크를 가득 채우거나, 프로세스가 좀비 상태로 남을 수 있죠.

바로 이럴 때 필요한 것이 trap 명령어입니다. trap은 마치 "비상 탈출구"와 같아서, 스크립트가 어떤 이유로든 종료될 때 반드시 실행해야 할 정리 코드를 등록할 수 있게 해줍니다.

개요

간단히 말해서, trap은 시그널(신호)이나 특정 이벤트가 발생했을 때 실행할 명령어를 지정하는 도구입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 스크립트가 정상적으로 종료되든 강제로 중단되든 항상 깔끔하게 뒷정리를 할 수 있기 때문입니다.

예를 들어, 대용량 파일 처리 스크립트에서 사용자가 실수로 터미널을 닫았을 때도 임시 파일을 자동으로 삭제하고 로그를 기록할 수 있습니다. 기존에는 스크립트가 중간에 중단되면 정리 작업을 할 방법이 없었다면, 이제는 trap을 사용해서 "어떤 상황에서도 이 코드는 반드시 실행"하도록 보장할 수 있습니다.

trap의 핵심 특징은 세 가지입니다. 첫째, EXIT 시그널로 스크립트 종료 시 항상 실행할 코드를 지정할 수 있습니다.

둘째, INT (Ctrl+C), TERM (kill 명령) 등 다양한 시그널을 잡아낼 수 있습니다. 셋째, ERR 시그널로 에러 발생 시 특별한 처리를 할 수 있습니다.

이러한 특징들이 스크립트를 훨씬 더 견고하고 신뢰할 수 있게 만들어줍니다.

코드 예제

#!/bin/bash
set -eu

TEMP_FILE=$(mktemp)
LOCK_FILE="/tmp/myscript.lock"

# 정리 함수 정의
cleanup() {
    echo "정리 작업을 시작합니다..."
    # 임시 파일 삭제
    rm -f "$TEMP_FILE"
    # 락 파일 삭제
    rm -f "$LOCK_FILE"
    echo "정리 완료!"
}

# EXIT 시그널에 정리 함수 등록 (스크립트 종료 시 항상 실행)
trap cleanup EXIT

# INT 시그널(Ctrl+C)에 대한 별도 처리
trap 'echo "중단되었습니다!"; exit 130' INT

# 락 파일 생성
touch "$LOCK_FILE"

# 메인 작업
echo "작업 시작..."
sleep 10  # 여기서 Ctrl+C를 눌러보세요
echo "작업 완료!"

설명

이것이 하는 일: trap은 특정 시그널이 발생했을 때 실행할 명령어를 미리 등록해두는 기능입니다. 첫 번째로, cleanup이라는 함수를 정의합니다.

이 함수 안에는 스크립트가 종료될 때 반드시 해야 할 작업들을 넣어둡니다. 왜 함수로 만드는지 궁금하시죠?

여러 곳에서 같은 정리 작업을 해야 할 때 코드 중복을 피하고, 코드를 깔끔하게 관리하기 위해서입니다. 마치 집을 나갈 때 "문 잠그기, 불 끄기, 가스 잠그기"를 체크리스트로 만들어두는 것과 같습니다.

그 다음으로, trap cleanup EXIT 명령어로 EXIT 시그널에 cleanup 함수를 등록합니다. EXIT는 특별한 시그널로, 스크립트가 정상 종료되든 에러로 종료되든 항상 발생합니다.

이렇게 등록해두면 스크립트가 어떻게 끝나든 cleanup 함수가 실행되어 임시 파일과 락 파일을 깔끔하게 삭제해줍니다. 세 번째로, trap 'echo "중단되었습니다!"; exit 130' INT로 INT 시그널(Ctrl+C)에 대한 별도 처리를 등록합니다.

사용자가 Ctrl+C를 누르면 "중단되었습니다!"라는 메시지를 출력하고 종료 코드 130으로 종료합니다. 여기서 중요한 점은, exit가 호출되면 EXIT 시그널도 발생하므로 cleanup 함수도 함께 실행된다는 것입니다.

마지막으로, 스크립트의 메인 작업이 실행됩니다. 여기서 sleep 10 중에 Ctrl+C를 누르면, INT 핸들러가 먼저 실행되고, 그 다음 EXIT 핸들러(cleanup)가 실행되어 모든 자원이 깔끔하게 정리됩니다.

여러분이 trap을 사용하면 스크립트가 예상치 못하게 종료되어도 자원 누수를 방지할 수 있고, 항상 일관된 상태를 유지할 수 있으며, 디버깅할 때도 종료 시점의 상태를 로그로 남길 수 있습니다. 실무에서는 데이터베이스 연결 종료, 임시 파일 삭제, 락 해제 등에 필수적으로 사용됩니다.

실전 팁

💡 trap의 시그널 목록은 kill -l 명령어로 확인할 수 있습니다. 가장 많이 사용되는 것은 EXIT, INT, TERM, ERR입니다.

💡 trap - SIGNAL 형태로 시그널 핸들러를 제거할 수 있습니다. 예: trap - EXIT

💡 set -E와 함께 trap 'error_handler' ERR을 사용하면 모든 에러를 중앙에서 처리할 수 있습니다. 에러 로깅에 매우 유용합니다.

💡 trap 명령어를 인자 없이 실행하면 현재 등록된 모든 trap 목록을 확인할 수 있어서 디버깅 시 유용합니다.

💡 서브쉘에서는 trap이 상속되지 않으므로, 백그라운드 작업(&)이나 파이프라인에서는 별도로 trap을 설정해야 합니다.


3. 에러 핸들링 패턴

시작하며

여러분이 복잡한 스크립트를 작성할 때 이런 고민을 해본 적 있나요? "이 명령어가 실패하면 어떻게 하지?", "에러 메시지를 어떻게 사용자에게 보여주지?", "실패한 부분부터 다시 시작할 수 있을까?" 이런 문제는 스크립트가 복잡해질수록 더 심각해집니다.

에러 처리를 제대로 하지 않으면 스크립트가 중간에 멈춰서 뭐가 잘못됐는지도 모르는 상황이 발생하거나, 반대로 에러를 무시하고 계속 진행해서 더 큰 문제를 일으킬 수 있습니다. 사용자는 무슨 일이 일어났는지 알 수 없어서 답답해하고, 개발자는 로그를 뒤져가며 원인을 찾느라 시간을 낭비하게 됩니다.

바로 이럴 때 필요한 것이 체계적인 에러 핸들링 패턴입니다. 에러를 예측하고, 적절하게 처리하고, 명확한 메시지를 제공하는 패턴을 익히면 스크립트가 훨씬 더 견고하고 사용자 친화적으로 변합니다.

개요

간단히 말해서, 에러 핸들링 패턴은 스크립트에서 발생할 수 있는 다양한 에러 상황을 체계적으로 처리하는 방법론입니다. 왜 이 패턴들이 필요한지 실무 관점에서 설명하자면, 프로덕션 환경에서는 예상치 못한 일이 항상 일어나기 때문입니다.

네트워크가 끊길 수도 있고, 디스크가 가득 찼을 수도 있고, 필요한 파일이 없을 수도 있습니다. 예를 들어, 서버 배포 스크립트에서 파일 다운로드가 실패했을 때 적절한 에러 메시지를 보여주고, 재시도 로직을 실행하고, 최종적으로 실패하면 롤백하는 것이 필요합니다.

기존에는 각 명령어마다 if문으로 체크하는 번거로운 방식을 사용했다면, 이제는 검증된 패턴을 활용해서 일관되고 효율적으로 에러를 처리할 수 있습니다. 에러 핸들링의 핵심 패턴은 네 가지입니다.

첫째, 종료 코드($?)를 체크하여 명령어 성공 여부를 확인합니다. 둘째, 에러 메시지를 stderr로 출력하여 정상 출력과 구분합니다.

셋째, 의미 있는 종료 코드를 반환하여 스크립트를 호출한 쪽에서 에러 타입을 알 수 있게 합니다. 넷째, 재시도 로직을 구현하여 일시적인 에러를 극복합니다.

이러한 패턴들이 스크립트의 신뢰성과 유지보수성을 크게 높여줍니다.

코드 예제

#!/bin/bash
set -eu

# 에러 메시지를 stderr로 출력하는 함수
error() {
    echo "[ERROR] $*" >&2
}

# 명령어 실행 후 결과 체크
check_command() {
    if "$@"; then
        echo "[SUCCESS] $1 completed"
        return 0
    else
        error "$1 failed with exit code $?"
        return 1
    fi
}

# 재시도 로직
retry() {
    local max_attempts=3
    local attempt=1

    while [ $attempt -le $max_attempts ]; do
        echo "시도 $attempt/$max_attempts..."
        if "$@"; then
            echo "성공!"
            return 0
        fi
        echo "실패. 3초 후 재시도..."
        sleep 3
        ((attempt++))
    done

    error "최대 재시도 횟수 초과"
    return 1
}

# 사용 예제
if check_command mkdir -p /tmp/test; then
    echo "디렉토리 생성 완료"
fi

retry curl -f https://example.com/file.txt -o /tmp/file.txt

설명

이것이 하는 일: 에러 핸들링 패턴은 스크립트에서 발생하는 다양한 에러 상황을 일관되게 처리하고 사용자에게 명확한 정보를 제공합니다. 첫 번째로, error 함수는 에러 메시지를 표준 에러(stderr)로 출력합니다.

&2는 출력을 stderr로 리다이렉트하는 문법인데, 왜 이렇게 하는지 설명할게요. 일상생활에서 좋은 소식과 나쁜 소식을 구분해서 전달하듯이, 프로그램도 정상 출력(stdout)과 에러 메시지(stderr)를 구분합니다.

이렇게 하면 스크립트를 파이프라인으로 연결할 때 에러 메시지가 정상 데이터를 오염시키지 않습니다. 그 다음으로, check_command 함수는 명령어를 실행하고 결과를 체크합니다.

"$@"는 함수에 전달된 모든 인자를 의미하며, if "$@"는 그 명령어를 실행하고 성공(종료 코드 0)이면 true, 실패면 false가 됩니다. 성공하면 성공 메시지를 출력하고 0을 반환하며, 실패하면 에러 메시지를 stderr로 출력하고 1을 반환합니다.

이렇게 함수로 만들어두면 스크립트 전체에서 일관된 방식으로 명령어 결과를 처리할 수 있습니다. 세 번째로, retry 함수는 명령어가 실패하면 자동으로 재시도하는 로직을 구현합니다.

max_attempts는 최대 시도 횟수를, attempt는 현재 시도 횟수를 추적합니다. while 루프 안에서 명령어를 실행하고, 성공하면 즉시 0을 반환하여 루프를 빠져나갑니다.

실패하면 3초를 기다린 후 다시 시도합니다. ((attempt++))는 시도 횟수를 1 증가시키는 산술 연산입니다.

최대 횟수를 초과하면 에러 메시지를 출력하고 1을 반환합니다. 마지막으로, 실제 사용 예제에서 check_command로 디렉토리 생성을 체크하고, retry로 네트워크 다운로드를 재시도합니다.

curl의 -f 옵션은 HTTP 에러 시 실패 코드를 반환하게 만들어서 에러 핸들링이 제대로 작동하도록 합니다. 여러분이 이러한 패턴을 사용하면 에러를 조기에 발견하고 적절하게 대응할 수 있으며, 사용자에게 명확한 피드백을 제공할 수 있고, 일시적인 네트워크 문제 등을 자동으로 극복할 수 있습니다.

실무에서는 이런 패턴들을 라이브러리처럼 만들어두고 여러 스크립트에서 재사용합니다.

실전 팁

💡 종료 코드는 0-255 범위를 가지며, 의미를 부여해서 사용하세요. 예: 1=일반 에러, 2=잘못된 사용법, 126=실행 불가, 127=명령어 없음, 130=Ctrl+C 중단

💡 set -o pipefail을 사용하면 파이프라인 전체의 에러를 잡을 수 있습니다. 예: curl ... | jq ... 에서 curl이 실패해도 감지 가능

💡 ${PIPESTATUS[@]} 배열로 파이프라인의 각 명령어 종료 코드를 개별적으로 확인할 수 있습니다.

💡 timeout 명령어로 명령어 실행 시간을 제한하세요. 예: timeout 30s curl ... (30초 후 자동 중단)

💡 에러 메시지에는 항상 타임스탬프와 컨텍스트 정보를 포함하세요. 예: echo "[ERROR $(date '+%Y-%m-%d %H:%M:%S')] Failed to process $filename" >&2


4. 로깅 전략

시작하며

여러분이 스크립트를 실행했는데 뭔가 이상하게 동작할 때, "아, 중간에 무슨 일이 있었는지 로그를 남겨뒀으면 좋았을 텐데..." 하고 후회한 적 있나요? 이런 문제는 특히 크론잡이나 백그라운드로 실행되는 스크립트에서 심각합니다.

터미널에 출력되는 메시지를 볼 수 없으니 문제가 생겨도 원인을 파악하기가 정말 어렵습니다. 더구나 며칠 전에 실행된 스크립트의 상황을 알아내려면 로그가 필수적입니다.

로그가 없으면 마치 블랙박스처럼 무슨 일이 일어났는지 전혀 알 수 없게 됩니다. 바로 이럴 때 필요한 것이 체계적인 로깅 전략입니다.

언제, 무엇을, 어떻게 로그로 남길지 일관된 규칙을 만들어두면 문제 해결이 훨씬 쉬워지고, 스크립트 실행 이력을 추적할 수 있으며, 성능 분석도 가능해집니다.

개요

간단히 말해서, 로깅 전략은 스크립트의 실행 과정과 결과를 체계적으로 기록하여 나중에 분석하고 디버깅할 수 있게 만드는 방법론입니다. 왜 이 전략이 필요한지 실무 관점에서 설명하자면, 프로덕션 환경에서는 눈앞에서 직접 스크립트를 실행하지 않기 때문입니다.

예를 들어, 매일 새벽 2시에 실행되는 데이터 백업 스크립트가 실패했다면, 로그를 보고 정확히 어느 단계에서 왜 실패했는지 파악할 수 있어야 합니다. 로그가 없으면 문제를 재현하기 위해 다음날 새벽까지 기다려야 할 수도 있습니다.

기존에는 echo로 메시지를 출력하거나 파일 리다이렉트(>>)를 직접 사용하는 방식이었다면, 이제는 로그 레벨(INFO, WARN, ERROR), 타임스탬프, 로그 로테이션 등을 포함한 체계적인 시스템을 만들 수 있습니다. 로깅 전략의 핵심 요소는 다섯 가지입니다.

첫째, 로그 레벨을 구분하여 메시지의 중요도를 표시합니다. 둘째, 모든 로그에 타임스탬프를 포함하여 시간 순서를 파악할 수 있게 합니다.

셋째, 로그 파일을 적절히 관리하여 디스크를 가득 채우지 않도록 합니다. 넷째, 화면과 파일에 동시에 로그를 출력하여 실시간 모니터링과 이력 추적을 모두 가능하게 합니다.

다섯째, 민감한 정보는 로그에 남기지 않아 보안을 유지합니다. 이러한 요소들이 스크립트를 프로덕션 수준으로 끌어올려줍니다.

코드 예제

#!/bin/bash
set -eu

# 로그 파일 설정
LOG_DIR="/var/log/myscript"
LOG_FILE="$LOG_DIR/script_$(date +%Y%m%d).log"
mkdir -p "$LOG_DIR"

# 로그 함수들
log() {
    local level=$1
    shift
    local message="$*"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}

log_info() {
    log "INFO" "$@"
}

log_warn() {
    log "WARN" "$@" >&2
}

log_error() {
    log "ERROR" "$@" >&2
}

# 실행 시간 측정
start_time=$(date +%s)

log_info "스크립트 시작"
log_info "처리할 파일: data.txt"

# 작업 수행
if [ -f "data.txt" ]; then
    log_info "파일 처리 중..."
    # 실제 작업
else
    log_error "파일을 찾을 수 없습니다: data.txt"
    exit 1
fi

# 실행 시간 계산
end_time=$(date +%s)
duration=$((end_time - start_time))
log_info "스크립트 완료 (실행 시간: ${duration}초)"

# 오래된 로그 정리 (30일 이상)
find "$LOG_DIR" -name "script_*.log" -mtime +30 -delete

설명

이것이 하는 일: 로깅 시스템은 스크립트의 모든 중요한 이벤트를 시간 순서대로 기록하여 나중에 분석할 수 있게 만듭니다. 첫 번째로, 로그 파일 경로를 설정합니다.

LOG_FILE="$LOG_DIR/script_$(date +%Y%m%d).log"는 날짜별로 로그 파일을 생성하는 패턴입니다. 왜 날짜별로 나누는지 궁금하시죠?

하나의 파일에 모든 로그를 쌓으면 파일이 너무 커져서 열기도 어렵고 관리도 힘듭니다. 날짜별로 나누면 특정 날짜의 로그만 쉽게 찾을 수 있고, 오래된 로그는 삭제하기도 편합니다.

마치 일기장을 날짜별로 정리하는 것과 같은 이치입니다. 그 다음으로, log 함수는 모든 로그의 기본 형식을 정의합니다.

level 파라미터로 로그 레벨을, shift로 나머지 인자들을 message로 받습니다. date '+%Y-%m-%d %H:%M:%S'는 "2024-01-15 14:30:25" 같은 형식의 타임스탬프를 생성합니다.

tee -a는 정말 유용한 명령어인데, 표준 입력을 화면에도 출력하고(-a 옵션으로) 파일에도 추가합니다. 이렇게 하면 스크립트를 실행할 때 실시간으로 진행 상황을 볼 수 있으면서도 로그 파일에도 자동으로 기록됩니다.

세 번째로, log_info, log_warn, log_error 같은 편의 함수들을 만듭니다. 이 함수들은 log 함수를 호출하되 로그 레벨을 미리 지정해줍니다.

log_warn과 log_error는 >&2를 추가하여 stderr로도 출력되게 만들어서, 에러 로그를 별도로 수집할 수 있게 합니다. 이렇게 함수로 만들어두면 스크립트 전체에서 log_info "메시지" 형태로 간단하게 로그를 남길 수 있습니다.

네 번째로, 실행 시간 측정 로직입니다. date +%s는 유닉스 타임스탬프(1970년 이후 초)를 반환하는데, 시작 시간과 종료 시간의 차이를 계산하면 실행 시간을 초 단위로 알 수 있습니다.

이 정보는 성능 분석이나 이상 징후 탐지에 매우 유용합니다. 평소 10초 걸리던 스크립트가 갑자기 5분 걸린다면 뭔가 문제가 있다는 신호입니다.

마지막으로, find "$LOG_DIR" -name "script_*.log" -mtime +30 -delete로 30일 이상 된 로그 파일을 자동으로 삭제합니다. -mtime +30은 "수정된 지 30일 초과"를 의미합니다.

이런 로그 로테이션 로직이 없으면 로그 파일이 디스크를 가득 채워서 시스템 전체에 문제를 일으킬 수 있습니다. 여러분이 이러한 로깅 시스템을 사용하면 문제가 발생했을 때 빠르게 원인을 파악할 수 있고, 스크립트 실행 이력을 추적할 수 있으며, 성능 저하를 조기에 발견할 수 있습니다.

실무에서는 이런 로그를 중앙 로그 수집 시스템(예: ELK 스택)으로 보내서 대시보드로 모니터링하기도 합니다.

실전 팁

💡 logger 명령어를 사용하면 시스템 syslog에도 로그를 보낼 수 있습니다. 예: logger -t myscript "중요한 이벤트 발생"

💡 로그에는 절대 비밀번호, API 키, 개인정보를 남기지 마세요. 변수를 로그에 찍기 전에 민감 정보인지 확인하세요.

💡 JSON 형식으로 로그를 남기면 나중에 파싱하기 쉽습니다. 예: log_info '{"action":"backup","status":"success","duration":120}'

💡 exec 1> >(tee -a "$LOG_FILE") 2>&1를 스크립트 시작 부분에 추가하면 모든 출력이 자동으로 로그 파일에 기록됩니다.

💡 logrotate 도구를 사용하면 더 정교한 로그 관리가 가능합니다. 크기 기반 로테이션, 압축, 이메일 알림 등을 설정할 수 있습니다.


5. ShellCheck 도구 활용

시작하며

여러분이 Bash 스크립트를 작성할 때 "이 문법이 맞나?", "변수를 제대로 인용했나?", "이 코드에 보안 문제는 없을까?" 같은 걱정을 해본 적 있나요? 이런 문제는 Bash의 복잡한 문법과 많은 함정들 때문에 발생합니다.

따옴표를 빠뜨려서 공백이 포함된 파일명을 제대로 처리하지 못한다거나, 변수를 잘못 사용해서 의도하지 않은 명령어가 실행되는 보안 문제가 생길 수 있습니다. 경험 많은 개발자도 이런 실수를 자주 하는데, 특히 코드 리뷰 없이 혼자 작업하면 버그를 놓치기 쉽습니다.

바로 이럴 때 필요한 것이 ShellCheck입니다. ShellCheck는 마치 맞춤법 검사기처럼 스크립트를 분석해서 잠재적인 버그, 문법 오류, 안티패턴을 자동으로 찾아주고, 어떻게 고쳐야 하는지까지 친절하게 알려줍니다.

개요

간단히 말해서, ShellCheck는 Bash 스크립트의 정적 분석 도구로, 코드를 실행하지 않고도 문제를 찾아내는 똑똑한 린터(linter)입니다. 왜 이 도구가 필요한지 실무 관점에서 설명하자면, Bash는 다른 프로그래밍 언어와 달리 컴파일 과정이 없어서 실행하기 전까지 오류를 발견하기 어렵기 때문입니다.

예를 들어, 프로덕션 서버에 배포한 스크립트가 특정 조건에서만 발생하는 버그 때문에 데이터를 삭제해버린다면 정말 큰 문제가 됩니다. ShellCheck를 사용하면 이런 문제를 배포 전에 미리 잡아낼 수 있습니다.

기존에는 스크립트를 실행해보고 문제가 생기면 그때 고치는 방식이었다면, 이제는 코드를 작성하는 즉시 자동으로 문제를 발견하고 수정할 수 있습니다. ShellCheck의 핵심 기능은 네 가지입니다.

첫째, 따옴표 누락, 변수 참조 오류 같은 일반적인 버그를 찾아냅니다. 둘째, 보안 취약점(명령어 인젝션 등)을 경고합니다.

셋째, 더 나은 코드를 작성할 수 있도록 개선 방안을 제안합니다. 넷째, 각 경고에 대해 상세한 설명과 예제를 제공하는 위키 페이지를 연결해줍니다.

이러한 기능들이 스크립트의 품질을 전문가 수준으로 끌어올려줍니다.

코드 예제

#!/bin/bash
# ShellCheck 사용 전 (문제 있는 코드)

# SC2086: 따옴표 없이 변수 사용 - 공백 문제 발생 가능
file=$1
cat $file

# SC2155: 선언과 할당 동시 사용 - 에러 체크 불가
local result=$(command_that_might_fail)

# SC2046: 명령어 치환 결과를 따옴표로 감싸지 않음
rm $(find . -name "*.tmp")

---

#!/bin/bash
# ShellCheck 사용 후 (수정된 코드)

# 변수를 따옴표로 감싸서 공백 처리
file=$1
cat "$file"

# 선언과 할당을 분리하여 에러 체크 가능
local result
result=$(command_that_might_fail)

# 명령어 치환 결과를 따옴표로 감싸기
# 또는 더 안전한 방법 사용
find . -name "*.tmp" -delete

# ShellCheck 경고 무시 (정당한 이유가 있을 때만)
# shellcheck disable=SC2086
echo $UNQUOTED_VAR

설명

이것이 하는 일: ShellCheck는 스크립트를 분석하여 잠재적인 문제를 발견하고 구체적인 수정 방법을 알려줍니다. 첫 번째로, 가장 흔한 문제인 따옴표 누락을 살펴보겠습니다.

cat $file처럼 변수를 따옴표 없이 사용하면 무슨 문제가 생길까요? 만약 file 변수가 "my document.txt"처럼 공백을 포함하면, Bash는 이것을 "my"와 "document.txt" 두 개의 파일로 해석합니다.

왜 이렇게 하는지는 Bash의 단어 분리(word splitting) 규칙 때문인데, 공백이나 탭으로 인자를 구분하기 때문입니다. cat "$file"처럼 따옴표로 감싸면 공백이 포함되어도 하나의 인자로 처리됩니다.

ShellCheck는 이런 문제를 SC2086 경고로 알려줍니다. 그 다음으로, 선언과 할당을 동시에 하는 패턴의 문제입니다.

local result=$(command_that_might_fail)처럼 작성하면, command_that_might_fail이 실패해도 result 변수는 선언에 성공했으므로 전체 명령어가 성공(종료 코드 0)으로 처리됩니다. 이렇게 되면 set -e를 사용해도 에러를 잡을 수 없습니다.

선언과 할당을 분리하면 할당 단계에서 실패를 감지할 수 있습니다. 세 번째로, rm $(find ...)는 매우 위험한 패턴입니다.

find 결과에 공백이나 특수문자가 포함된 파일명이 있으면 예상치 못한 파일을 삭제할 수 있습니다. 더 심각한 것은, find가 너무 많은 파일을 찾으면 "인자 목록이 너무 길다"는 에러가 발생할 수 있다는 것입니다.

find ... -delete나 find ...

-exec rm {} + 같은 안전한 방법을 사용해야 합니다. 네 번째로, ShellCheck는 때로는 특정 경고를 무시해야 할 때가 있습니다.

shellcheck disable=SC2086 주석을 사용하면 다음 줄의 해당 경고를 무시할 수 있습니다. 하지만 이것은 정말 필요할 때만 사용해야 하며, 왜 무시하는지 주석으로 이유를 남기는 것이 좋습니다.

여러분이 ShellCheck를 사용하면 코드 리뷰 전에 대부분의 문제를 미리 수정할 수 있고, Bash의 모범 사례를 자연스럽게 배울 수 있으며, 프로덕션 환경에서 발생할 수 있는 버그를 사전에 방지할 수 있습니다. 실무에서는 CI/CD 파이프라인에 ShellCheck를 통합하여 모든 스크립트가 자동으로 검사되도록 합니다.

실전 팁

💡 VS Code나 vim 같은 에디터에 ShellCheck 플러그인을 설치하면 코드를 작성하면서 실시간으로 경고를 볼 수 있습니다.

💡 shellcheck -x script.sh로 source로 포함된 다른 파일까지 함께 분석할 수 있습니다.

💡 CI/CD에서는 shellcheck -f json으로 JSON 형식 출력을 받아 자동화된 리포트를 생성할 수 있습니다.

💡 https://www.shellcheck.net/ 웹사이트에서 설치 없이 바로 스크립트를 검사해볼 수 있습니다. 빠른 테스트에 유용합니다.

💡 각 경고 코드(SC####)를 클릭하면 상세한 설명과 올바른 예제를 볼 수 있습니다. 이것을 읽으면 Bash를 제대로 배울 수 있습니다.


6. 디버깅 모드 (bash -x)

시작하며

여러분이 복잡한 스크립트를 실행했는데 예상과 다른 결과가 나왔을 때, "도대체 어느 부분에서 잘못된 거지?", "이 변수가 어떤 값을 가지고 있는 거지?" 하고 답답했던 적 있나요? 이런 문제는 스크립트가 길고 복잡할수록 더 심각해집니다.

조건문이 여러 개 중첩되어 있거나, 변수가 여러 단계를 거쳐 변환되거나, 파이프라인으로 여러 명령어가 연결되어 있으면 어디서 무엇이 잘못됐는지 파악하기가 정말 어렵습니다. echo를 여기저기 추가해서 디버깅하다 보면 코드가 지저분해지고, 디버깅이 끝나면 다시 echo를 다 지워야 하는 번거로움도 있습니다.

바로 이럴 때 필요한 것이 bash -x 디버깅 모드입니다. 이 모드를 켜면 마치 스크립트가 스스로 "지금 이것을 실행하고 있어요"라고 말해주는 것처럼 모든 명령어가 실행 전에 화면에 출력됩니다.

개요

간단히 말해서, bash -x는 스크립트를 실행하면서 각 명령어를 실행 전에 화면에 출력하는 디버깅 모드로, xtrace(execution trace)라고도 부릅니다. 왜 이 모드가 필요한지 실무 관점에서 설명하자면, 복잡한 스크립트에서 정확히 어느 시점에 어떤 값으로 무엇이 실행되는지 눈으로 확인할 수 있기 때문입니다.

예를 들어, 파일 경로를 조합하는 스크립트에서 /home/user//data처럼 슬래시가 중복되는 버그가 있다면, 디버깅 모드로 변수 값을 확인하면 즉시 원인을 찾을 수 있습니다. 기존에는 코드 곳곳에 echo "DEBUG: 변수=$변수"를 추가하고 나중에 다시 지우는 번거로운 방식이었다면, 이제는 한 줄의 옵션만으로 모든 실행 과정을 추적할 수 있습니다.

bash -x의 핵심 특징은 네 가지입니다. 첫째, 실행되는 모든 명령어를 + 기호와 함께 출력하여 실행 흐름을 보여줍니다.

둘째, 변수가 실제 값으로 치환된 후의 모습을 보여줘서 변수 문제를 쉽게 찾을 수 있습니다. 셋째, set -x와 set +x로 디버깅 모드를 동적으로 켜고 끌 수 있어서 특정 구간만 집중적으로 디버깅할 수 있습니다.

넷째, PS4 변수로 출력 형식을 커스터마이징할 수 있어서 더 유용한 정보를 추가할 수 있습니다. 이러한 특징들이 디버깅 시간을 크게 단축시켜줍니다.

코드 예제

#!/bin/bash

# 방법 1: 스크립트 전체를 디버깅 모드로 실행
# bash -x script.sh

# 방법 2: 셔뱅에 -x 옵션 추가
#!/bin/bash -x

# 방법 3: set 명령어로 동적 제어
set -x  # 디버깅 모드 시작

name="John Doe"
greeting="Hello, $name"
echo "$greeting"

set +x  # 디버깅 모드 종료 (이 이후는 출력 안 됨)

echo "This won't show debug output"

# 방법 4: PS4로 출력 형식 커스터마이징
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x

calculate() {
    local result=$((10 * 5))
    echo $result
}

calculate

# 출력 예시:
# +(script.sh:15): calculate(): local result=$((10 * 5))
# +(script.sh:16): calculate(): echo 50

설명

이것이 하는 일: bash -x는 스크립트의 각 명령어를 실행하기 직전에 화면에 출력하여 실행 과정을 시각적으로 추적할 수 있게 합니다. 첫 번째로, 디버깅 모드를 활성화하는 세 가지 방법이 있습니다.

bash -x script.sh처럼 실행할 때 옵션을 주는 방법, #!/bin/bash -x처럼 셔뱅에 추가하는 방법, set -x를 스크립트 안에서 사용하는 방법입니다. 왜 여러 방법이 있는지 궁금하시죠?

상황에 따라 편한 방법이 다르기 때문입니다. 스크립트를 수정할 수 없는 상황이라면 bash -x로 실행하고, 특정 구간만 디버깅하려면 set -x/+x를 사용하면 됩니다.

그 다음으로, 디버깅 모드가 켜지면 어떤 일이 일어나는지 살펴보겠습니다. name="John Doe" 같은 명령어를 실행하면, 화면에 + name='John Doe'처럼 출력됩니다.

  • 기호는 이것이 디버깅 출력임을 표시하고, 작은따옴표로 감싸진 것은 실제로 실행되는 명령어를 정확히 보여주는 것입니다. greeting="Hello, $name"을 실행하면 + greeting='Hello, John Doe'처럼 변수가 치환된 결과를 볼 수 있습니다.

이것이 정말 유용한 이유는, 변수에 예상치 못한 값이 들어있는지 즉시 알 수 있기 때문입니다. 세 번째로, set +x는 디버깅 모드를 끕니다.

이것은 스크립트의 일부분만 집중적으로 디버깅하고 싶을 때 유용합니다. 예를 들어, 1000줄짜리 스크립트에서 500-600줄 사이의 코드만 디버깅하고 싶다면, 500줄 앞에 set -x를, 600줄 뒤에 set +x를 추가하면 됩니다.

이렇게 하면 디버깅 출력이 너무 많아서 정작 필요한 정보를 찾기 어려운 문제를 피할 수 있습니다. 네 번째로, PS4 변수는 디버깅 출력의 프롬프트(기본값 +)를 커스터마이징할 수 있게 해줍니다.

export PS4='+(${BASH_SOURCE}:${LINENO}): '처럼 설정하면 +(script.sh:15): 같은 형식으로 파일명과 줄 번호를 보여줍니다. ${FUNCNAME[0]:+${FUNCNAME[0]}(): } 부분은 함수 안에서 실행되면 함수명도 표시하라는 의미입니다.

이렇게 하면 "아, 이 명령어는 script.sh의 15번째 줄에 있는 calculate 함수 안에서 실행되는 거구나"라고 즉시 알 수 있습니다. 마지막으로, 디버깅 출력을 파일로 저장하고 싶다면 bash -x script.sh 2> debug.log처럼 stderr를 리다이렉트하면 됩니다.

디버깅 출력은 stderr로 나가기 때문에 정상 출력(stdout)과 분리할 수 있습니다. 여러분이 bash -x를 사용하면 변수 값을 실시간으로 확인할 수 있고, 조건문이 어느 분기로 실행되는지 알 수 있으며, 파이프라인의 각 단계가 어떻게 처리되는지 볼 수 있습니다.

실무에서는 프로덕션 문제를 재현할 때나 복잡한 레거시 스크립트를 이해할 때 필수적으로 사용됩니다.

실전 팁

💡 PS4='+ ${BASH_SOURCE##*/}:${LINENO} ' 형식을 사용하면 전체 경로 대신 파일명만 표시되어 출력이 깔끔해집니다.

💡 BASH_XTRACEFD 변수로 디버깅 출력을 별도 파일 디스크립터로 보낼 수 있습니다. 예: exec 19>/tmp/trace.log; export BASH_XTRACEFD=19

💡 set -v는 명령어를 실행 전에 원본 그대로(변수 치환 전) 출력합니다. set -x와 함께 사용하면 더 상세한 디버깅이 가능합니다.

💡 trap 'echo ">>> 함수 $FUNCNAME 종료"' RETURN으로 함수 종료 시점을 추적할 수 있어서 bash -x와 조합하면 강력합니다.

💡 프로덕션 환경에서는 환경변수로 제어하세요. 예: [ "$DEBUG" = "true" ] && set -x 이렇게 하면 필요할 때만 디버깅 모드를 켤 수 있습니다.


#Bash#Debugging#ErrorHandling#ShellScript#BestPractices#Linux,Bash,Shell

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.