기술 가이드

Bash 스크립트 안전 작성 가이드

6 min read 쉘 스크립팅 업데이트됨 22 Oct 2025
Bash 스크립트 안전 작성 가이드
Bash 스크립트 안전 작성 가이드

특수 문자가 표시된 리눅스 PC용 키보드 사진.

Bash 스크립트는 강력합니다. 하지만 강력함에는 책임이 따릅니다. 부주의하거나 설계가 부족한 코드가 실제로 심각한 피해를 일으키기 쉽기 때문에 방어적 프로그래밍을 습관화해야 합니다.

다행히도 Bash에는 개발자를 보호하는 여러 내장 메커니즘이 있습니다. 많은 권장 기법은 오래되고 문제를 일으키는 방식 대신 현대적이고 안전한 문법을 사용하도록 유도합니다. 이 문서는 버그를 줄이고, 디버깅을 쉽게 하며, 경계 조건(엣지 케이스)을 다루는 방법을 정리합니다.

목적과 대상

이 문서는 다음을 위해 설계되었습니다:

  • 개인용과 팀용 Bash 스크립트를 작성하는 개발자와 운영자
  • Bash 초중급자 — 기본 문법은 알고 있으나 안전/유지보수 관점이 필요한 분
  • 자동화 파이프라인, 배포 스크립트, 운영 도구를 작성하는 엔지니어

주요 용어 한 줄 정의:

  • 쉐뱅(shebang): 스크립트의 첫 줄에 위치한 해시(#)와 느낌표(!)로 시작하는 주석. 어떤 인터프리터로 실행할지 선언한다.

중요 요약

  • 항상 명시적인 인터프리터 지정(쉐뱅)을 사용하세요. (/bin/bash vs /usr/bin/env bash 선택 이유를 이해하세요.)
  • 변수는 항상 인용(quoting)하세요: “$VAR”.
  • 실패 시 스크립트를 즉시 중단(set -e)하고 파이프라인 실패를 잡으려면 set -o pipefail을 사용하세요.
  • 디버깅에는 set -o xtrace(-x)를 사용하세요.
  • 다른 명령 호출 시 긴 형식 옵션(예: –recursive)을 우선 사용하면 가독성이 좋아집니다.
  • 명시적 인수 종료 시 “–“를 쓰십시오(파일명이 -로 시작할 때 옵션 혼동 방지).
  • 함수 내부에서 필요하면 지역 변수(local)를 사용하여 전역 오염을 방지하세요.

1. 좋은 쉐뱅 라인 사용

스크립트의 첫 줄은 항상 인터프리터를 선언하는 쉐뱅이어야 합니다. 쉐뱅은 스크립트를 독립 실행 파일로 만들고, 어떤 언어로 작성되었는지 명시합니다. 두 가지 일반적인 방식이 있습니다.

예제(절대 경로):

#!/bin/bash
echo "Hello, world"

이 방식은 /bin/bash에 있는 특정 bash를 실행합니다. 시스템에 특정 위치의 bash를 신뢰할 때 유리합니다. 반면 포터블성을 원하면 다음을 선호합니다.

예제(env 사용, 포터블):

#!/usr/bin/env bash
echo "Hello, world"

env는 PATH에서 첫 번째로 발견되는 bash를 실행합니다. 사용자의 환경에 설치된 bash를 따르게 하므로 다양한 시스템에서 더 유연하게 동작합니다.

선택 가이드(의사결정 흐름):

flowchart TD
  A[스크립트가 배포되는 환경이 통제되는가?] -->|예| B[절대 경로'/bin/bash' 사용]
  A -->|아니오| C[/usr/bin/env bash 사용]
  B --> D[특정 배포 환경에서 안정성 우선]
  C --> E[다양한 시스템에서 포터블성 우선]

중요: 보안 요구사항 때문에 로컬에 설치된 악성 bash가 실행되지 않도록 보장해야 한다면 절대 경로를 사용해 특정 바이너리를 지정하십시오.

언제 어떤 것을 선택해야 하나요?

  • 단일 서버나 컨테이너 등 배포 환경을 완전히 통제한다면 /bin/bash를 사용해 확실성을 높일 수 있습니다.
  • 스크립트를 여러 시스템(개발자 로컬, CI, 다양한 배포대상)에 배포한다면 /usr/bin/env bash가 더 좋습니다.

2. 변수는 항상 인용하기

공백(whitespace)은 셸에서 인수 분리자입니다. 변수 확장이 이루어질 때 인용을 하지 않으면 공백 때문에 인수가 쪼개져 예기치 않은 동작을 합니다.

문제 예제:

#!/bin/bash

FILENAME="docs/Letter to bank.doc"
ls $FILENAME

확장 후 셸은 다음과 같이 해석합니다:

ls docs/Letter to bank.doc

이는 ls 명령을 세 개의 인수로 해석합니다. 해결책은 다음과 같습니다.

ls "$FILENAME"

중괄호({}) 사용은 변수를 다른 문자와 바로 붙여 써야 할 때 유용합니다:

echo "_${FILENAME}_ is one of my favourite files"
# 또는 안전하게
echo "_${FILENAME}_는 내 파일 중 하나입니다"

중괄자가 없으면 셸은 FILENAME_이라는 다른 변수를 찾으려 시도합니다.

팁: 항상 “${VAR}” 형태를 기본 규칙으로 삼으면 실수를 줄일 수 있습니다.

3. 오류 발생 시 스크립트 중단 (set -e)

명령 실패를 무시하는 것만큼 위험한 행동은 없습니다. set -e 옵션은 파이프라인, 단일 명령, 혹은 복합 명령이 비 0(status != 0)를 반환하면 즉시 스크립트를 종료합니다.

set -e

예:

#!/bin/bash
set -e

touch /file
echo "Now do something with that file..."

위 예제에서 touch가 실패하면 스크립트는 즉시 종료됩니다. 그러나 set -e만으로 충분하지 않을 때가 있습니다. 파이프라인 내부의 최초 실패를 포착하려면 다음을 추가하세요.

set -o pipefail

이 설정은 파이프라인 전체의 종료 상태를, 그 구성 요소 중 비정상 상태를 낸 첫 명령의 상태로 반영합니다. 기본 동작은 파이프라인의 마지막 명령 상태만 반영하기 때문에 초기에 실패가 나도 눈치채지 못할 수 있습니다.

중요: set -e는 모든 상황에서 완벽하게 직관적이지 않습니다. 명령 치환, 서브쉘, 조건문에 따라 작동 방식이 달라질 수 있으니 이해하고 사용하세요.

4. 실패를 전파하고 처리하기

set -e는 좋은 안전망입니다. 하지만 구체적 상황에서는 개별 명령 실패를 직접 처리해 더 적절한 복구 동작을 수행해야 합니다.

명령 종료 상태 확인 예제:

cd "$DIR"
if [ $? -ne 0 ]; then
  echo "디렉터리 이동 실패: $DIR" >&2
  exit 1
fi

또는 간결한 논리 연산자 사용:

cd "$DIR" || { echo "디렉터리 이동 실패: $DIR" >&2; exit 1; }

또 다른 패턴은 함수로 실패 처리를 캡슐화하는 것입니다. 예:

fail_if() {
  local status=$1
  shift
  if [ "$status" -ne 0 ]; then
    echo "에러: $*" >&2
    exit "$status"
  fi
}

somecommand
fail_if $? "somecommand 실패"

5. 각 명령을 디버그하기 (xtrace)

디버깅에 유용한 설정은 xtrace(-x)입니다. 이 옵션은 명령을 실행하기 전에 확장된 형태로 출력합니다.

set -o xtrace
# 또는
set -x

예시 출력 이미지는 다음과 같습니다:

xtrace 설정을 사용해 date와 ls 명령의 세부 동작을 출력하는 스크립트의 터미널 스크린샷.

유용한 팁:

  • 디버깅을 위해 전체 스크립트에 set -x를 두지 말고, 문제 영역만 감싸는 것이 좋습니다.
  • 민감한 정보(비밀번호, 토큰 등)가 로그에 남지 않도록 주의하세요.

예: 선택적 트레이스

debug_on() { set -x; }
debug_off() { set +x; }

# 필요 구간에서만 활성화
debug_on
somecommand
debug_off

6. 다른 명령을 호출할 때는 긴 파라미터(롱옵션)를 사용

짧은 단일 문자 옵션은 익숙하면 빠르지만 가독성이 떨어집니다. 스크립트는 문서화의 수단이기도 하므로 긴 옵션을 선호하세요.

비교:

rm -rf filename
rm --recursive --force filename

롱옵션은 합칠 수 없지만, 읽는 사람이 의도를 바로 이해할 수 있어 협업에 유리합니다.

대체 접근: 스크립트 내에서 일관된 깃발(flag) 매핑을 제공해 단축형과 장형을 모두 지원할 수도 있습니다.

7. 명시적 인수 종료: “–“

파일명이 하이픈(-)으로 시작하면 명령이 이를 옵션으로 해석할 수 있습니다. 예를 들어 파일명이 “-rf”라면 rm * 명령이 위험해질 수 있습니다.

안전한 패턴:

rm -- *.md

“–“ 뒤의 모든 토큰은 옵션이 아니라 피연산자로 취급됩니다. 스크립트에서 외부 입력(파일명, 사용자 입력)을 그대로 명령에 전달할 때는 항상 “–“ 사용을 고려하세요.

8. 함수 내 지역 변수 사용

셸에서 변수는 기본적으로 전역입니다. 함수 안에서 값을 변경하면 전역 상태가 바뀌어 스크립트 다른 부분에 영향을 줄 수 있습니다.

문제 예제:

#!/bin/bash

function run {
  DIR=`pwd`
  echo "doing something..."
}

DIR="/usr/local/bin"
run
echo $DIR

이 코드는 run이 DIR 값을 바꿔 의도하지 않은 결과를 초래할 수 있습니다. 해결책은 local을 사용하는 것입니다.

function run {
  local DIR=$(pwd)
  echo "doing something..."
}

팁: 함수 최상단에서 지역 변수 목록을 선언하면 가독성과 유지보수가 좋아집니다.

9. 명령 치환은 현대형 $(…) 사용

두 가지 방식의 명령 치환이 존재합니다:

VAR=$(ls)
VAR2=`ls`

백틱(`) 방식은 중첩에 취약하고 가독성이 떨어집니다. 항상 $(…) 형식을 사용하세요. ## 10. 기본값 선언(${VAR:-default}) 환경 변수나 명령행 인수가 없을 때 기본값을 깔끔하게 지정할 수 있습니다. bash CMD=${PAGER:-more} 중첩도 가능합니다: bash DIR=${1:-${HOME:-/home/default}} ## 11. 공백·특수문자 취급과 안전한 파일명 처리 - 가능한 한 파일명 규칙을 지키세요(영문 소문자, 숫자, 점, 밑줄). - 외부 입력으로 파일명을 받을 때는 배열을 사용하고 항상 인용하세요. 안전한 루프 예제: bash shopt -s nullglob for f in *.txt; do echo "처리: $f" done 배열 사용 예: bash files=("$@") for f in "${files[@]}"; do echo "파일: $f" done ## 12. 입력 검증과 제한된 권한 실행 - 사용자 입력, 환경 변수, 파일 내용은 신뢰하지 마세요. - 외부 입력을 명령어로 평가(eval)하는 것을 피하세요. - 가능한 경우 최소 권한 원칙을 따르세요(Capabilities, sudoers 설정 등). 예: 사용자 입력을 정수로 검증하기 bash is_integer() { [[ "$1" =~ ^-?[0-9]+$ ]]; } if ! is_integer "$1"; then echo "첫 번째 인자는 정수여야 합니다" >&2 exit 2 fi ## 13. 보안 하드닝 체크리스트 - 민감 정보(비밀번호, 토큰)는 환경 변수 대신 비밀 관리 도구에 보관하고 스크립트에서는 참조만 하세요. - 로그에 민감 정보가 남지 않도록 주의하세요. 디버깅 출력 시 토큰은 마스킹하세요. - sudo는 꼭 필요한 부분에만 사용하고, 가능한 경우 특권을 최소화하세요. - 외부 명령 실행 시 신뢰할 수 있는 경로를 사용하세요(절대 경로 또는 PATH 고정). - 스크립트 파일 권한을 적절히 설정하세요(예: 0750 또는 0700). ## 14. 테스트 케이스와 수락 기준 테스트를 자동화하면 회귀를 막을 수 있습니다. 기본 테스트 케이스 예: - 정상 경로: 모든 입력이 유효할 때 기대되는 출력과 반환 코드(0)를 확인한다. - 입력 유효성 검사 실패: 잘못된 입력에서 오류 메시지 출력과 비 0 반환을 확인한다. - 파일이 없을 때: 에러 처리 로직이 동작하는지 확인한다. - 권한 오류: 명령이 권한 부족으로 실패할 때 스크립트가 안전하게 종료하는지 확인한다. 수락 기준(예시): - 주기능의 성공 시 반환코드 0 - 잘못된 입력에 대해 반드시 명확한 에러 메시지 출력 - 민감 정보가 로그에 출력되지 않음 - 자동화된 CI에서 모든 테스트가 통과함 ## 15. 역할별 체크리스트 개발자 체크리스트: - [ ] 코드에 주석과 사용 예시가 있는가? - [ ] 변수는 인용되어 있는가? - [ ] 함수는 지역 변수를 사용하는가? - [ ] 위험한 명령에 –를 사용했는가? 운영(운영팀/SRE) 체크리스트: - [ ] 배포 대상의 bash 위치를 확인했는가? (/bin/bash 또는 env) - [ ] 로그 수준과 로테이션 정책이 설정되어 있는가? - [ ] 비밀 관리와 접근 제어가 적용되어 있는가? 보안팀 체크리스트: - [ ] 입력 검증과 출력 인코딩이 되어 있는가? - [ ] 민감 정보가 평문으로 남지 않는가? - [ ] 스크립트 파일 권한과 소유권이 적절한가? ## 16. 예외 케이스와 한계 (언제 실패하는가) - set -e는 모든 경우에 직관적으로 동작하지 않습니다. 예를 들어 조건문의 일부로 사용된 명령은 set -e가 있어도 중단되지 않을 수 있습니다. - 복잡한 파이프라인에서 일부 명령의 실패를 의도적으로 무시해야 하는 경우, 실패를 허용하는 부분을 명시적으로 처리해야 합니다. - 많은 시스템에서 기본 shell이 dash 등 다른 POSIX 셸일 수 있으므로, POSIX 호환 스크립트를 작성해야 하는지 검토하세요. - 외부 종속(특정 coreutils 옵션, GNU 확장 등)은 이식성을 저하시킬 수 있습니다. ## 17. 대안 접근 방식 - 복잡한 로직은 Bash가 아니라 Python, Go, Rust 같은 언어로 작성하는 편이 안전성과 테스트 용이성 측면에서 좋을 수 있습니다. - 간단한 텍스트 처리에는 awk, sed, jq 같은 도구를 조합해 사용하되, 각 도구의 입력 검증을 철저히 하세요. ## 18. 마인드셋과 휴리스틱(의사결정 요령) - 작은 스크립트라도 항상 재현 가능한 동작을 목표로 하세요. - 불확실한 입력은 의심하라: 모든 외부 입력은 공격 표면입니다. - 명령 실행 전과 후의 상태를 로깅해 문제 발생 시 원인 추적을 쉽게 만드세요. - “명시적이거나 안전한(default-safe) 행동”을 기본으로 하세요. ## 19. 간단한 SOP(표준 운영 절차) 템플릿 1. 목적: 스크립트 목적을 한 문장으로 작성. 2. 사용법: 입력, 출력, 환경변수, 예시. 3. 권한: 실행 권한 및 필요한 사용자/그룹. 4. 실패 정책: 실패 시 로그/알람/롤백 절차. 5. 유지관리: 변경 이력, 리뷰 기준. ## 20. 짧은 치트시트(핵심 스니펫) - 스크립트 시작 템플릿 bash #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' # set -u: 정의되지 않은 변수 사용 시 오류 발생 # IFS를 좁게 설정하여 단어 분할 관련 문제 완화 - 안전한 for 루프 bash IFS=$'\n\t' files=("$@") for f in "${files[@]}"; do # 안전하게 처리 : done - 임시 파일 안전하게 생성 bash tmpfile=$(mktemp --tmpdir myscript.XXXXXX) || exit 1 trap 'rm -f "$tmpfile"' EXIT - 권한 검사 bash if [ "$(id -u)" -eq 0 ]; then echo "루트로 실행 중입니다" else echo "비-루트로 실행 중" fi ## 21. 호환성 및 마이그레이션 팁 - POSIX 호환을 원하면 bash 전용 기능(예: [[ ]], arrays 등)은 피하고 /bin/sh로 테스트하세요. - 컨테이너 환경에서는 기본 셸과 PATH 구성이 다를 수 있으므로 ENTRYPOINT/CMD와 함께 쉐뱅을 검토하세요. - CI/CD 파이프라인에서 스크립트를 실행할 때는 동일한 셸 버전을 사용하도록 런너를 고정하세요. ## 22. 짧은 용어집(한 줄 정의) - set -e: 오류 발생 시 스크립트 종료 - set -o pipefail: 파이프라인 중간 실패를 반영 - set -x: 실행되는 명령을 출력 - 쉐뱅: 스크립트 상단의 인터프리터 선언 - 인용(quoting): 변수 확장 시 공백 보호 — 중요: 이 문서는 패턴과 권장사항을 제공합니다. 모든 권장사항이 모든 상황에 100% 적용되지는 않습니다. 팀의 요구사항, 배포 환경, 보안 정책을 고려해 적절히 조정하세요. ## 요약 - 쉐뱅을 명확히 하세요(/bin/bash vs /usr/bin/env bash를 상황에 맞게 선택). - 변수는 항상 인용하고, 함수 내부에서는 local을 사용하세요. - set -e, set -o pipefail, set -o xtrace를 적절히 사용해 오류를 포착하고 디버깅하세요. - 긴 옵션, “–“ 사용, 안전한 임시파일 생성 같은 방어적 기법을 습관화하세요. - 테스트 케이스와 역할별 체크리스트를 만들어 팀 내 일관성을 확보하세요. 마지막으로, 복잡한 로직은 더 안전한 언어로 옮기는 것을 고려하세요. Bash는 강력하지만 모든 문제에 최적의 도구는 아닙니다.

공유하기: X/Twitter Facebook LinkedIn Telegram
저자
편집

유사한 자료

Debian 11에 Podman 설치 및 사용하기
컨테이너

Debian 11에 Podman 설치 및 사용하기

Apt-Pinning 간단 소개 — Debian 패키지 우선순위 설정
시스템 관리

Apt-Pinning 간단 소개 — Debian 패키지 우선순위 설정

OptiScaler로 FSR 4 주입: 설치·설정·문제해결 가이드
그래픽 가이드

OptiScaler로 FSR 4 주입: 설치·설정·문제해결 가이드

Debian Etch에 Dansguardian+Squid(NTLM) 구성
네트워크

Debian Etch에 Dansguardian+Squid(NTLM) 구성

안드로이드 SD카드 설치 오류(Error -18) 완전 해결
안드로이드 오류

안드로이드 SD카드 설치 오류(Error -18) 완전 해결

KNetAttach로 원격 네트워크 폴더 연결하기
네트워킹

KNetAttach로 원격 네트워크 폴더 연결하기