Devin.KR

출력과 실패를 다루는 셸

90분 안팎

학습 목표

표준 출력·오류 출력과 종료 상태를 연결합니다.

개념

왜 성공 메시지를 믿으면 안 될까요

준비 명령이 실패했는데 그 다음 줄에서 배포를 시작하면 잘못된 설정으로 서비스를 교체할 수 있습니다. 화면의 문장은 사람이 읽는 정보이며 자동화는 종료 상태로 성공과 실패를 판단합니다. 0은 성공이고 비영 값은 실패 또는 프로그램이 정한 다른 상태입니다. 개별 명령의 정의를 확인해야 하며 모든 비영 값이 같은 원인을 뜻하지는 않습니다. 이 레슨에서는 설정 없음은 2, 내용 오류는 3으로 약속하고 실패한 준비 뒤에는 후속 표시 파일이 생기지 않게 만듭니다.

세 가지 채널을 구분합니다

프로그램은 표준 출력으로 결과를, 표준 오류로 진단을 보내고 종료 상태로 호출자에게 판단 값을 전달합니다. Bash에서 >는 표준 출력을 파일로 보내고 2>는 표준 오류를 별도 파일로 보냅니다. printf의 >&2는 진단을 표준 오류로 보냅니다. 메시지가 화면에 보이는 것과 어느 채널에서 나왔는지는 다른 문제입니다. 검사기는 성공 시 오류 출력이 비어 있고 실패 시 결과 출력이 비어 있는지까지 확인합니다. 실패 메시지가 결과 데이터에 섞이면 다음 자동화 단계가 이를 정상 결과로 읽을 수 있기 때문입니다.

종료 상태는 즉시 저장합니다

명령 뒤의 $?는 바로 직전에 끝난 명령의 종료 상태입니다. 확인 전에 echo나 다른 명령을 실행하면 그 명령의 상태로 바뀝니다. 예를 들어 실패 명령 다음 줄에서 status=$?로 저장하고 그 다음 printf로 값을 출력합니다. 이 실습의 따라하기는 의도적으로 비영 종료를 실행하므로 사용자의 대화형 셸 전체를 종료시키지 않게 별도의 bash -c 안에서 수행합니다. 검사용 셸에서 set -e를 무심코 켜 두면 상태를 저장하기 전에 종료될 수 있으므로 실패를 예상하는 곳은 if로 명시적으로 분기합니다.

설정의 존재와 내용을 나눠 검사합니다

[[ -f "$config" ]]는 일반 파일인지 확인합니다. 그 다음 grep -qx로 APP_NAME=notice-board 한 줄이 있는지 검사합니다. -q는 내용을 출력하지 않고 -x는 부분 문자열이 아닌 한 줄 전체와 일치하는지 확인합니다. 이번 계약은 그 줄의 존재만 요구하며 env 파일 전체 문법을 검사하는 것은 아닙니다. APP_NAME=wrong은 거부하고 설정 파일을 셸 코드로 실행하지 않습니다. source로 임의 파일을 읽으면 그 안의 명령까지 실행할 수 있으므로 이 레슨에서는 읽을 데이터의 계약만 검사합니다.

실패 분기를 눈에 보이게 만듭니다

설정이 없으면 표준 오류에 prepare: missing app.env를 보내고 exit 2로 종료합니다. 내용이 맞지 않으면 prepare: invalid APP_NAME를 보내고 exit 3으로 종료합니다. 이 분기 뒤에만 next-stage.txt를 생성합니다. if ! grep ...는 성공 여부를 뒤집어 실패 때 분기하는 표현이며, 그 안의 $?로 원래 grep 상태를 읽는 구현은 피합니다. 이 레슨은 진단 종류별 종료 값을 직접 정하므로 grep의 상태를 그대로 전달할 필요가 없습니다. 준비 실패와 다음 단계 실행의 관계를 코드 구조로 드러냅니다.

set -e와 파이프를 과신하지 않습니다

set -euo pipefail은 예상하지 못한 실패와 정의되지 않은 변수·파이프라인 실패를 발견하는 데 도움이 됩니다. 그러나 set -e는 if 조건 같은 문맥에서 동작이 달라지므로 중요한 검증은 명시적인 분기와 exit로 작성합니다. 기본 파이프라인 상태는 마지막 명령 상태이므로 false | tee 파일이 성공처럼 보일 수 있습니다. Bash의 pipefail을 켜면 파이프라인 안의 비영 종료가 반영됩니다. 표준 오류도 기록하려면 파이프 앞에서 2>&1로 합쳐야 하며 결과 파싱이 필요한 경우에는 두 채널을 분리해 유지합니다.

실패 fixture로 경계를 확인합니다

fixture는 특정 상황을 재현하기 위해 만든 시험 입력입니다. 검사기는 유효한 설정, 설정 없음, 잘못된 이름을 각각 새로운 복사본에서 실행합니다. 실패 뒤 후속 파일이 없어야 한다는 조건은 빈 복사본에서 확인합니다. 이전 성공 때 만든 파일이 남아 있으면 현재 실행이 만들었다고 단정할 수 없습니다. 이번 표시 파일은 배포 실행 자체가 아니라 다음 단계에 도달했음을 보여 주는 교육용 증거입니다. 파이프라인 로그 저장의 상세 옵션은 tee 더 읽기로 보내고 미션에는 명시적 실패 처리 구조를 가져갑니다.

리다이렉션 순서가 바꾸는 결과

Bash는 리다이렉션을 왼쪽부터 적용합니다. 명령 > combined.txt 2>&1은 표준 출력을 파일로 연결한 뒤 표준 오류를 그 목적지로 복제합니다. 반대로 명령 2>&1 > result.txt는 표준 오류를 먼저 기존 표준 출력으로 연결하므로 오류가 result.txt에 함께 들어간다고 보장하지 않습니다. 파일 디스크립터 1과 2를 어떤 순서로 연결하는지 설명하면서 로그를 읽습니다. 이번 검사는 결과와 진단을 별도로 비교하므로 성공 문구를 stderr에 쓰거나 실패 진단을 stdout에 쓰면 종료 값이 맞아도 계약을 만족하지 못합니다.

실패 상태를 보존하는 호출을 작성합니다

예상한 실패를 다루는 호출자는 if bash scripts/prepare.sh; then 성공 처리; else status=$?; 실패 처리; fi처럼 분기할 수 있습니다. else에서 다른 명령을 실행하기 전에 status를 저장해야 원래 상태를 보존합니다. 실패를 기록한 뒤 호출자도 실패를 알려야 한다면 마지막에 exit "$status"로 전달합니다. 오류 메시지를 남긴 printf가 성공했다고 전체 작업을 0으로 끝내면 상위 자동화가 후속 작업을 시작할 수 있습니다. 재시도도 모든 비영 값을 같은 원인으로 취급하지 않습니다. 파일이 없는 경우에는 입력 준비가 필요하고 잘못된 이름인 경우에는 내용 수정이 필요합니다.

실습에서 적용할 구현

scripts/prepare.sh에서 입력 검증을 먼저 하고 실패 분기에서는 stderr와 종료 값을 전달합니다. 성공 분기에만 후속 파일 생성을 두어 진행 조건을 코드에서 확인합니다.

#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
config="$ROOT/workspace/app.env"
if [[ ! -f "$config" ]]; then
  printf 'prepare: missing app.env\n' >&2
  exit 2
fi
if ! grep -qx 'APP_NAME=notice-board' "$config"; then
  printf 'prepare: invalid APP_NAME\n' >&2
  exit 3
fi
printf 'prepared\n'
printf 'next-stage\n' > "$ROOT/workspace/next-stage.txt"

따라하기

정상 준비

모범 답안 zip을 새 폴더에 푼 루트에서 실행합니다. 시작 코드는 아래 검사에서 실패하며 실습에서 수정합니다.

cp workspace/app.example.env workspace/app.env
bash scripts/prepare.sh

실행 결과

prepared

출력과 종료 상태 분리

모범 답안 zip을 새 폴더에 푼 루트에서 실행합니다. 시작 코드는 아래 검사에서 실패하며 실습에서 수정합니다.

cp workspace/app.example.env workspace/app.env
bash -c 'printf "diagnostic\n" >&2; exit 3' > result.txt 2> error.txt
status=$?
printf 'exit=%s\n' "$status"
cat error.txt
test ! -s result.txt && printf 'stdout empty\n'

실행 결과

exit=3
diagnostic
stdout empty

실패 입력과 후속 단계 검사

모범 답안 zip을 새 폴더에 푼 루트에서 실행합니다. 시작 코드는 아래 검사에서 실패하며 실습에서 수정합니다.

cp workspace/app.example.env workspace/app.env
bash check.sh

실행 결과

PASS example config exists
PASS valid setup
PASS success marker
PASS missing rejects with status and stderr
PASS missing blocks next stage
PASS invalid rejects with status and stderr
PASS invalid blocks next stage
RESULT: 0 failure(s)

확인 문제

실습

시작 코드 zip을 개인 폴더에 풀고 README를 읽은 뒤 bash check.sh로 실패 항목을 확인합니다. scripts/prepare.sh를 고쳐 설정 없음은 종료 2, 잘못된 APP_NAME은 종료 3과 지정 진단으로 실패하게 합니다. 실패에서는 next-stage.txt를 만들지 않습니다. 검사기는 복사본에서 정상·경계 상황을 확인합니다. check.sh와 check.py는 수정하지 않습니다.

시작 코드·테스트 내려받기

실행 명령

bash check.sh

기대 결과

모든 항목 PASS, RESULT: 0 failure(s), 종료 상태 0

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 표준 출력·표준 오류·종료 상태의 역할을 설명해 주세요.
  • Bash 파이프라인에서 앞 명령의 실패가 가려지는 이유와 pipefail의 역할은 무엇인가요?