[도커 30강] 16강. 헬스체크와 depends_on으로 기동 순서 안정화

[도커 30강] 16강. 헬스체크와 depends_on으로 기동 순서 안정화

지금까지 compose로 여러 서비스를 한 번에 올리는 흐름을 만들었다면, 이제부터 반드시 마주치게 되는 현실 문제가 있습니다. 바로 기동 순서(race condition) 입니다. 예를 들어 app 컨테이너는 잘 올라왔는데 DB가 아직 초기화 중이라 연결에 실패하고, 그 한 번의 실패로 앱이 종료되는 상황이 대표적입니다. 이런 문제는 "가끔" 생겨서 더 어렵습니다. 로컬에서는 운 좋게 통과하고, CI나 운영에서는 실패해 팀 전체 시간을 잡아먹죠.

이번 강의의 목표는 단순히 depends_on 문법을 외우는 게 아닙니다. 서비스가 '실행 중(running)'인지가 아니라 '준비 완료(ready)'인지 기준으로 의존성을 설계하는 방법을 익히는 것입니다. 이를 위해 healthcheck를 정확히 정의하고, depends_on: condition: service_healthy를 사용해 앱 시작 시점을 안정화하겠습니다. 추가로 재시도 전략과 장애 진단 루틴까지 함께 정리해 실무에서 바로 쓸 수 있는 기준을 만들겠습니다.


핵심 개념

  • depends_on은 기본적으로 "컨테이너 시작 순서"만 보장합니다. 즉, DB 프로세스가 떠 있다는 사실만 확인할 뿐 "쿼리를 받을 준비"까지 보장하지 않습니다.
  • healthcheck는 컨테이너 내부에서 주기적으로 상태를 점검해 starting, healthy, unhealthy를 판정하는 메커니즘입니다.
  • 실무 안정화의 핵심은 "app이 db/redis가 healthy일 때만 시작"하도록 조건을 거는 것입니다.
  • start_period, interval, timeout, retries 값을 서비스 특성에 맞게 조정해야 flapping(healthy/unhealthy 반복)과 오탐을 줄일 수 있습니다.
  • healthcheck를 넣어도 앱 자체의 연결 재시도(backoff) 로직이 없으면 일시적 네트워크 흔들림에 취약합니다. 인프라 제어와 애플리케이션 제어를 함께 가져가야 안정적입니다.

기본 사용

예제 1) DB/Redis 헬스체크를 포함한 compose 기본 골격

아래 예제는 Postgres와 Redis에 헬스체크를 추가하고, 앱이 두 서비스가 healthy가 될 때까지 대기하도록 정의한 구성입니다.

mkdir -p ~/docker100/lesson16/app
cd ~/docker100/lesson16

cat > compose.yaml <<'YAML'
services:
  db:
    image: postgres:16-alpine
    container_name: lesson16_db
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppass
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb -h 127.0.0.1 || exit 1"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s

  redis:
    image: redis:7-alpine
    container_name: lesson16_redis
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 5s

  app:
    image: python:3.12-slim
    container_name: lesson16_app
    working_dir: /app
    volumes:
      - ./app:/app
    command: ["python", "app.py"]
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
YAML

설명:

  • DB는 프로세스 시작만으로 준비 완료라고 볼 수 없어서 pg_isready로 실제 연결 가능 상태를 점검합니다.
  • Redis는 redis-cli pingPONG을 반환해야 준비 완료로 간주합니다.
  • depends_on.condition: service_healthy를 사용하면 앱이 너무 빨리 뜨는 문제를 크게 줄일 수 있습니다.

예제 2) 앱에서 재시도 로직까지 포함해 이중 안전장치 만들기

인프라 레벨에서 healthy 조건을 걸었더라도, 운영 환경의 짧은 네트워크 흔들림이나 재시작 타이밍 문제를 대비해 앱 쪽 재시도는 꼭 넣는 것을 권장합니다.

cd ~/docker100/lesson16

cat > app/app.py <<'PY'
import os
import socket
import time

DB_HOST = os.getenv("DB_HOST", "db")
DB_PORT = int(os.getenv("DB_PORT", "5432"))
REDIS_HOST = os.getenv("REDIS_HOST", "redis")
REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))


def wait_tcp(host: str, port: int, name: str, attempts: int = 20, delay: float = 1.5):
    for i in range(1, attempts + 1):
        try:
            with socket.create_connection((host, port), timeout=1.5):
                print(f"[READY] {name} 연결 성공 ({host}:{port})")
                return
        except Exception as e:
            print(f"[WAIT] {name} 대기 중 {i}/{attempts} - {e}")
            time.sleep(delay)
    raise SystemExit(f"[FATAL] {name} 연결 실패: 재시도 초과")


wait_tcp(DB_HOST, DB_PORT, "Postgres")
wait_tcp(REDIS_HOST, REDIS_PORT, "Redis")
print("[OK] 의존 서비스 준비 완료. 앱 시작")
PY

docker compose up -d
docker compose ps
docker compose logs --tail=120 app

설명:

  • depends_on으로 1차 안정화, 앱 재시도로 2차 완충을 만들면 환경 차이로 인한 간헐 실패를 크게 줄일 수 있습니다.
  • 재시도 횟수/지연은 서비스 시작 시간에 맞춰 조정하세요. DB 마이그레이션이 길다면 조금 더 여유가 필요합니다.
  • 실패 로그를 구조적으로 남기면 장애 분석 시 "무엇이 늦었는지"가 즉시 드러납니다.

예제 3) 헬스 상태 관측과 실패 재현으로 동작 검증하기

안정화 설정은 "있는 것"보다 "검증된 것"이 중요합니다. 아래처럼 의도적으로 장애를 재현해 실제로 보호 장치가 작동하는지 확인해야 합니다.

cd ~/docker100/lesson16

# 상태 확인: health 컬럼 체크
for c in lesson16_db lesson16_redis lesson16_app; do
  echo "===== $c ====="
  docker inspect --format='{{.State.Status}} / {{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$c"
done

# DB 강제 재시작 후 앱 로그 확인
docker restart lesson16_db
sleep 2
docker compose logs --tail=200 app db

# compose 이벤트를 보며 상태 변화 추적
docker compose events --json | head -n 30

설명:

  • docker inspect로 health 상태를 직접 보는 습관이 중요합니다. running만 보면 준비 완료를 착각하기 쉽습니다.
  • 강제 재시작 시 앱이 즉시 죽지 않고 복구되는지 확인하면 운영 안정성 수준을 미리 점검할 수 있습니다.
  • 이벤트 로그는 타이밍 이슈(누가 먼저 죽고 먼저 올라왔는지) 분석에 큰 도움이 됩니다.

자주 하는 실수

실수 1) depends_on만 넣고 "준비 완료"라고 착각

  • 원인: depends_on을 기동 안정화의 만능으로 오해.
  • 문제: DB 프로세스는 떴지만 초기화/리커버리 중이면 앱 연결 실패.
  • 해결: DB/캐시/브로커에 각각 적합한 healthcheck를 정의하고 service_healthy 조건 사용.

실수 2) healthcheck 명령을 너무 무겁게 작성

  • 원인: 실제 비즈니스 쿼리까지 매 2~3초마다 실행.
  • 문제: 불필요한 부하 증가, 오히려 헬스체크가 장애 원인으로 변함.
  • 해결: "준비 여부"만 검증하는 최소 명령 사용(pg_isready, redis-cli ping, HTTP /healthz 등).

실수 3) start_period 없이 retries만 늘리기

  • 원인: 초기 부팅 시간 개념 없이 숫자만 크게 조정.
  • 문제: 시작 직후 실패를 누적으로 기록해 unhealthy로 조기 판정.
  • 해결: 서비스 초기화 시간이 긴 경우 start_period를 충분히 확보한 뒤 interval/retries 튜닝.

실수 4) 앱 쪽 재시도 로직 생략

  • 원인: 인프라 설정만 믿고 애플리케이션 복원력을 고려하지 않음.
  • 문제: 일시적 네트워크 흔들림이나 DB failover 시 앱 프로세스가 바로 종료.
  • 해결: 연결 재시도 + 지수 백오프 + 명확한 오류 로그를 앱 시작 루틴에 포함.

실무 패턴

실무에서 가장 효과적인 패턴은 3단계 보호 구조입니다.

  1. 컨테이너 레벨 준비 확인: healthcheck로 각 서비스의 readiness 정의
  2. 의존성 레벨 순서 제어: depends_on: condition: service_healthy 적용
  3. 애플리케이션 레벨 복원력: 재시도/backoff/타임아웃 정책 구현

이 세 가지가 같이 있어야 운영에서 흔들림을 견딜 수 있습니다. 하나라도 빠지면 특정 조건에서 바로 깨집니다. 예를 들어 DB가 느리게 뜨는 날에는 1,2가 중요하고, 네트워크가 순간적으로 튀는 날에는 3이 중요합니다.

또 하나 중요한 기준은 헬스체크의 목적을 분리하는 것입니다. readiness(요청 받을 준비)와 liveness(프로세스 살아있음)를 혼동하면 운영 정책이 꼬입니다. Compose 환경에서는 주로 readiness 관점으로 설계하고, 오케스트레이션 환경(Kubernetes 등)으로 넘어갈 때 liveness/readiness를 더 정교하게 분리하면 학습 곡선이 매끈해집니다.

팀 협업에서는 아래 규칙을 문서로 고정해두는 것이 좋습니다.

  • 모든 상태 저장 서비스(DB, 캐시, 큐)는 healthcheck 필수
  • 앱 서비스는 의존 서비스에 대해 service_healthy 조건 필수
  • 신규 서비스 추가 시 "헬스 명령 + 실패 재현 시나리오"를 PR에 포함
  • 배포 전 smoke test에서 docker inspect health 상태 확인 자동화

이 규칙만 지켜도 "내 컴퓨터에서는 되는데 CI에서만 실패" 같은 소모전이 크게 줄어듭니다.

오늘의 결론

한 줄 요약: 기동 순서 안정화는 depends_on 한 줄이 아니라, healthcheck 기반 준비 완료 판단 + 앱 재시도 로직까지 묶은 다층 방어로 완성된다.

연습문제

  1. Postgres healthcheck의 start_period를 0s로 바꾸고 docker compose up을 여러 번 반복해보세요. 어떤 상황에서 unhealthy가 더 자주 발생하는지 관찰하고 이유를 설명해보세요.
  2. app 컨테이너에서 재시도 횟수를 3회로 줄인 뒤 DB를 의도적으로 늦게 기동시켜 보세요. 실패 로그를 보고 "적절한 attempts/delay" 값을 제안해보세요.
  3. Redis healthcheck를 일부러 실패하도록 명령을 틀리게 바꾼 후, app이 시작되지 않는 흐름을 확인하세요. 이런 실패를 CI에서 사전에 잡기 위한 검증 항목을 작성해보세요.

이전 강의 정답

15강 연습문제 해설:

  • DB_PASSWORD를 비우면 앱의 필수 환경변수 검증에서 즉시 실패해야 정상입니다. 이 빠른 실패 덕분에 DB 연결 타임아웃을 오래 기다리지 않고 원인을 정확히 찾을 수 있습니다.
  • JWT_SECRET 같은 키를 추가할 때는 .env.example에 키를 문서화하고, 앱의 REQUIRED 목록에 포함해 누락 시 명확한 에러를 출력하는 것이 핵심입니다.
  • docker compose config 결과나 부팅 로그에서 민감값이 노출된다면 즉시 마스킹/필터링 정책을 적용해야 합니다. 특히 운영 로그 수집 시스템에 들어간 데이터는 회수가 어렵기 때문에 "처음부터 노출 금지"가 정답입니다.

실습 환경/재현 정보

  • OS: macOS 15+ (Apple Silicon) 또는 Ubuntu 22.04+
  • Docker: Engine/Desktop 25.x 이상
  • Compose: v2.x (docker compose version)
  • 사용 이미지: postgres:16-alpine, redis:7-alpine, python:3.12-slim
  • 테스트 순서:
    1. compose 파일 작성 및 healthcheck 반영
    2. docker compose up -d 실행
    3. docker inspect로 health 상태 확인
    4. DB/Redis 재시작으로 장애 시나리오 재현
    5. app 로그에서 재시도/복구 여부 검증
  • 재현 체크 포인트:
    • app이 의존 서비스 healthy 이전에 시작되지 않는가?
    • healthcheck 파라미터가 과도하게 공격적이지 않은가?
    • 장애 재현 후 자동 복구 또는 명확한 실패 로그를 제공하는가?
    • 팀원이 동일 절차로 같은 결과를 재현할 수 있는가?