Skip to content

필사 모드: Shell 기본기부터 고급 운용까지: 엔지니어를 위한 실전 Shell 가이드

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

서버에 SSH 접속하고, CI/CD 파이프라인을 작성하고, 로그를 분석하고, 배포 스크립트를 돌린다. 엔지니어의 하루는 Shell 위에서 시작되고 Shell 위에서 끝난다. 하지만 의외로 많은 개발자가 Shell의 기본 동작 원리를 깊이 이해하지 못한 채 "되는 명령어"만 반복한다.

이 글에서는 **Bash/Zsh 기본 문법**에서 출발하여 **파이프라인, 프로세스 치환, 시그널 핸들링, 성능 최적화**까지 엔지니어가 알아야 할 Shell 기법을 실전 예제 중심으로 다룬다.

1. Shell 선택: Bash vs Zsh vs Fish

| 항목 | Bash | Zsh | Fish |

| ------------------- | -------------------- | ------------------------- | -------------- |

| **기본 탑재** | 대부분 Linux 배포판 | macOS (Catalina+) | 별도 설치 |

| **POSIX 호환** | 거의 완전 | 거의 완전 | 비호환 |

| **자동완성** | 기본 수준 | 플러그인으로 강력 | 기본 내장 최강 |

| **스크립트 호환** | 표준 | Bash 호환 모드 지원 | 독자 문법 |

| **프롬프트 커스텀** | PS1 직접 수정 | Oh My Zsh / Powerlevel10k | 내장 설정 |

| **추천 용도** | 서버 스크립트, CI/CD | 로컬 개발 환경 | 개인 터미널 |

> **실전 원칙**: 서버 스크립트는 `#!/usr/bin/env bash`로 작성하고, 로컬 인터랙티브 셸은 Zsh를 쓴다.

2. 기본기: 변수·조건·반복

2.1 변수 선언과 스코프

로컬 변수 (현재 셸에서만)

APP_NAME="my-service"

환경 변수 (자식 프로세스에 전달)

export DB_HOST="db.prod.internal"

readonly - 실수로 덮어쓰기 방지

readonly CONFIG_PATH="/etc/app/config.yaml"

변수 기본값 패턴

: "${LOG_LEVEL:=info}" # 미설정 시 info 할당

: "${TIMEOUT:?TIMEOUT 환경변수 필수}" # 미설정 시 에러 종료

echo "${USER:-unknown}" # 미설정 시 unknown 출력 (할당 안 함)

2.2 조건문 패턴

문자열 비교 - [[ ]] 사용 (Bash/Zsh 확장)

if [[ "$ENV" == "production" ]]; then

echo "프로덕션 모드"

elif [[ "$ENV" =~ ^(staging|dev)$ ]]; then

echo "비프로덕션 환경: $ENV"

else

echo "알 수 없는 환경"

fi

파일 테스트

[[ -f /etc/hosts ]] # 파일 존재

[[ -d /var/log ]] # 디렉터리 존재

[[ -r "$file" ]] # 읽기 권한

[[ -s "$file" ]] # 파일 크기 > 0

[[ "$f1" -nt "$f2" ]] # f1이 f2보다 최신

산술 비교 - (( )) 사용

if (( retries > 3 )); then

echo "재시도 한도 초과"

fi

2.3 반복문 패턴

파일 목록 순회 - glob 사용 (ls 파싱 금지!)

for f in /var/log/*.log; do

[[ -f "$f" ]] || continue

echo "처리 중: $f ($(wc -l < "$f") 줄)"

done

C-style for

for (( i=0; i<10; i++ )); do

curl -s "http://api.local/health" > /dev/null && break

sleep 1

done

while + read - 파일/명령 출력 한 줄씩 처리

while IFS=',' read -r name email role; do

echo "사용자 생성: $name ($role)"

done < users.csv

무한 루프 + 탈출 조건

while true; do

status=$(curl -s -o /dev/null -w '%{http_code}' http://api/health)

[[ "$status" == "200" ]] && break

sleep 5

done

3. 파이프라인 심화

3.1 파이프라인 기본 원리

파이프(`|`)는 앞 명령의 stdout을 뒤 명령의 stdin에 연결한다. 각 명령은 **별도 서브셸**에서 동시 실행된다.

접속 IP Top 10

awk '{print $1}' /var/log/nginx/access.log \

| sort \

| uniq -c \

| sort -rn \

| head -10

pipefail - 파이프라인 중간 실패 감지

set -o pipefail

curl -s "$URL" | jq '.items[]' | wc -l

curl 실패 시 전체 파이프라인 종료 코드 ≠ 0

3.2 프로세스 치환 (Process Substitution)

두 명령의 출력을 **파일처럼** 다른 명령에 전달한다.

두 서버의 패키지 목록 비교

diff <(ssh server1 'rpm -qa | sort') <(ssh server2 'rpm -qa | sort')

두 API 응답 비교

diff <(curl -s api-v1/users | jq -S .) <(curl -s api-v2/users | jq -S .)

tee + 프로세스 치환: 한 스트림을 여러 곳에 동시 전달

cat access.log \

| tee >(grep 'ERROR' > errors.log) \

| tee >(awk '{print $1}' | sort -u > unique_ips.txt) \

| wc -l

3.3 리다이렉션 고급 패턴

stderr만 캡처

errors=$(command 2>&1 1>/dev/null)

stdout + stderr 모두 파일로

command &> output.log # Bash 4+

command > output.log 2>&1 # POSIX 호환

Here String

grep "pattern" <<< "$variable"

File Descriptor 활용

exec 3>/tmp/audit.log # FD 3 열기

echo "작업 시작: $(date)" >&3

do_something

echo "작업 완료: $(date)" >&3

exec 3>&- # FD 3 닫기

4. 함수와 에러 처리

4.1 함수 정의 패턴

방어적 함수 구조

log() {

local level="${1:?level 필수 (INFO|WARN|ERROR)}"

local message="${2:?message 필수}"

printf '[%s] [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$message" >&2

}

retry() {

local max_attempts="${1:?}"

local delay="${2:?}"

shift 2

local attempt=1

until "$@"; do

if (( attempt >= max_attempts )); then

log ERROR "명령 실패 ($max_attempts회 시도): $*"

return 1

fi

log WARN "재시도 $attempt/$max_attempts (${delay}s 후): $*"

sleep "$delay"

(( attempt++ ))

done

}

사용

retry 5 3 curl -sf http://api.internal/health

4.2 안전한 스크립트 헤더

#!/usr/bin/env bash

set -euo pipefail

IFS=$'\n\t'

set -e: 명령 실패 시 즉시 종료

set -u: 미정의 변수 사용 시 에러

set -o pipefail: 파이프라인 중간 실패 감지

IFS: 단어 분리 기준을 줄바꿈·탭으로 제한

클린업 트랩

cleanup() {

local exit_code=$?

rm -f "$TMPFILE"

log INFO "종료 (exit code: $exit_code)"

exit "$exit_code"

}

trap cleanup EXIT

trap 'log ERROR "라인 $LINENO에서 에러 발생"; exit 1' ERR

TMPFILE=$(mktemp)

5. 텍스트 처리 파이프라인

5.1 도구 비교표

| 도구 | 용도 | 속도 | 복잡성 |

| ----------- | -------------------- | --------- | ------ |

| `grep` | 패턴 매칭·필터링 | 매우 빠름 | 낮음 |

| `sed` | 스트림 편집·치환 | 빠름 | 중간 |

| `awk` | 필드 기반 처리·집계 | 빠름 | 높음 |

| `jq` | JSON 처리 | 빠름 | 중간 |

| `yq` | YAML 처리 | 보통 | 중간 |

| `cut/paste` | 단순 필드 추출·병합 | 매우 빠름 | 낮음 |

| `xargs` | 표준입력 → 인수 변환 | 빠름 | 중간 |

5.2 실전 예제

1. 로그에서 5xx 에러 요청 경로 Top 10

awk '$9 ~ /^5[0-9]{2}$/ {print $7}' access.log \

| sort | uniq -c | sort -rn | head -10

2. JSON API 응답에서 특정 필드 추출 + CSV 변환

curl -s https://api.example.com/users \

| jq -r '.[] | [.id, .name, .email] | @csv'

3. YAML 설정에서 이미지 태그 일괄 변경

yq -i '.spec.template.spec.containers[].image |= sub("v1\\.2\\.3", "v1.2.4")' \

k8s/deployment.yaml

4. 대용량 로그 병렬 검색 (xargs + grep)

find /var/log -name '*.log' -mtime -1 -print0 \

| xargs -0 -P4 grep -l 'OutOfMemoryError'

5. CSV 3번째 컬럼 합계

awk -F',' '{sum += $3} END {printf "합계: %.2f\n", sum}' sales.csv

6. 시그널 핸들링과 프로세스 관리

6.1 주요 시그널

| 시그널 | 번호 | 기본 동작 | 용도 |

| --------- | ---- | ----------- | ----------------- |

| `SIGHUP` | 1 | 종료 | 데몬 설정 리로드 |

| `SIGINT` | 2 | 종료 | Ctrl+C |

| `SIGQUIT` | 3 | 코어 덤프 | Ctrl+\ |

| `SIGKILL` | 9 | 강제 종료 | 트랩 불가 |

| `SIGTERM` | 15 | 종료 | 정상 종료 요청 |

| `SIGUSR1` | 10 | 사용자 정의 | 로그 레벨 변경 등 |

| `SIGSTOP` | 19 | 일시정지 | 트랩 불가 |

6.2 Graceful Shutdown 패턴

#!/usr/bin/env bash

set -euo pipefail

RUNNING=true

CHILD_PID=""

shutdown() {

log INFO "종료 시그널 수신, graceful shutdown 시작"

RUNNING=false

if [[ -n "$CHILD_PID" ]]; then

kill -TERM "$CHILD_PID" 2>/dev/null || true

wait "$CHILD_PID" 2>/dev/null || true

fi

}

trap shutdown SIGTERM SIGINT

while $RUNNING; do

process_job &

CHILD_PID=$!

wait "$CHILD_PID" || true

CHILD_PID=""

sleep 5

done

log INFO "정상 종료"

6.3 Job Control

백그라운드 실행 + 완료 대기

build_frontend &

pid1=$!

build_backend &

pid2=$!

wait "$pid1" "$pid2"

echo "빌드 완료"

nohup - 세션 종료 후에도 실행 유지

nohup long_task.sh > /var/log/task.log 2>&1 &

disown

timeout - 명령 실행 시간 제한

timeout 30s curl -s http://slow-api.com/data

7. 배열과 연관 배열

인덱스 배열

servers=("web01" "web02" "web03" "db01")

echo "서버 수: ${#servers[@]}"

echo "첫 번째: ${servers[0]}"

echo "전체: ${servers[@]}"

배열 슬라이스

web_servers=("${servers[@]:0:3}")

배열에 추가

servers+=("cache01")

연관 배열 (Bash 4+)

declare -A service_ports

service_ports=(

[nginx]=80

[api]=8080

[redis]=6379

[postgres]=5432

)

for svc in "${!service_ports[@]}"; do

echo "$svc → ${service_ports[$svc]}"

done

배열로 안전한 명령 구성

curl_opts=(

-s

--max-time 10

--retry 3

-H "Authorization: Bearer $TOKEN"

-H "Content-Type: application/json"

)

curl "${curl_opts[@]}" "$API_URL"

8. 고급 패턴

8.1 Subshell vs Command Group

Subshell () - 별도 프로세스, 부모 변수 변경 없음

(cd /tmp && tar czf backup.tar.gz /var/data)

현재 디렉터리 변경 없음

Command Group {} - 현재 셸에서 실행

{

echo "=== 시스템 정보 ==="

uname -a

free -h

df -h

} > system_report.txt

8.2 동적 변수명 (nameref)

Bash 4.3+ nameref

setup_db() {

local -n result=$1 # nameref

result="postgresql://localhost:5432/app"

}

setup_db DB_URL

echo "$DB_URL" # postgresql://localhost:5432/app

8.3 병렬 실행 패턴

GNU parallel을 이용한 병렬 처리

cat server_list.txt | parallel -j10 'ssh {} "df -h / | tail -1"'

xargs 병렬

find . -name '*.png' -print0 \

| xargs -0 -P$(nproc) -I{} convert {} -resize 50% resized/{}

wait + 배열로 병렬 제어

pids=()

for host in web0{1..5}; do

deploy.sh "$host" &

pids+=($!)

done

failed=0

for pid in "${pids[@]}"; do

wait "$pid" || (( failed++ ))

done

echo "배포 완료: 실패 $failed건"

9. 성능 최적화 체크리스트

| 항목 | 느린 패턴 | 빠른 패턴 |

| ----------------- | --------------------------------------------- | ---------------------------------- |

| 루프 내 외부 명령 | `for f in ...; do cat "$f" \| grep ...; done` | `grep -r ... /path/` |

| 서브셸 남발 | `result=$(echo "$var" \| sed ...)` | `result="${var//old/new}"` |

| 불필요한 파이프 | `cat file \| grep pattern` | `grep pattern file` |

| 정렬 후 유니크 | `sort \| uniq` | `sort -u` |

| 큰 파일 행 수 | `cat file \| wc -l` | `wc -l < file` |

| 파일 존재 확인 | `ls /path/file 2>/dev/null` | `[[ -f /path/file ]]` |

| 문자열에서 추출 | `echo "$s" \| cut -d. -f1` | `"${s%%.*}"` (Parameter Expansion) |

Parameter Expansion 주요 패턴

file="/var/log/nginx/access.log"

echo "${file##*/}" # access.log (경로 제거)

echo "${file%.*}" # /var/log/nginx/access (확장자 제거)

echo "${file%%/*}" # (빈 문자열, 첫 / 이전)

echo "${file%.log}.bak" # /var/log/nginx/access.bak

version="v1.2.3-rc1"

echo "${version#v}" # 1.2.3-rc1

echo "${version%-*}" # v1.2.3

echo "${version^^}" # V1.2.3-RC1 (대문자)

echo "${version,,}" # v1.2.3-rc1 (소문자)

echo "${#version}" # 10 (문자열 길이)

10. 실전 스크립트 템플릿

배포 스크립트

#!/usr/bin/env bash

set -euo pipefail

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

readonly APP_NAME="${1:?사용법: $0 <app-name> <version>}"

readonly VERSION="${2:?사용법: $0 <app-name> <version>}"

readonly DEPLOY_ENV="${DEPLOY_ENV:-staging}"

readonly LOG_FILE="/var/log/deploy/${APP_NAME}-$(date +%Y%m%d-%H%M%S).log"

--- 로깅 ---

log() { printf '[%s] [%-5s] %s\n' "$(date +%T)" "$1" "$2" | tee -a "$LOG_FILE" >&2; }

info() { log INFO "$1"; }

warn() { log WARN "$1"; }

die() { log ERROR "$1"; exit 1; }

--- 사전 점검 ---

preflight() {

info "사전 점검 시작"

command -v docker >/dev/null || die "docker가 설치되어 있지 않습니다"

command -v kubectl >/dev/null || die "kubectl이 설치되어 있지 않습니다"

local context

context=$(kubectl config current-context)

[[ "$context" == *"$DEPLOY_ENV"* ]] || die "kubectl context($context)가 $DEPLOY_ENV 와 일치하지 않습니다"

info "사전 점검 통과 (context: $context)"

}

--- 배포 ---

deploy() {

info "$APP_NAME:$VERSION → $DEPLOY_ENV 배포 시작"

kubectl set image "deployment/$APP_NAME" \

"$APP_NAME=registry.internal/$APP_NAME:$VERSION" \

--record

info "롤아웃 대기 중..."

if ! kubectl rollout status "deployment/$APP_NAME" --timeout=300s; then

warn "롤아웃 실패, 롤백 실행"

kubectl rollout undo "deployment/$APP_NAME"

die "배포 실패 → 롤백 완료"

fi

info "배포 성공"

}

--- 메인 ---

main() {

mkdir -p "$(dirname "$LOG_FILE")"

info "=== $APP_NAME $VERSION 배포 ($DEPLOY_ENV) ==="

preflight

deploy

info "=== 배포 완료 ==="

}

main "$@"

마무리 체크리스트

- [ ] 스크립트 상단에 `set -euo pipefail` 선언했는가?

- [ ] 모든 변수를 큰따옴표(`"$var"`)로 감쌌는가?

- [ ] 외부 입력(사용자, 파일명)을 그대로 명령에 넣지 않았는가?

- [ ] `trap`으로 임시 파일·프로세스 정리를 보장했는가?

- [ ] 루프 안에서 불필요한 외부 명령 호출을 줄였는가?

- [ ] ShellCheck(`shellcheck script.sh`)로 정적 분석을 통과했는가?

- [ ] POSIX 호환이 필요한 환경이면 Bash 확장 문법을 피했는가?

Shell은 "알면 빠르고, 모르면 위험한" 도구다. 기본기를 탄탄히 다지고 안전한 패턴을 습관화하면, 어떤 서버 환경에서도 자신 있게 문제를 해결할 수 있다.

현재 단락 (1/277)

서버에 SSH 접속하고, CI/CD 파이프라인을 작성하고, 로그를 분석하고, 배포 스크립트를 돌린다. 엔지니어의 하루는 Shell 위에서 시작되고 Shell 위에서 끝난다. 하지만...

작성 글자: 0원문 글자: 8,562작성 단락: 0/277