Linux Command Line & Bash Scripting Mastery · 강의

멱등적 스크립트와 지수 백오프 재시도 로직

다시 실행해도 안전한 작업을 설계하고 불안정한 외부 호출에 지수 백오프를 추가합니다.

레슨 4/413개 단계

멱등적 스크립트와 지수 백오프 재시도 로직은(는) CoddyKit의 무료 Linux Command Line & Bash Scripting Mastery 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Linux Command Line & Bash Scripting Mastery 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Linux Command Line & Bash Scripting Mastery 강의에는 총 4개의 강의가 포함되어 있습니다.

멱등성이란 무엇이며 왜 중요한가

멱등성이란 동일한 작업을 여러 번 실행해도 한 번 실행한 것과 같은 결과가 나오는 특성입니다. 배시 스크립팅에서 이는 매우 중요합니다. 스크립트가 충돌할 수 있고, 네트워크가 끊길 수 있으며, 사람이 실수로 작업을 다시 실행할 수 있기 때문입니다.

  • 멱등적이지 않은 스크립트로 사용자를 두 번 생성하면 실패하거나 데이터가 중복될 수 있습니다.
  • 멱등적인 스크립트는 먼저 이미 존재하는가?를 확인합니다.
  • 멱등적인 스크립트는 Cron 작업, CI 파이프라인, 재시도 반복문에서 안전하게 사용할 수 있습니다.

핵심 원칙은 행동하기 전에 확인하는 것입니다. 삭제하거나 새로 만드는 모든 작업은 사전 조건 확인으로 보호해야 합니다.

파일과 디렉터리 생성을 안전하게 처리하기

가장 일반적인 멱등성 패턴은 리소스를 만들기 전에 이미 존재하는지 확인하는 것입니다. 배시는 이를 간결한 한 줄 명령으로 작성할 수 있게 해 줍니다.

  • [ -d dir ] — 디렉터리가 존재하면 참
  • [ -f file ] — 일반 파일이 존재하면 참
  • mkdir -p — 디렉터리가 없을 때만 생성(내장된 멱등성)

가능하다면 수동 확인보다 -p와 --no-clobber 같은 내장 플래그를 우선 사용하십시오. 이러한 플래그는 원자적으로 동작하며 경쟁 조건에 안전합니다.

#!/usr/bin/env bash
set -euo pipefail

CONFIG_DIR="$HOME/.myapp"
CONFIG_FILE="$CONFIG_DIR/config.ini"

# Idempotent: mkdir -p never fails if dir already exists
mkdir -p "$CONFIG_DIR"

# Idempotent: only write config if it doesn't exist yet
if [ ! -f "$CONFIG_FILE" ]; then
  echo '[defaults]' > "$CONFIG_FILE"
  echo 'timeout=30' >> "$CONFIG_FILE"
  echo "Created $CONFIG_FILE"
else
  echo "Config already exists, skipping."
fi

멱등적인 사용자 및 그룹 관리

사용자나 그룹 추가와 같은 시스템 관리 작업은 멱등적이어야 합니다. 동일한 컴퓨터에서 스크립트를 다시 실행해도 오류가 발생하거나 중복 항목이 만들어지지 않아야 합니다.

  • id username은 사용자가 존재하면 0을 반환합니다.
  • getent group groupname은 그룹이 존재하는지 확인합니다.
  • 각 작업을 조건 확인으로 감싸 스크립트를 안전하게 다시 실행할 수 있도록 합니다.

이 패턴은 Ansible과 같은 구성 관리 도구의 기반입니다. 모든 작업은 조건으로 보호되는 멱등적 작업입니다.

#!/usr/bin/env bash
set -euo pipefail

APP_USER="apprunner"
APP_GROUP="appgroup"

# Idempotent group creation
if ! getent group "$APP_GROUP" &>/dev/null; then
  groupadd "$APP_GROUP"
  echo "Group '$APP_GROUP' created."
else
  echo "Group '$APP_GROUP' already exists."
fi

# Idempotent user creation
if ! id "$APP_USER" &>/dev/null; then
  useradd -m -g "$APP_GROUP" -s /bin/bash "$APP_USER"
  echo "User '$APP_USER' created."
else
  echo "User '$APP_USER' already exists."
fi

잠금 파일로 동시 실행 방지하기

멱등적인 스크립트라도 두 인스턴스가 동시에 실행되면 문제가 발생할 수 있습니다. 잠금 파일은 한 번에 하나의 인스턴스만 실행되도록 보장합니다.

  • 시작할 때 잠금 파일을 만들고 종료할 때 삭제합니다.
  • trap을 사용해 스크립트가 중단되더라도 잠금을 정리합니다.
  • 단일 경로에 대한 mkdir은 대부분의 Linux 파일 시스템에서 원자적으로 동작하므로 잠금에 touch를 사용하는 것보다 안전합니다.

잠금이 없으면 느린 Cron 작업과 수동 재실행이 충돌하여 공유 상태를 손상시킬 수 있습니다.

#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/myapp_deploy.lock"

# Atomic lock acquisition using mkdir
if ! mkdir "$LOCKFILE" 2>/dev/null; then
  echo "ERROR: Another instance is running (lock: $LOCKFILE). Exiting." >&2
  exit 1
fi

# Guarantee lock removal on any exit
trap 'rmdir "$LOCKFILE"; echo "Lock released."' EXIT

echo "Lock acquired. Running deployment..."
sleep 2   # simulate work
echo "Deployment complete."

상태 파일로 완료된 단계 추적하기

여러 단계로 구성된 스크립트(마이그레이션, 설치, 프로비저닝)에서는 상태 파일을 사용해 이미 완료된 단계를 추적할 수 있습니다. 각 단계는 실행하기 전에 상태 파일을 확인하고, 완료되면 상태 파일에 기록합니다.

  • 저렴하고 이식성이 뛰어나며 데이터베이스가 필요하지 않습니다.
  • 실패한 스크립트를 중단된 지점부터 다시 실행할 수 있습니다.
  • /var/lib/myapp/ 또는 ~/.myapp/state/와 같이 예측할 수 있는 위치에 상태를 저장합니다.

이 패턴은 apt, cloud-init, 데이터베이스 마이그레이션 프레임워크와 같은 주요 도구에서 사용됩니다.

#!/usr/bin/env bash
set -euo pipefail

STATE_DIR="/tmp/myapp_state"
mkdir -p "$STATE_DIR"

run_step() {
  local step_name="$1"
  local step_cmd="$2"
  local marker="$STATE_DIR/${step_name}.done"

  if [ -f "$marker" ]; then
    echo "[SKIP] $step_name already completed."
    return 0
  fi

  echo "[RUN]  $step_name ..."
  eval "$step_cmd"
  touch "$marker"
  echo "[DONE] $step_name"
}

run_step "install_deps"   "echo 'Installing dependencies...'"
run_step "configure_db"   "echo 'Configuring database...'"
run_step "start_service"  "echo 'Starting service...'"

재시도 로직 소개

HTTP 요청, DNS 조회, 클라우드 API 호출과 같은 외부 호출은 본질적으로 신뢰할 수 없습니다. 한 번의 실패로 전체 스크립트가 중단되어서는 안 됩니다. 재시도 로직은 실패한 명령을 자동으로 다시 시도합니다.

  • 단순한 재시도: 명령이 성공할 때까지 N번 반복합니다.
  • 무한 반복을 피하려면 항상 최대 재시도 횟수를 설정합니다.
  • 실패 원인을 진단할 수 있도록 시도할 때마다 기록합니다.

가장 단순한 재시도 함수는 어떤 명령이든 감싸고 일정한 지연 시간으로 정해진 횟수만큼 다시 시도합니다. 많은 사용 사례에서는 충분하지만 부하가 발생하면 중요한 결함이 드러납니다. 다음에서 이를 다룹니다.

#!/usr/bin/env bash
set -euo pipefail

# Simple fixed-delay retry (3 attempts, 2s apart)
retry() {
  local max_attempts=3
  local delay=2
  local attempt=1

  until "$@"; do
    if (( attempt >= max_attempts )); then
      echo "ERROR: Command failed after $max_attempts attempts: $*" >&2
      return 1
    fi
    echo "Attempt $attempt failed. Retrying in ${delay}s..." >&2
    sleep "$delay"
    (( attempt++ ))
  done
}

# Example: retry a curl call
retry curl --silent --fail --max-time 5 https://httpbin.org/get -o /dev/null
echo "Request succeeded."

천둥 치는 무리 문제

많은 클라이언트가 실패 후 동시에 재시도하면 천둥 치는 무리가 발생합니다. 모든 클라이언트가 동일한 고정 간격으로 재시도하여 정확히 같은 순간에 서버를 집중적으로 요청하고, 서버의 복구를 불가능하게 만듭니다.

  • 100개의 스크립트가 5초마다 재시도하면 5초마다 100개의 요청이 동시에 발생합니다.
  • 서버는 이미 과부하 상태인데, 동기화된 부하가 상황을 더 악화시킵니다.
  • 해결책은 지수 백오프입니다. 실패할 때마다 대기 시간을 두 배로 늘립니다.
  • 클라이언트 간 재시도 시점을 분산하도록 지터(무작위 변동)를 추가합니다.

지터를 적용한 지수 백오프는 AWS SDK, Google Cloud 클라이언트, 모든 주요 분산 시스템에서 사용하는 업계 표준입니다.

지수 백오프 구현

지수 백오프는 실패할 때마다 대기 시간을 지수적으로 늘립니다(1초, 2초, 4초, 8초, 16초...). 이렇게 하면 원격 시스템이 복구할 시간을 확보하면서 전체 부하를 줄일 수 있습니다.

수식: delay = base * (2 ^ attempt)

  • 대기 시간이 제한 없이 늘어나지 않도록 상한(최대 지연 시간)을 설정합니다.
  • 조정할 매개변수: base_delay, max_delay, max_attempts.

이 함수는 재사용할 수 있으므로 어떤 명령이든 인수로 전달할 수 있습니다.

#!/usr/bin/env bash
set -euo pipefail

retry_with_backoff() {
  local max_attempts="${RETRY_MAX_ATTEMPTS:-5}"
  local base_delay="${RETRY_BASE_DELAY:-1}"
  local max_delay="${RETRY_MAX_DELAY:-30}"
  local attempt=0
  local delay="$base_delay"

  until "$@"; do
    (( attempt++ ))
    if (( attempt >= max_attempts )); then
      echo "ERROR: '$*' failed after $max_attempts attempts." >&2
      return 1
    fi
    echo "Attempt $attempt failed. Backing off for ${delay}s..." >&2
    sleep "$delay"
    # Double the delay, but cap it
    delay=$(( delay * 2 ))
    (( delay > max_delay )) && delay=$max_delay
  done

  echo "Command succeeded on attempt $(( attempt + 1 ))."
}

retry_with_backoff echo "Simulated success"

백오프에 지터 추가

지터는 백오프 대기 시간에 무작위성을 추가합니다. 지수 백오프를 사용하더라도 모든 클라이언트가 동시에 시작하면 여전히 동기화된 상태로 재시도합니다. 지터는 이러한 동기화를 깨뜨립니다.

일반적인 지터 전략은 다음과 같습니다.

  • 전체 지터: sleep random(0, cap) — 분산 효과가 가장 크고 최대 부하가 가장 낮습니다.
  • 균등 지터: sleep cap/2 + random(0, cap/2) — 최소 대기 시간을 보장하며 즉시 반복적으로 요청하는 상황을 피합니다.

배시에서는 $RANDOM(0–32767)을 사용하여 난수를 생성합니다. 나머지 연산으로 지연 시간 범위에 맞게 조정합니다.

#!/usr/bin/env bash
set -euo pipefail

retry_with_jitter() {
  local max_attempts="${1:-5}"; shift
  local base_delay=1
  local max_delay=32
  local attempt=0
  local cap=$base_delay

  until "$@"; do
    (( attempt++ ))
    if (( attempt >= max_attempts )); then
      echo "ERROR: Giving up after $max_attempts attempts." >&2
      return 1
    fi

    # Full jitter: random value in [0, cap]
    local jitter=$(( RANDOM % (cap + 1) ))
    echo "Attempt $attempt failed. Sleeping ${jitter}s (cap=${cap}s)..." >&2
    sleep "$jitter"

    # Grow cap exponentially, bounded by max_delay
    cap=$(( cap * 2 ))
    (( cap > max_delay )) && cap=$max_delay
  done
}

retry_with_jitter 4 curl --silent --fail --max-time 3 https://httpbin.org/get -o /dev/null
echo "Done."

멱등성과 재시도를 실제 작업 흐름에 결합하기

운영 환경의 스크립트에서는 멱등성과 재시도 로직이 함께 작동합니다. 일반적인 배포 작업 흐름은 다음과 같을 수 있습니다.

  1. 잠금을 획득합니다(동시 실행 방지).
  2. 상태 파일을 확인합니다(완료된 단계를 건너뜀).
  3. 외부 호출에는 백오프 재시도를 사용합니다(다운로드, API, DNS).
  4. 성공을 확인한 후에만 단계를 완료된 것으로 표시합니다.
  5. trap을 통해 잠금을 해제합니다.

이 조합을 사용하면 충돌, 시간 초과 또는 수동 중단이 발생한 뒤에도 시스템을 망가진 상태로 남기지 않고 어느 시점에서든 스크립트를 안전하게 다시 실행할 수 있습니다.

#!/usr/bin/env bash
set -euo pipefail

STATE_DIR="/tmp/deploy_state" && mkdir -p "$STATE_DIR"
LOCK="/tmp/deploy.lock"

mkdir "$LOCK" 2>/dev/null || { echo "Already running."; exit 1; }
trap 'rmdir "$LOCK"' EXIT

step_done() { [ -f "$STATE_DIR/$1.done" ]; }
mark_done() { touch "$STATE_DIR/$1.done"; }

retry_backoff() {
  local attempt=0 delay=1
  until "${@:2}"; do
    (( ++attempt >= $1 )) && { echo "Failed after $1 attempts."; return 1; }
    echo "Retry $attempt in ${delay}s..."; sleep $delay; delay=$(( delay * 2 ))
  done
}

if ! step_done "download_artifact"; then
  retry_backoff 4 curl -fsSL https://httpbin.org/get -o /tmp/artifact.json
  mark_done "download_artifact"
  echo "[DONE] download_artifact"
else
  echo "[SKIP] download_artifact"
fi

echo "Deployment finished successfully."

재시도할 수 없는 오류 처리

모든 오류를 재시도해야 하는 것은 아닙니다. 404 찾을 수 없음이나 403 금지를 재시도하는 것은 낭비입니다. 사람의 개입 없이는 성공할 수 없기 때문입니다. 재시도 로직은 다음을 구분해야 합니다.

  • 일시적 오류 — 네트워크 시간 초과, 503 서비스를 사용할 수 없음, DNS 실패 → 재시도
  • 영구적 오류 — 401 인증되지 않음, 404 찾을 수 없음, 잘못된 입력 → 즉시 실패

curl에서는 HTTP 상태 코드를 확인하고 4xx 응답은 재시도하지 않습니다. --write-out '%{http_code}'를 사용하여 본문과 별도로 상태를 가져옵니다.

#!/usr/bin/env bash
set -euo pipefail

fetch_with_retry() {
  local url="$1"
  local max_attempts=4
  local delay=1
  local attempt=0
  local http_code

  until http_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url"); do
    : # curl itself failed (network error)
    (( ++attempt >= max_attempts )) && { echo "Network error, giving up."; return 1; }
    sleep $delay; delay=$(( delay * 2 ))
  done

  # Permanent client errors — do not retry
  if [[ "$http_code" =~ ^4 ]]; then
    echo "ERROR: HTTP $http_code for $url — not retrying." >&2
    return 1
  fi

  # Transient server errors — retry
  if [[ "$http_code" =~ ^5 ]]; then
    (( ++attempt >= max_attempts )) && { echo "Server error, giving up."; return 1; }
    echo "HTTP $http_code — backing off ${delay}s..."
    sleep $delay; delay=$(( delay * 2 ))
    fetch_with_retry "$url"
    return
  fi

  echo "HTTP $http_code — success."
}

fetch_with_retry "https://httpbin.org/status/200"

지식 확인: 백오프 전략 선택

배포 스크립트가 S3 버킷에서 릴리스 아티팩트를 다운로드합니다. 최근 장애에서 짧은 S3 중단으로 인해 파이프라인 인스턴스 80개가 동시에 실패했습니다. 10초 후 S3가 복구되자 80개 인스턴스가 모두 같은 순간에 재시도했고, 이로 인해 또 다른 과부하가 발생하여 중단 시간이 3분 연장되었습니다.

향후 장애에서 이러한 동시 폭주 연쇄를 가장 효과적으로 방지할 재시도 전략은 무엇일까요?

복습: 멱등성과 백오프 재시도

이 레슨에서는 안전하게 다시 실행할 수 있고 일시적 실패에 견고한 Bash 스크립트를 설계하는 방법을 배웠습니다.

멱등성 패턴:

  • 존재 여부 확인([ -f ], [ -d ], id, getent)으로 모든 작업을 보호합니다.
  • 사용할 수 있다면 mkdir -p 및 기타 내장 멱등성 옵션을 사용합니다.
  • 동시 실행을 방지하기 위해 잠금 파일(원자적 mkdir)을 사용합니다.
  • 실패 후 재개할 수 있도록 상태 파일(단계별 표시 파일)을 사용합니다.

백오프 재시도 패턴:

  • 최대 시도 횟수를 항상 설정합니다. 무한히 재시도해서는 안 됩니다.
  • 지수 백오프를 사용합니다. 실패할 때마다 지연 시간을 두 배로 늘립니다.
  • 동시 폭주 동기화를 방지하기 위해 지터(무작위성)를 추가합니다.
  • 일시적 오류(재시도)와 영구적 오류(빠른 실패)를 구분합니다.

이러한 패턴을 결합하면 안전하고, 상태를 관찰할 수 있으며, 스스로 복구하는 운영 환경 수준의 스크립트를 만들 수 있습니다.

무료로 시작

AI 튜터와 함께 Bash을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
22
레슨
88

자주 묻는 질문

“멱등적 스크립트와 지수 백오프 재시도 로직” 강의는 무료인가요?

네 — “멱등적 스크립트와 지수 백오프 재시도 로직” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Linux Command Line & Bash Scripting Mastery 강의 전체를 잠금 해제할 수 있습니다. Linux Command Line & Bash Scripting Mastery 강의에는 총 4개의 강의가 포함되어 있습니다.

“멱등적 스크립트와 지수 백오프 재시도 로직”에서 뭘 배우나요?

다시 실행해도 안전한 작업을 설계하고 불안정한 외부 호출에 지수 백오프를 추가합니다. 브라우저에서 직접 실행하는 실습 코드로 Linux Command Line & Bash Scripting Mastery을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Linux Command Line & Bash Scripting Mastery을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Linux Command Line & Bash Scripting Mastery은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“멱등적 스크립트와 지수 백오프 재시도 로직” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Linux Command Line & Bash Scripting Mastery 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Linux Command Line & Bash Scripting Mastery 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. set -euo pipefail을 활용한 엄격 모드
  2. 정리 및 신호 처리를 위한 트랩 핸들러
  3. 안전한 임시 파일 및 잠금 디렉터리
  4. 멱등적 스크립트와 지수 백오프 재시도 로직
← Linux Command Line & Bash Scripting Mastery(으)로 돌아가기