문서 읽는 데 38분 · A12

A-12: bash 스크립팅 ② — 흐름 제어와 자동화

목차 24
전체 12강 중 12강 · 리눅스
난이도 · 입문

ℹ️서버·배포에서 쓰는 리눅스 셸과 명령어 기본기. 백엔드·인프라로 가기 전에 익혀두면 든든해요.

안녕하세요, 홍순구입니다. 드디어 이 과목의 진짜 마지막, 피날레에 도착했어요. 지난 시간 우리는 스크립트를 만들어 실행하고, 변수에 값을 담고, 바깥에서 인자로 값을 건네받는 기본기를 손에 익혔습니다. 재료는 다 모았어요.

그런데 지금까지의 스크립트는 위에서 아래로 한 줄씩, 정해진 대로만 흘렀죠. 진짜 자동화는 거기서 한 발 더 나아갑니다. "디스크가 80%를 넘었으면 경고하고, 아니면 넘어간다"처럼 상황에 따라 갈라지고, "로그 파일을 하나씩 전부 백업한다"처럼 반복하고, 같은 일을 함수로 묶어 재사용하죠. 오늘 배울 흐름 제어가 바로 그것이에요.

겁먹지 마세요. 조건·반복·함수라고 하면 프로그래밍 같지만, 사실 우리가 머릿속으로 매일 하는 판단을 글로 옮긴 것뿐이에요. "비 오면 우산 챙기고, 아니면 그냥 나간다" — 이게 if예요. 천천히 하나씩 손에 익혀가요.

오늘 우리가 걸어갈 길이자, 이 과목의 마지막 길을 그려둘게요.

텍스트
  [판단]   if · 종료 코드($?)    case (여러 갈래를 깔끔히)
  [반복]   for · while · until
  [재사용]  함수로 묶기
  [완성]   실전 자동화 스크립트 (백업·디스크·헬스체크) + cron 예약

지난 열한 모듈에서 모은 모든 재료를, 오늘 하나의 요리로 완성합니다. 마지막까지 편하게 따라오세요.

💡 오늘 수업의 핵심 — "조건·반복·함수로 흐름을 제어해, 흩어진 명령을 진짜 자동화 스크립트로 묶는다"

상황에 따라 갈라지는 ifcase, 같은 일을 반복하는 for·while, 명령 묶음을 이름 붙여 재사용하는 함수 — 이 흐름 제어를 익히고, 마지막엔 이 과목에서 배운 명령들을 한데 엮어 "매일 밤 자동으로 도는 점검 스크립트"를 직접 완성하는 것. 여기까지가 오늘이자 이 과목의 목표입니다.

🎯 학습 목표

  • 조건문(if/case)과 종료 코드($?)로 상황에 따라 갈라지는 스크립트를 만든다
  • 반복문(for/while/until)과 함수로 반복을 줄이고 명령을 재사용한다
  • 지금까지 배운 명령을 엮어 백업·점검·헬스체크를 하는 실전 자동화 스크립트를 완성하고 cron으로 예약한다

Step 1: "판단하는 스크립트 — if와 종료 코드"

스크립트가 똑똑해지는 첫걸음은 "상황을 보고 갈라지는" 거예요. 우리가 "비 오면 우산, 아니면 그냥"이라고 판단하듯, 스크립트도 조건을 보고 다르게 행동하게 만듭니다. 그게 if예요.

Bash
arg="start"
if [ "$arg" = "start" ]; then
  echo "시작합니다"
fi
텍스트
시작합니다

if [ 조건 ]; then ... fi 구조예요. [ ... ] 안의 조건이 참이면 thenfi(if를 거꾸로 쓴 거예요. 끝을 알리는 표시죠) 사이를 실행합니다. 여기서 "$arg"를 큰따옴표로 감싼 것도 지난 시간에 배운 안전한 습관이고요.

갈래가 여럿일 땐 elifelse를 더해요.

Bash
score=85
if [ "$score" -ge 90 ]; then
  echo "A"
elif [ "$score" -ge 80 ]; then
  echo "B"
else
  echo "C"
fi
텍스트
B

숫자를 비교할 땐 -ge(크거나 같다), -gt(크다), -eq(같다) 같은 기호를 써요. 글자가 아니라 숫자라서 >= 대신 이런 표기를 쓴다고 기억하면 됩니다.

조건에서 특히 자주 쓰는 게 파일 검사예요. "이 파일이 있으면", "이 폴더가 없으면" 같은 판단이죠. 자동화 스크립트의 단골입니다.

Bash
if [ -f report.log ]; then
  echo "report.log 가 있다"
fi

if [ ! -f nofile.txt ]; then
  echo "nofile.txt 는 없다"
fi

-f는 "파일이 있는가", -d는 "폴더가 있는가", -z는 "변수가 비었는가"를 묻고, 앞에 !를 붙이면 "아니라면"으로 뒤집어요. 백업 전에 if [ -d "$로그폴더" ]로 폴더가 진짜 있는지 확인하는 식으로 쓰죠.

그런데 if는 사실 [ ... ]만 보는 게 아니에요. 그 속을 들여다보면 종료 코드(exit code)라는 게 흐르고 있어요. 모든 명령은 끝나면서 "성공했어"(0) 또는 "실패했어"(0이 아닌 수)라는 숫자를 남깁니다. 그 숫자를 $?로 꺼내볼 수 있어요.

Bash
ls report.log > /dev/null
echo "방금 명령의 종료 코드: $?"
텍스트
방금 명령의 종료 코드: 0

성공하면 0, 없는 파일을 찾는 등 실패하면 0이 아닌 값이 나와요. A-4 답안에서 "이건 마지막 모듈에서 다룬다"고 잠깐 비췄던 그 종료 코드가 바로 이거예요. if는 바로 이 종료 코드를 보고 갈라지는 거랍니다 — 조건이 0(성공)이면 then으로 가는 거죠. 그래서 if grep -q ERROR app.log; then ...처럼 명령을 직접 조건으로 쓸 수도 있어요. 내 스크립트도 끝에 exit 0(정상)이나 exit 1(문제 있음)로 자신의 종료 코드를 남길 수 있고요.

요즘은 [ ... ] 대신 [[ ... ]](대괄호 두 개)를 권장해요. 거의 같지만 더 안전하고, 와일드카드 비교 같은 게 편하거든요.

Bash
file="report.log"
if [[ "$file" == *.log ]]; then
  echo "로그 파일이군요"
fi
텍스트
로그 파일이군요

💡 한 줄 정리

if [ 조건 ]; then ... fi로 상황에 따라 갈라지고, elif/else로 여러 갈래를 만든다. 모든 명령은 종료 코드($?, 성공 0)를 남기며 if는 이 값을 보고 판단한다. 요즘은 [ ]보다 [[ ]]가 권장된다.

🙋 학생 질문 — "튜터님, [ ][[ ]]는 뭐가 달라요? 둘 다 되면 헷갈리는데요."

좋은 질문이에요. 역사 순서로 보면 [ ]가 먼저고 [[ ]]가 나중에 나온 개선판이에요.

[ ]는 사실 test라는 옛 명령의 다른 얼굴이에요. 그래서 규칙이 조금 까다로워요. 예를 들어 변수가 비어 있는데 따옴표로 안 감싸면 [ ] 안의 조건이 깨져서 에러가 나기도 해요. 그래서 [ ]를 쓸 땐 변수를 꼭 "$var"로 감싸야 합니다.

[[ ]]는 bash가 나중에 추가한 개선판이라 이런 함정이 줄었고, ==로 와일드카드 비교(*.log)나 &&·|| 같은 걸 안에서 더 편하게 쓸 수 있어요. 그래서 요즘 bash 스크립트는 [[ ]]를 권장해요.

정리하면, 입문자는 그냥 [[ ]]를 기본으로 쓰면 편해요. 다만 아주 오래된 스크립트나 다른 셸(sh)에서는 [[ ]]가 안 되고 [ ]만 되는 경우가 있으니, [ ]도 읽을 줄은 알아두면 좋습니다. 둘 다 "조건이 참이냐"를 묻는 같은 일을 한다는 게 핵심이에요.


Step 2: "여러 갈래를 깔끔하게 — case"

if·elif·else로도 여러 갈래를 만들 수 있지만, 한 값을 여러 경우와 맞춰볼 땐 elif가 길게 늘어져 지저분해져요. 이럴 때 깔끔한 게 case예요. 서비스를 다루는 스크립트가 좋은 예죠. start·stop·restart 중 무엇이 들어왔는지에 따라 갈라지거든요.

Bash
#!/bin/bash
case "$1" in
  start)   echo "서비스를 시작합니다" ;;
  stop)    echo "서비스를 멈춥니다" ;;
  restart) echo "서비스를 재시작합니다" ;;
  *)       echo "사용법: $0 {start|stop|restart}" ;;
esac

이 스크립트를 svc.sh로 저장하고 실행하면 인자에 따라 갈라져요.

Bash
$ ./svc.sh start
서비스를 시작합니다
$ ./svc.sh hello
사용법: ./svc.sh {start|stop|restart}

case "$1" in ... esac 구조예요(esaccase를 거꾸로 쓴 끝 표시). $1의 값을 위에서부터 각 경우(start)·stop)…)와 맞춰보고, 일치하는 갈래의 명령을 실행한 뒤 ;;로 그 갈래를 닫아요. 맨 아래 *)는 "위의 어디에도 안 맞을 때"를 받는 갈래라, 잘못된 입력에 사용법을 안내하기 딱 좋죠.

start/stop/restart 패턴, 어디서 봤죠? 맞아요, A-8에서 systemctl start nginx처럼 서비스를 다룰 때 봤어요. 실제로 옛날 리눅스 서비스 스크립트들이 바로 이 case 구조로 짜여 있었답니다. 입력을 몇 갈래로 나눠 받는 스크립트라면 caseif 줄줄이보다 훨씬 읽기 좋아요.

💡 한 줄 정리

case "$값" in 경우) 명령 ;; ... *) 기본 ;; esac는 한 값을 여러 경우와 맞춰 갈라질 때 if·elif보다 깔끔하다. *)로 나머지 경우를 받아 잘못된 입력을 안내한다.

🙋 학생 질문 — "튜터님, if로도 되는데 언제 case를 써요?"

기준은 "한 값을 여러 경우와 맞춰보는가"예요.

case는 하나의 값($1 같은)을 여러 고정된 경우와 비교할 때 빛나요. startstop이냐 restart냐처럼요. 이걸 if로 쓰면 if [ "$1" = "start" ]; then ... elif [ "$1" = "stop" ]; then ...로 같은 "$1" =가 계속 반복돼 지저분해지죠. case는 그 반복을 없애줘요.

반대로 if는 서로 다른 종류의 조건을 따질 때 좋아요. "디스크가 80% 넘고 그리고 로그 파일이 존재하면"처럼 비교 대상이 매번 다르거나, 숫자 크기를 따지는 판단이요. 이런 건 case로 표현하기 어색해요.

정리하면, 한 값을 정해진 여러 후보와 맞출 땐 case, 그 외의 일반적인 판단은 if. 실무에선 "이 스크립트를 어떤 모드로 실행할까"를 받는 입구에서 case를 자주 만나게 됩니다.


Step 3: "반복을 진짜 자동으로 — for·while·until"

자동화의 핵심은 반복이에요. 로그 파일 열 개를 하나씩 손으로 백업하는 대신, "전부 돌면서 백업해"라고 한 번에 시키는 거죠. 가장 많이 쓰는 게 for예요.

Bash
for f in *.log; do
  echo "백업 대상: $f"
done
텍스트
백업 대상: access.log
백업 대상: app.log
백업 대상: error.log

for f in 목록; do ... done 구조예요. *.log라는 와일드카드(A-2에서 배웠죠)가 현재 폴더의 로그 파일들로 펼쳐지고, f에 하나씩 담기며 dodone 사이를 반복해요. 파일 목록뿐 아니라 그냥 값 목록으로도 돌 수 있어요.

Bash
for i in 1 2 3; do
  echo "시도 $i 회"
done

for가 "정해진 목록을 도는" 거라면, while은 "조건이 참인 동안 도는" 반복이에요. 횟수가 정해지지 않았을 때 좋죠.

Bash
n=1
while [ "$n" -le 3 ]; do
  echo "카운트 $n"
  n=$((n + 1))
done
텍스트
카운트 1
카운트 2
카운트 3

while [ 조건 ]; do ... done은 조건이 참인 한 계속 돌아요. 안에서 n=$((n + 1))로 값을 키워주는데, $(( ))는 셸에서 산수를 하는 표기예요. 이걸 빼먹으면 조건이 영영 참이라 무한 반복에 빠지니 조심하세요. untilwhile의 반대로, "조건이 참이 될 때까지" 돌아요.

Bash
m=1
until [ "$m" -gt 3 ]; do
  echo "대기 $m"
  m=$((m + 1))
done

셋을 한눈에 정리하면 이래요.

텍스트
  for    정해진 목록을 하나씩      예: 모든 *.log 파일을 백업
  while  조건이 참인 동안          예: 서버가 응답할 때까지 기다린다
  until  조건이 참이 될 때까지     예: 파일이 생길 때까지 기다린다

💡 한 줄 정리

for는 정해진 목록(*.log 등)을 하나씩 돌고, while은 조건이 참인 동안, until은 조건이 참이 될 때까지 반복한다. while/until에선 값을 바꿔주지 않으면 무한 반복에 빠지니 주의한다.

🙋 학생 질문 — "튜터님, 실수로 무한 반복에 빠지면 어떻게 멈춰요?"

누구나 한 번쯤 겪는 일이에요. while 안에서 카운터 키우는 걸 빠뜨려서 화면에 같은 줄이 끝없이 쏟아지는 상황이요. 당황하지 마세요. 멈추는 법은 이미 배웠어요.

A-7에서 다룬 Ctrl + C예요. 터미널에서 돌고 있는 작업을 강제로 중단시키는 단축키죠. 무한 반복에 빠진 스크립트도 Ctrl + C 한 번이면 멈춰요. 셸이 그 작업에 "그만!"이라는 신호(A-7에서 본 시그널이에요)를 보내는 거랍니다.

그래서 새 while이나 until을 짤 땐, 마음 편히 일단 돌려보세요. 무한 반복이 나도 Ctrl + C로 빠져나오면 되니까요. 빠져나온 뒤 "아, 카운터를 안 키웠네" 하고 n=$((n + 1)) 한 줄을 추가하면 됩니다. 안전장치가 있으니 겁내지 말고 시도하세요.


Step 4: "묶어서 재사용 — 함수"

스크립트가 길어지면 같은 명령 묶음을 여기저기서 반복하게 돼요. "로그를 한 줄 남긴다" 같은 동작을 매번 똑같이 쓰는 거죠. 이럴 때 그 묶음에 이름을 붙여 재사용하는 게 함수(function)예요.

Bash
greet() {
  local name="$1"
  echo "안녕하세요, ${name}님!"
}

greet "민지"
greet "원영"
텍스트
안녕하세요, 민지님!
안녕하세요, 원영님!

이름() { ... }로 함수를 정의하고, 나중에 이름만 부르면 그 안의 명령이 실행돼요. 함수도 스크립트처럼 인자를 받아요. 함수 안에서 $1은 함수를 부를 때 건넨 첫 인자예요(greet "민지"의 "민지"). 앞에 붙은 local은 "이 변수는 이 함수 안에서만 쓴다"는 표시예요. 함수 밖의 같은 이름 변수와 섞이지 않게 막아주죠.

함수도 명령처럼 종료 코드로 성공·실패를 알릴 수 있어요. return 0은 성공, return 1은 실패예요.

Bash
is_even() {
  if [ $(( $1 % 2 )) -eq 0 ]; then
    return 0
  else
    return 1
  fi
}

if is_even 4; then echo "4는 짝수"; fi
if is_even 7; then echo "7은 짝수"; else echo "7은 홀수"; fi
텍스트
4는 짝수
7은 홀수

is_even 함수가 return으로 남긴 종료 코드를, if가 Step 1에서 배운 대로 읽어서 갈라지는 거예요. 여기서 한 가지 짚을 게 있어요. bash의 함수에서 return은 "값"이 아니라 "성공/실패 신호(종료 코드)"만 돌려줘요. 계산 결과 같은 진짜 값을 돌려주고 싶으면 echo로 출력하고 부르는 쪽에서 결과=$(함수이름)으로 받습니다. 지난 시간 배운 명령 치환($(...))이 여기서 다시 쓰이는 거죠.

💡 한 줄 정리

이름() { ... }로 명령 묶음에 이름을 붙여 재사용한다. 함수도 $1로 인자를 받고, local로 함수 안에서만 쓰는 변수를 만든다. return은 종료 코드(성공/실패)를 돌려주고, 진짜 값은 echo + $(함수)로 받는다.

🙋 학생 질문 — "튜터님, 함수랑 스크립트는 뭐가 달라요? 둘 다 명령을 묶는 거 아닌가요?"

맞아요, 둘 다 명령을 묶는다는 점은 같아요. 차이는 "범위"와 "재사용 방식"이에요.

스크립트는 파일 하나로 독립해서 어디서든 실행할 수 있는 단위예요. ./backup.sh처럼 부르죠. 함수는 한 스크립트 안에서 정의해서, 그 스크립트 안에서 이름으로 부르는 작은 묶음이에요. 말하자면 스크립트가 큰 그릇이고, 함수는 그 안에서 반복되는 동작을 깔끔히 정리한 작은 그릇이에요.

언제 함수를 쓰냐면, 한 스크립트 안에서 같은 동작을 여러 번 할 때예요. 오늘 마지막에 만들 점검 스크립트를 보면 "로그를 한 줄 남긴다"는 동작을 백업할 때도, 디스크 잴 때도, 헬스체크할 때도 반복해요. 이걸 log() 함수 하나로 묶어두면, 나중에 로그 형식을 바꿀 때 함수 한 곳만 고치면 전부 바뀌죠. 이게 함수의 힘이에요. 다음 Step에서 직접 보게 될 거예요.


Step 5: "실전 종합 — 매일 밤 도는 점검 스크립트"

자, 드디어 다 모았어요. 변수·인자(지난 시간), 조건·반복·함수(오늘). 이제 이 과목에서 배운 명령들을 한데 엮어 진짜 자동화 스크립트를 만들 차례예요. 목표는 이거예요. "매일 밤 자동으로 로그를 백업하고, 디스크가 꽉 찼는지 점검하고, 서비스가 살아있는지 확인한다." A-10 마무리에서 약속했던 바로 그 스크립트죠.

한 번에 다 짜면 부담스러우니, 조각으로 나눠 함수로 만들고 마지막에 이어 붙일게요. 먼저 어디서나 쓸 "로그 한 줄 남기기" 함수와 설정값부터.

Bash
#!/bin/bash
LOG_DIR="$HOME/practice/logs"
BACKUP_DIR="$HOME/practice/backups"
HEALTH_URL="https://example.com"
DISK_LIMIT=80

log() {
  echo "[$(date '+%H:%M:%S')] $1"
}

맨 위에 설정값을 변수로 모아뒀어요. 나중에 경로나 한도를 바꿀 때 위만 고치면 되죠. log 함수는 $(date ...)로 현재 시각을 앞에 붙여 한 줄 출력해요. 이제 세 가지 일을 각각 함수로 만듭니다. 먼저 로그 백업이에요.

Bash
backup_logs() {
  mkdir -p "$BACKUP_DIR"
  local archive="$BACKUP_DIR/logs-$(date +%Y-%m-%d).tar.gz"
  if [ -d "$LOG_DIR" ]; then
    tar -czf "$archive" -C "$LOG_DIR" .
    log "백업 완료: $archive"
  else
    log "로그 폴더가 없어 백업을 건너뜁니다"
  fi
}

A-2에서 배운 tar로 로그 폴더를 통째로 묶고, 파일 이름엔 $(date +%Y-%m-%d)로 날짜를 붙였어요(매일 다른 이름으로 쌓이죠). if [ -d "$LOG_DIR" ]로 폴더가 진짜 있을 때만 백업하게 막은 것도 보이죠. 다음은 디스크 점검이에요.

Bash
check_disk() {
  local usage=$(df -P / | awk 'NR==2 {print $5}' | tr -d '%')
  if [ "$usage" -ge "$DISK_LIMIT" ]; then
    log "경고: 디스크 사용률 ${usage}% (한도 ${DISK_LIMIT}%)"
  else
    log "디스크 사용률 ${usage}% — 정상"
  fi
}

df로 디스크 사용률을 보고, A-4·A-5에서 익힌 파이프와 awk로 퍼센트 숫자만 뽑아냈어요. 그 값이 한도(80%)를 넘으면 경고를, 아니면 정상을 남깁니다. 마지막은 서비스 헬스체크예요.

Bash
check_health() {
  local code=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL")
  if [ "$code" = "200" ]; then
    log "서비스 정상 (HTTP $code)"
  else
    log "서비스 이상! (HTTP $code)"
  fi
}

A-10에서 배운 curl로 서비스를 두드려 상태 코드만 뽑아내고(-w "%{http_code}"), 200이면 정상, 아니면 이상으로 판단해요. 이제 이 함수들을 차례로 부르는 본문만 맨 아래 붙이면 완성이에요.

Bash
log "=== 야간 점검 시작 ==="
backup_logs
check_disk
check_health
log "=== 야간 점검 끝 ==="

이 다섯 조각을 nightly-check.sh 한 파일에 차례로 담고 실행하면, 이렇게 돌아갑니다.

텍스트
[03:00:01] === 야간 점검 시작 ===
[03:00:01] 백업 완료: /home/alice/practice/backups/logs-2026-06-17.tar.gz
[03:00:01] 디스크 사용률 5% — 정상
[03:00:02] 서비스 정상 (HTTP 200)
[03:00:02] === 야간 점검 끝 ===

백업이 만들어지고, 디스크가 점검되고, 서비스 상태까지 한 번에 확인됐어요. 흩어져 있던 tar·df·awk·curl이 조건·반복·함수라는 접착제로 하나의 스크립트에 묶인 거예요.

마지막 한 걸음이 남았어요. 이걸 매일 밤 사람 없이 자동으로 돌리는 거죠. A-8에서 배운 cron을 기억하세요? crontab -e로 이 한 줄만 등록하면 됩니다.

Bash
0 3 * * * /home/alice/bin/nightly-check.sh >> /home/alice/nightly.log 2>&1

앞의 0 3 * * *가 "매일 새벽 3시"(A-8의 5필드죠), 그다음이 실행할 스크립트예요. 경로를 /home/alice/bin/...처럼 절대경로로 적은 건 A-8에서 짚은 cron의 함정 때문이고, 뒤의 >> ... 2>&1은 A-4에서 배운 리다이렉션으로 출력과 에러를 로그에 쌓는 거예요. 이제 여러분이 잠든 사이에도, 매일 새벽 3시면 이 점검이 사람 손 하나 안 대고 저절로 돌아갑니다.

💡 한 줄 정리

설정값을 변수로 모으고, 할 일을 함수(backup_logs·check_disk·check_health)로 나눈 뒤, 본문에서 차례로 불러 엮으면 실전 자동화 스크립트가 된다. cron에 한 줄 등록하면 사람 없이 매일 자동 실행된다.

🙋 학생 질문 — "튜터님, 이 긴 스크립트를 처음부터 한 번에 다 짜야 하나요? 막막한데요."

전혀요. 오늘 우리가 한 것처럼 조각으로 나눠 하나씩 만드는 게 정석이에요. 실무 개발자도 이렇게 합니다.

비결은 "작게 만들어 바로 확인"이에요. backup_logs 함수 하나 만들고 그것만 돌려봐요. 잘 되면 check_disk를 더하고 또 돌려보고. 한 조각씩 확인하며 쌓으면, 어디서 문제가 생겨도 방금 더한 그 조각만 보면 되니 훨씬 쉬워요. 처음부터 50줄을 다 쓰고 한 번에 돌리면 어디가 틀렸는지 찾기 어렵죠.

그리고 잘 만든 스크립트는 대부분 남이 만든 걸 가져다 고쳐 써요. 인터넷에 좋은 백업·점검 스크립트가 많거든요. 오늘 배운 if·for·함수를 읽을 줄 알면, 남의 스크립트를 가져와 내 경로와 조건에 맞게 고치는 것도 자유로워져요. 오늘 배운 건 "처음부터 다 짜는 법"이 아니라 "스크립트를 읽고 고치고 이어 붙이는 법"이에요. 그거면 충분합니다.


마무리

오늘 우리는 흩어진 명령에 흐름을 입혀 진짜 자동화로 묶었어요. 상황을 보고 갈라지고(if·case), 반복하고(for·while), 묶어 재사용하고(함수), 마지막엔 이 과목에서 배운 모든 명령을 하나의 점검 스크립트로 완성했죠. 핵심을 셋으로 정리할게요.

오늘 배운 핵심 세 가지

💡 하나if [ 조건 ]; then ... fi로 상황에 따라 갈라지고, elif/else/case로 여러 갈래를 만든다. 모든 명령은 종료 코드($?, 성공 0)를 남기며, if는 이 값을 보고 판단한다.

💡 for는 정해진 목록(*.log 등)을, while은 조건이 참인 동안, until은 참이 될 때까지 반복한다. 같은 명령 묶음은 이름() { ... } 함수로 묶어 재사용하고, local로 함수 안 변수를 가둔다.

💡 — 설정값을 변수로 모으고 할 일을 함수로 나눈 뒤 본문에서 엮으면 실전 자동화 스크립트가 된다. tar·df·curl 같은 명령을 조건·반복으로 묶고, cron에 한 줄 등록하면 사람 없이 매일 자동으로 돈다.

이 과목을 마치며 — 열두 모듈의 여정

여기까지 온 여러분, 정말 고생 많았어요. 처음 A-1에서 "검은 창이 무섭다"로 시작했던 걸 떠올려보세요. 지금 우리가 어디까지 왔는지 한눈에 그려볼게요.

텍스트
  A-1~A-3   터미널·파일·권한        파일시스템을 누비고 권한을 다룬다
  A-4~A-6   파이프·sed/awk·편집기    텍스트를 흐름으로 가공하고 직접 편집한다
  A-7~A-8   프로세스·서비스          살아 움직이는 시스템을 다룬다
  A-9~A-10  네트워크·원격 운영       SSH로 원격 서버에 들어가 운영한다
  A-11~A-12 bash 스크립팅            그 모든 걸 스크립트로 자동화한다

이제 여러분은 터미널을 열어 파일시스템을 누비고, 권한을 이해해 "Permission denied"를 스스로 풀고, 파이프와 grep·sed·awk로 텍스트를 다루고, 프로세스와 서비스를 관리하고, SSH로 원격 서버에 들어가, bash 스크립트로 반복을 자동화할 수 있어요. 검은 창이 더는 무섭지 않은, 서버를 다루는 개발자의 든든한 기본기를 갖춘 거예요.

다음 여정 — infra-devops로

그런데 우리가 오늘 한 자동화는 "리눅스 한 대 안에서, 내 손으로" 한 거예요. 현업에서는 이 흐름을 더 크게 확장합니다. 여러 대의 서버를 컨테이너로 묶고(A-10에서 개념만 짚은 그 Docker죠), 코드를 올리면 자동으로 빌드·배포되는 파이프라인을 걸고, 수십 대를 한 번에 관리하죠.

그게 바로 다음 과목 infra-devops예요. 오늘 우리가 cron으로 한 대 서버에서 자동화한 그 일을, infra-devops에서는 컨테이너와 CI/CD 파이프라인으로 여러 대에 걸쳐 자동화합니다. 그 과목이 "리눅스 셸은 친숙하다"고 전제하고 시작할 수 있는 건, 바로 여러분이 이 열두 모듈을 거쳐왔기 때문이에요. 단단한 땅을 다졌으니, 이제 그 위에 더 큰 걸 쌓으러 갈 차례예요. 그동안 정말 수고 많았습니다.


과제

오늘 과제는 지금까지 배운 흐름 제어를 직접 손에 익히는 실습이에요. 전부 여러분 자신의 리눅스 환경에서 만들어 돌려보세요. 무한 반복에 빠져도 Ctrl + C로 빠져나오면 되니, 겁내지 말고 시도하세요.

[기초] 인자를 검사하는 스크립트 만들기

위치 인자로 파일 이름을 하나 받아($1), if [ -f "$1" ]로 그 파일이 있는지 검사하는 checkfile.sh를 만드세요. 있으면 "○○ 파일이 있습니다", 없으면 "○○ 파일이 없습니다"를 출력하게 합니다. 실제로 있는 파일과 없는 파일 이름을 각각 넣어 실행해보고, 인자를 아예 안 줬을 때는 어떻게 되는지도 살펴보세요.

[응용] 반복으로 여러 파일 한 번에 처리하기

연습 폴더에 .txt 파일을 몇 개 만들어두고, for로 모든 .txt 파일을 돌면서 각 파일의 이름과 줄 수(wc -l, A-2에서 배웠죠)를 출력하는 스크립트를 만드세요. 여유가 되면 if를 더해 "줄 수가 0인 빈 파일"은 따로 표시해보세요.

[심화] 나만의 점검 스크립트 만들기

오늘 만든 nightly-check.sh를 참고해, 함수를 두 개 이상 가진 나만의 점검 스크립트를 만들어보세요. 예를 들어 "연습 폴더를 백업하는 함수"와 "특정 폴더의 파일 개수를 세어 보고하는 함수"를 만들고, 본문에서 차례로 부르는 거예요. 다 됐으면, 이 스크립트를 매일 자동으로 돌리려면 cron에 어떤 한 줄을 등록해야 할지 적어보세요(A-8의 5필드를 떠올리며).


생각해볼 주제

1. 조건문에서 $?(종료 코드)가 보이지 않게 일하는 방식

if grep -q ERROR app.log; then ...처럼, if 뒤에 [ ] 없이 명령을 바로 쓸 수 있습니다. if가 사실 명령의 종료 코드를 보고 갈라진다는 점을 떠올리며, if [ "$x" = "1" ][ ]도 결국 하나의 "명령"이라는 사실이 무슨 뜻인지 생각해보세요. 종료 코드라는 약속이 왜 모든 명령에 공통으로 있는지와 연결해보면 좋습니다.

2. 무한 반복은 위험하기만 한 걸까

while 조건을 잘못 써서 빠지는 무한 반복은 분명 사고예요. 그런데 일부러 끝나지 않는 반복(while true)을 만드는 경우도 있습니다. 예를 들어 "서버가 살아있는지 1분마다 영원히 점검하는" 감시 프로그램이요. 끝나지 않는 반복이 사고일 때와 의도된 설계일 때의 차이가 무엇인지, 그리고 의도된 무한 반복은 어떻게 안전하게 멈추거나 관리하는지 생각해보세요.

3. 자동화 스크립트는 "실패"를 어떻게 다뤄야 할까

오늘 만든 점검 스크립트는 백업·디스크·헬스체크를 차례로 합니다. 그런데 만약 백업이 실패했는데도 스크립트가 아무 일 없다는 듯 다음으로 넘어가 "점검 끝"이라고 남긴다면 어떨까요? 사람이 지켜보지 않는 자동화일수록 "조용한 실패"가 위험합니다. 종료 코드($?)와 조건문을 활용해, 자동화 스크립트가 실패를 어떻게 감지하고 알려야 할지 생각해보세요.

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안입니다. 흐름 제어는 직접 짜서 돌려보고, 일부러 틀려도 봐야 손에 익어요. 무한 반복에 빠져도 Ctrl + C로 빠져나오면 되니 겁내지 말고 시도하세요. 출력의 날짜·숫자는 환경에 따라 다른 게 당연하니, 값보다 흐름이 맞는지를 보면 됩니다.


🎯 [과제 1 예시답안] 인자를 검사하는 스크립트 만들기

채점 포인트

확인 항목 무엇을 보면 되는가
위치 인자 파일 이름을 $1로 받았는가
파일 검사 if [ -f "$1" ]로 존재 여부를 갈랐는가
큰따옴표 "$1"을 큰따옴표로 감쌌는가
두 경우 확인 있는 파일·없는 파일을 각각 넣어 봤는가

풀이 예시

Bash
#!/bin/bash
if [ -f "$1" ]; then
  echo "$1 파일이 있습니다"
else
  echo "$1 파일이 없습니다"
fi
Bash
$ chmod +x checkfile.sh
$ touch exists.txt
$ ./checkfile.sh exists.txt
exists.txt 파일이 있습니다
$ ./checkfile.sh ghost.txt
ghost.txt 파일이 없습니다
$ ./checkfile.sh
 파일이 없습니다

$1로 받은 파일 이름을 if [ -f "$1" ]로 검사해, 있으면 then, 없으면 else로 갈라졌어요. 마지막에 인자 없이 실행하면 $1이 비어서 " 파일이 없습니다"처럼 이름 자리가 빈 채로 나오죠. 이건 다음 단계로 가는 좋은 힌트예요. 제대로 만들려면 맨 앞에 if [ -z "$1" ]; then echo "사용법: $0 파일이름"; exit 1; fi로 "인자를 안 줬으면 사용법을 안내하고 멈추기"를 더해주면 더 단단한 스크립트가 됩니다.

💡 튜터의 한마디: -f로 파일이 있는지 먼저 확인하는 습관은 자동화에서 정말 중요해요. 없는 파일을 백업하거나 지우려다 에러가 나는 걸, 검사 한 줄로 미리 막을 수 있거든요. "행동하기 전에 확인한다"는 A-2에서 배운 rm 조심 습관과도 통하는 결이에요.


🎯 [과제 2 예시답안] 반복으로 여러 파일 한 번에 처리하기

채점 포인트

확인 항목 무엇을 보면 되는가
for 반복 for f in *.txt로 모든 txt 파일을 돌았는가
줄 수 세기 wc -l로 각 파일의 줄 수를 구했는가
변수에 담기 명령 치환 $(wc -l < "$f")로 결과를 변수에 담았는가
빈 파일 분기 if로 줄 수 0인 파일을 따로 표시했는가

풀이 예시

Bash
#!/bin/bash
for f in *.txt; do
  lines=$(wc -l < "$f")
  if [ "$lines" -eq 0 ]; then
    echo "$f: $lines 줄 (빈 파일)"
  else
    echo "$f: $lines 줄"
  fi
done
Bash
$ ./counttxt.sh
a.txt: 1 줄
b.txt: 2 줄
empty.txt: 0 줄 (빈 파일)

for f in *.txt로 모든 txt 파일을 하나씩 돌면서, wc -l < "$f"로 줄 수를 세어 lines에 담았어요. wc -l < "$f"처럼 리다이렉션(<, A-4에서 배웠죠)으로 파일을 넘기면 파일 이름 없이 숫자만 깔끔하게 나와서 비교하기 좋아요. 그 값이 0이면 if로 "빈 파일"이라고 따로 표시했고요. 반복과 조건이 함께 쓰인 전형적인 모습이에요.

💡 튜터의 한마디: for 파일 in *.확장자; do ... done은 자동화에서 가장 자주 쓰는 골격이에요. "폴더 안의 모든 파일에 같은 일을 한다"는 건 백업·변환·정리 어디에나 나오거든요. 오늘 만든 점검 스크립트의 백업도 결국 이 반복의 친척이에요.


🎯 [과제 3 예시답안] 나만의 점검 스크립트 만들기

채점 포인트

확인 항목 무엇을 보면 되는가
함수 2개 이상 동작을 함수로 나눠 정의했는가
본문 호출 본문에서 함수를 차례로 불렀는가
명령 엮기 tar·ls·wc 등 배운 명령을 함수 안에 엮었는가
cron 한 줄 5필드 + 절대경로로 예약 줄을 적었는가

풀이 예시

Bash
#!/bin/bash
TARGET="mydata"

backup_dir() {
  local archive="backup-$(date +%Y-%m-%d).tar.gz"
  tar -czf "$archive" "$TARGET"
  echo "백업 완료: $archive"
}

count_files() {
  local n=$(ls -1 "$TARGET" | wc -l | tr -d ' ')
  echo "$TARGET 안의 파일 개수: $n"
}

backup_dir
count_files
Bash
$ ./mycheck.sh
백업 완료: backup-2026-06-17.tar.gz
mydata 안의 파일 개수: 2

backup_dircount_files 두 함수로 동작을 나누고, 본문에서 차례로 불렀어요. count_files 안에서는 A-4의 파이프(ls | wc -l)와 tr로 개수만 깔끔히 뽑았고요. 함수로 나눠두니 나중에 "백업만 빼고 싶다"거나 "점검을 하나 더 추가하고 싶다"는 요구가 와도 본문의 호출 줄만 손대면 되죠.

이 스크립트를 매일 새벽 2시에 자동으로 돌리려면, crontab -e에 이렇게 한 줄을 등록합니다.

Bash
0 2 * * * /home/alice/bin/mycheck.sh >> /home/alice/mycheck.log 2>&1

0 2 * * *가 "매일 새벽 2시", 경로는 cron을 위해 절대경로로(A-8의 함정), >> ... 2>&1로 출력과 에러를 로그에 쌓아둡니다.

💡 튜터의 한마디: 동작을 함수로 쪼개 두면 스크립트가 레고 블록처럼 됩니다. 블록을 더하고 빼며 내게 맞는 점검 스크립트를 키워가면 돼요. 현업에서 운영하는 점검 스크립트도 처음엔 이렇게 함수 두세 개로 시작해서, 필요할 때마다 블록을 하나씩 늘려간 것들이에요.


🤔 [생각해볼 주제 1] 조건문에서 $?(종료 코드)가 보이지 않게 일하는 방식

문제 상황 요약

if grep -q ERROR app.log; then ...처럼 if 뒤에 [ ] 없이 명령을 바로 쓸 수 있습니다. if [ "$x" = "1" ][ ]도 사실은 하나의 "명령"이라는데, 이게 무슨 뜻일까요?

튜터의 가이드 및 해설

if의 진짜 정체를 들여다보면 이래요. if는 "뒤에 오는 명령을 실행해보고, 그 명령이 성공(종료 코드 0)했으면 then으로 간다"입니다. 조건이 참이냐 거짓이냐가 아니라, 명령이 성공이냐 실패냐를 보는 거예요.

그래서 if grep -q ERROR app.log는 자연스러워요. grep은 찾으면 종료 코드 0(성공), 못 찾으면 0이 아닌 값(실패)을 남기거든요. if가 그걸 보고 "찾았으면 then"으로 가는 거죠.

그럼 if [ "$x" = "1" ][ ]는 뭘까요? 놀랍게도 [는 그 자체로 하나의 명령이에요(원래 이름은 test고요). [ "$x" = "1" ]은 "x가 1과 같은지 검사하는 명령"을 실행하는 거고, 그 명령이 같으면 종료 코드 0, 다르면 1을 남겨요. 결국 if [ ... ]if grep ...과 똑같은 구조였던 거예요. if는 늘 명령의 종료 코드만 봅니다.

이래서 종료 코드라는 약속이 모든 명령에 공통으로 존재하는 게 중요해요. 모든 명령이 "성공은 0, 실패는 0이 아닌 값"이라는 같은 언어로 결과를 말하기 때문에, if·&&·|| 같은 흐름 제어가 어떤 명령과도 맞물려 돌아갈 수 있는 거죠.

💡 핵심을 한마디로

if는 조건의 참·거짓이 아니라 명령의 종료 코드(성공 0)를 본다. [ ]조차 test라는 하나의 명령이라, 모든 명령이 종료 코드라는 공통 언어로 결과를 말하기에 흐름 제어가 어떤 명령과도 맞물린다.


🤔 [생각해볼 주제 2] 무한 반복은 위험하기만 한 걸까

문제 상황 요약

while 조건을 잘못 써서 빠지는 무한 반복은 분명 사고입니다. 그런데 일부러 끝나지 않는 반복(while true)을 만드는 경우도 있어요. 둘의 차이는 무엇일까요?

튜터의 가이드 및 해설

핵심은 "의도했는가"예요. 사고로 빠지는 무한 반복은 카운터를 안 키우는 등 실수로 조건이 영영 참이 된 경우예요. 멈추려던 게 안 멈추니 화면이 도배되고 자원을 낭비하죠.

반면 의도된 무한 반복도 분명히 있어요. "서버가 살아있는지 1분마다 영원히 점검하는" 감시 프로그램이 대표적이에요. 이건 애초에 끝나면 안 되는 일이거든요. while true; do 점검; sleep 60; done처럼, 한 번 돌 때마다 sleep으로 쉬었다가 다시 도는 식으로 짜요. 여기서 sleep이 중요해요. 쉬는 구간이 없으면 1초에 수천 번 도는 폭주가 되니까요.

차이를 가르는 기준은 셋이에요. 첫째, 그 반복이 끝나지 않는 게 설계 의도인가(감시는 그렇고, 카운트다운은 아니죠). 둘째, 한 바퀴마다 적절히 쉬어(sleep) 자원을 안 잡아먹는가. 셋째, 멈추거나 관리할 방법이 마련돼 있는가예요.

의도된 무한 반복을 다루는 법은 이미 배운 것들로 충분해요. 잠깐 멈추려면 A-7의 Ctrl + C, 화면 닫혀도 계속 돌리려면 A-7의 nohup이나 백그라운드 실행, 부팅 때부터 알아서 살아있게 하려면 A-8의 systemd 서비스로 등록하죠. 사실 systemd로 관리하는 서비스 상당수가 속으로는 이런 "의도된 무한 반복"이에요.

💡 핵심을 한마디로

무한 반복은 의도했는가로 갈린다. 사고는 조건 실수로 안 멈추는 것이고, 감시 프로그램처럼 의도된 무한 반복은 한 바퀴마다 sleep으로 쉬고 systemd 등으로 관리한다는 점이 다르다.


🤔 [생각해볼 주제 3] 자동화 스크립트는 "실패"를 어떻게 다뤄야 할까

문제 상황 요약

오늘 만든 점검 스크립트는 백업·디스크·헬스체크를 차례로 합니다. 그런데 백업이 실패했는데도 아무 일 없다는 듯 "점검 끝"이라고 남긴다면 어떨까요?

튜터의 가이드 및 해설

사람이 지켜보지 않는 자동화일수록 "조용한 실패"가 가장 위험해요. 매일 백업이 도는 줄 알았는데, 사실 한 달 전부터 실패하고 있었고 아무도 몰랐다면? 정작 복구가 필요한 순간에 백업이 없는 최악의 상황이 벌어지죠.

그래서 자동화 스크립트는 실패를 감지하고 알리도록 설계해야 해요. 여기서 오늘 배운 종료 코드($?)와 조건문이 무기예요. 예를 들어 백업 명령 바로 뒤에서 종료 코드를 확인하는 거죠.

텍스트
  tar -czf "$archive" -C "$LOG_DIR" .
  if [ $? -ne 0 ]; then
      log "백업 실패! 확인이 필요합니다"
      exit 1
  fi

tar가 실패하면($?가 0이 아니면) 그냥 넘어가지 않고 실패를 로그에 또렷이 남기고, 필요하면 exit 1로 스크립트 전체가 "나 실패했어"라는 종료 코드를 남기며 멈춰요. 그러면 이 스크립트를 부른 쪽(예: cron이나 상위 스크립트)이 그 실패를 알아챌 수 있죠.

한 걸음 더 나아가면, 실패를 사람에게 알리는 것까지 자동화해요. 실패 로그를 모아 메일이나 메신저로 알림을 보내는 거죠. 그건 이 과목을 넘어 infra-devops에서 본격적으로 다룰 모니터링·알림의 영역이에요. 다만 그 출발점은 오늘 배운 "종료 코드를 확인해 실패를 감지한다"는 한 줄이에요. 자동화의 완성도는 "잘 될 때"가 아니라 "실패할 때 어떻게 행동하는가"에서 갈립니다.

💡 핵심을 한마디로

사람이 안 보는 자동화에서 가장 위험한 건 조용한 실패다. 명령마다 종료 코드($?)를 확인해 실패를 감지·기록하고, 필요하면 exit로 알린다. 자동화의 완성도는 잘 될 때가 아니라 실패할 때의 행동에서 갈린다.

전체 목록 리눅스
이 토픽을 끝까지 봤어요 🎉 다른 트랙도 둘러보기 →