TL;DR

  • Spring Boot Actuator, Health Endpoint, Liveness Probe, Readiness Probe, Startup Probe, Kubernetes, HealthIndicator, Health Groups, ApplicationAvailability, LivenessState, ReadinessState, StatusAggregator, Graceful Shutdown, Zero-Downtime, Rolling Update, preStop, terminationGracePeriodSeconds, Cascading Failure, Thundering Herd, Spring Boot 2.3, KUBERNETES_SERVICE_HOST, Prometheus, Micrometer, management.server.port, show-details, probes.enabled, AbstractHealthIndicator, AvailabilityChangeEvent, CompositeHealthContributor, Spring Security, 자가 치유, 캐스케이딩 실패
  • Spring Boot Actuator Health를 알아두면 설계 판단과 구현 선택을 더 분명하게 할 수 있다.
  • 원문 전체는 아래 상세 내용에 그대로 포함했다.

1. 개념

Spring Boot Actuator, Health Endpoint, Liveness Probe, Readiness Probe, Startup Probe, Kubernetes, HealthIndicator, Health Groups, ApplicationAvailability, LivenessState, ReadinessState, StatusAggregator, Graceful Shutdown, Zero-Downtime, Rolling Update, preStop, terminationGracePeriodSeconds, Cascading Failure, Thundering Herd, Spring Boot 2.3, KUBERNETES_SERVICE_HOST, Prometheus, Micrometer, management.server.port, show-details, probes.enabled, AbstractHealthIndicator, AvailabilityChangeEvent, CompositeHealthContributor, Spring Security, 자가 치유, 캐스케이딩 실패

2. 배경

Spring Boot Actuator Health가 등장한 배경과 문제 상황을 이해하는 데 도움이 된다.

3. 이유

Spring Boot Actuator Health를 알아두면 설계 판단과 구현 선택을 더 분명하게 할 수 있다.

4. 특징

Spring Boot Actuator Health의 특징, 장단점, 적용 포인트를 원문에서 자세히 확인할 수 있다.

5. 상세 내용

Spring Boot Actuator Health

1. Spring Boot Actuator란?

정의

┌─────────────────────────────────────────────────────────────────┐
│                  Spring Boot Actuator                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  Actuator = 프로덕션 준비(Production-Ready) 기능을               │
│             애플리케이션에 한방에 번들하는 Spring Boot 서브모듈   │
│                                                                   │
│  이름 유래:                                                      │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  제조업 용어 "Actuator"                                     │ │
│  │  = 작은 입력으로 큰 동작을 만드는 기계 장치                 │ │
│  │                                                              │ │
│  │  Spring Boot Actuator도 마찬가지:                            │ │
│  │  의존성 하나(작은 입력) → 거대한 운영 기능(큰 동작)         │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

핵심 기능 카테고리

┌─────────────────────────────────────────────────────────────────┐
│                  Actuator 엔드포인트 카테고리                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌──────────────┬─────────────────────────────────────────┐     │
│  │  카테고리    │  엔드포인트 예시                         │     │
│  ├──────────────┼─────────────────────────────────────────┤     │
│  │  상태/건강   │  /actuator/health                       │     │
│  │              │  /actuator/health/liveness              │     │
│  │              │  /actuator/health/readiness             │     │
│  ├──────────────┼─────────────────────────────────────────┤     │
│  │  메트릭      │  /actuator/metrics                      │     │
│  │              │  /actuator/prometheus                    │     │
│  ├──────────────┼─────────────────────────────────────────┤     │
│  │  인트로스펙션│  /actuator/beans                        │     │
│  │              │  /actuator/mappings                      │     │
│  ├──────────────┼─────────────────────────────────────────┤     │
│  │  설정        │  /actuator/env                          │     │
│  │              │  /actuator/configprops                   │     │
│  ├──────────────┼─────────────────────────────────────────┤     │
│  │  런타임 제어 │  /actuator/loggers (로그 레벨 동적 변경)│     │
│  │              │  /actuator/shutdown (앱 종료)            │     │
│  ├──────────────┼─────────────────────────────────────────┤     │
│  │  진단        │  /actuator/threaddump                   │     │
│  │              │  /actuator/heapdump                      │     │
│  ├──────────────┼─────────────────────────────────────────┤     │
│  │  DB 마이그레이션│ /actuator/flyway                     │     │
│  │              │  /actuator/liquibase                     │     │
│  └──────────────┴─────────────────────────────────────────┘     │
│                                                                   │
│  기본 노출:                                                      │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  HTTP로 기본 노출되는 엔드포인트:                           │ │
│  │  → /actuator/health  (건강 상태)                            │ │
│  │  → /actuator/info    (앱 정보)                              │ │
│  │  → 나머지는 명시적으로 활성화해야 함                        │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

2. 설치: 정말 의존성 하나면 되나?

Maven

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Gradle (Kotlin DSL)

implementation("org.springframework.boot:spring-boot-starter-actuator")

Bazel (rules_jvm_external)

# MODULE.bazel
maven.install(
    artifacts = [
        "org.springframework.boot:spring-boot-starter-actuator:3.2.0",
    ],
)

# BUILD
deps = ["@maven//:org_springframework_boot_spring_boot_starter_actuator"]
┌─────────────────────────────────────────────────────────────────┐
│                  Bazel Maven 좌표 변환 규칙                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  Maven 좌표:                                                     │
│  org.springframework.boot:spring-boot-starter-actuator           │
│                                                                   │
│  Bazel 라벨:                                                     │
│  @maven//:org_springframework_boot_spring_boot_starter_actuator  │
│                                                                   │
│  변환 규칙:                                                      │
│  ├── dots(.)   → underscores(_)                                  │
│  ├── hyphens(-) → underscores(_)                                 │
│  └── colons(:) → double underscores(__)  (groupId와 artifactId) │
│                                                                   │
│  특별한 게 없음.                                                 │
│  같은 Maven 아티팩트를 Bazel 문법으로 선언할 뿐이다.            │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

의존성만 추가하면 바로 얻는 것

┌─────────────────────────────────────────────────────────────────┐
│                  설정 0줄로 얻는 것들                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  의존성 추가 직후, 아무 설정 없이:                               │
│                                                                   │
│  GET /actuator                                                   │
│  → 디스커버리 엔드포인트 (사용 가능한 엔드포인트 목록)          │
│                                                                   │
│  GET /actuator/health                                            │
│  → { "status": "UP" }                                            │
│                                                                   │
│  GET /actuator/info                                              │
│  → {}                                                            │
│                                                                   │
│  결론: 설정 0줄로 핵심 건강 체크가 동작한다.                     │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

application.yml 설정 (개발 vs 프로덕션)

개발 환경

management:
  endpoints:
    web:
      exposure:
        include: "*"        # 모든 엔드포인트 노출 (개발용)
  endpoint:
    health:
      show-details: always  # 상세 정보 항상 표시

프로덕션 환경

management:
  endpoints:
    web:
      exposure:
        include: health, prometheus  # 필요한 것만 노출
  endpoint:
    health:
      show-details: when-authorized  # 인증된 사용자만 상세 보기
  server:
    port: 8081                       # 별도 관리 포트

3. 등장 배경: 왜 Liveness/Readiness가 필요한가?

Spring Boot 2.3 이전의 문제 (2020년 5월 이전)

┌─────────────────────────────────────────────────────────────────┐
│          Spring Boot 2.3 이전: 단일 /health의 비극               │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  문제 상황:                                                      │
│  ├── 단일 /actuator/health에 liveness와 readiness 모두 연결     │
│  ├── 외부 의존성(DB, Redis 등)도 health에 포함                  │
│  └── K8s가 이 단일 엔드포인트로 liveness 체크                   │
│                                                                   │
│  시나리오: DB가 일시적으로 느려진 경우                           │
│                                                                   │
│  DB 느려짐                                                       │
│       │                                                          │
│       ▼                                                          │
│  /health → DOWN (DB 연결 타임아웃)                               │
│       │                                                          │
│       ▼                                                          │
│  K8s: "Pod이 죽었다!" (liveness 실패)                            │
│       │                                                          │
│       ├──→ Pod 1 재시작                                          │
│       ├──→ Pod 2 재시작                                          │
│       ├──→ Pod 3 재시작                                          │
│       └──→ 모든 Pod 동시 재시작!                                 │
│                │                                                 │
│                ▼                                                 │
│  트래픽 처리 불가! (전체 서비스 중단)                            │
│                                                                   │
│  그런데... Pod은 멀쩡했다!                                       │
│  DB가 느렸을 뿐이다!                                             │
│  재시작해도 DB는 안 고쳐진다!                                    │
│                                                                   │
│  이것이 바로 "캐스케이딩 실패 / Thundering Herd"                 │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Kubernetes의 원래 의도

┌─────────────────────────────────────────────────────────────────┐
│          K8s는 처음부터 분리를 원했다                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  Kubernetes 설계:                                                │
│  ├── livenessProbe: "이 컨테이너가 살아있는가?"                  │
│  ├── readinessProbe: "이 컨테이너가 요청 받을 수 있는가?"       │
│  └── 처음부터 두 개를 분리하고 있었음                            │
│                                                                   │
│  Java/Spring 앱의 문제:                                          │
│  └── 이 두 상태를 애플리케이션 레벨에서 표현할 방법이 없었음     │
│                                                                   │
│  Spring Boot 2.3 (2020년 5월):                                   │
│  └── 그 간극을 메운 것                                           │
│  └── ApplicationAvailability API 도입                            │
│  └── /health/liveness, /health/readiness 분리                    │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

4. Liveness vs Readiness: 근본적 차이

핵심 멘탈 모델

┌─────────────────────────────────────────────────────────────────┐
│          Liveness vs Readiness: 한눈에 비교                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌──────────┬─────────────────────┬──────────────────────────┐  │
│  │  개념    │  질문               │  K8s 반응 (실패 시)      │  │
│  ├──────────┼─────────────────────┼──────────────────────────┤  │
│  │ Liveness │ "이 앱이            │ Pod 재시작 (RESTART)     │  │
│  │          │  살아있는가?"       │ → 강력한 조치            │  │
│  ├──────────┼─────────────────────┼──────────────────────────┤  │
│  │Readiness │ "이 앱이 트래픽을   │ 로드밸런서에서 제거      │  │
│  │          │  받을 수 있는가?"   │ (REMOVE, 재시작 안 함)   │  │
│  │          │                     │ → 부드러운 조치          │  │
│  └──────────┴─────────────────────┴──────────────────────────┘  │
│                                                                   │
│  비유:                                                           │
│  ├── Liveness  = "환자가 살아있는가?" → 아니면 심폐소생술       │
│  └── Readiness = "환자가 퇴원 가능한가?" → 아니면 병실에 유지   │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Liveness: /actuator/health/liveness

┌─────────────────────────────────────────────────────────────────┐
│          Liveness = "이 앱을 재시작해야 하나?"                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  DOWN이어야 하는 경우 (재시작이 필요한 경우):                    │
│  ├── 복구 불가능한 데드락                                        │
│  ├── 내부 상태의 비가역적 손상                                   │
│  └── 재시작만이 유일한 해결책인 상황                             │
│                                                                   │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  절대 규칙:                                                  │ │
│  │  외부 시스템(DB, API, 메시지 브로커)을                       │ │
│  │  liveness에 절대 포함하지 마라!                              │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  왜? 가상 시나리오:                                              │
│                                                                   │
│  DB 다운                                                         │
│  │                                                               │
│  ├── DB를 liveness에 포함했다면:                                 │
│  │   ├── /liveness → DOWN                                        │
│  │   ├── K8s가 Pod 1 재시작                                      │
│  │   ├── Pod 1 올라옴, DB 여전히 다운                            │
│  │   ├── /liveness → 또 DOWN                                     │
│  │   ├── K8s가 Pod 2, 3도 재시작                                 │
│  │   ├── 모든 Pod 재시작 중 → 트래픽 처리 불가                   │
│  │   └── 캐스케이딩 실패!                                        │
│  │                                                               │
│  └── 올바른 접근:                                                │
│      ├── DB 다운 ≠ 앱이 고장났다는 뜻                            │
│      ├── 트래픽을 멈추면 됨 (readiness의 역할)                   │
│      └── 재시작할 필요 없음                                      │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Readiness: /actuator/health/readiness

┌─────────────────────────────────────────────────────────────────┐
│          Readiness = "이 앱이 지금 트래픽을 받을 수 있나?"       │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  REFUSING_TRAFFIC인 경우 (트래픽 못 받는 경우):                  │
│  ├── 시작 중 / 워밍업 중                                         │
│  ├── 필수 의존성(DB, Redis 등) 불가용                            │
│  ├── 일시적 과부하 상태                                          │
│  └── 그레이스풀 셧다운 진행 중                                   │
│                                                                   │
│  DOWN이면:                                                       │
│  ├── K8s가 LB에서 제거 → 트래픽 중단                             │
│  ├── Pod은 재시작되지 않음 (여전히 살아있으므로)                  │
│  └── 복구되면 LB에 다시 추가 → 트래픽 자동 재개                 │
│                                                                   │
│  핵심: readiness는 "일시적"이다.                                 │
│  Pod을 죽이지 않고, 트래픽만 잠시 멈추고, 회복을 기다린다.      │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

상태 열거형

// Liveness 상태
public enum LivenessState implements AvailabilityState {
    CORRECT,   // 앱 살아있음 → HTTP 200 (UP)
    BROKEN     // 치명적 내부 장애 → HTTP 503 (DOWN)
}

// Readiness 상태
public enum ReadinessState implements AvailabilityState {
    ACCEPTING_TRAFFIC,  // 준비됨 → HTTP 200 (UP)
    REFUSING_TRAFFIC    // 미준비 → HTTP 503 (DOWN)
}

5. 애플리케이션 생명주기 상태 전이

시작 시퀀스

┌─────────────────────────────────────────────────────────────────┐
│          시작 시퀀스: Liveness/Readiness 상태 전이                │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌──────────────────┬──────────┬──────────────────┬───────────┐ │
│  │  단계            │ Liveness │ Readiness        │  상황     │ │
│  ├──────────────────┼──────────┼──────────────────┼───────────┤ │
│  │  앱 시작 중      │ BROKEN   │ REFUSING_TRAFFIC │  컨텍스트 │ │
│  │                  │          │                  │  미초기화  │ │
│  ├──────────────────┼──────────┼──────────────────┼───────────┤ │
│  │  컨텍스트        │ CORRECT  │ REFUSING_TRAFFIC │  빈 준비  │ │
│  │  리프레시 완료   │          │                  │  시작태스크│ │
│  │                  │          │                  │  실행 중   │ │
│  ├──────────────────┼──────────┼──────────────────┼───────────┤ │
│  │  Application     │ CORRECT  │ ACCEPTING_TRAFFIC│  트래픽   │ │
│  │  ReadyEvent      │          │                  │  수신 준비│ │
│  │                  │          │                  │  완료!    │ │
│  └──────────────────┴──────────┴──────────────────┴───────────┘ │
│                                                                   │
│  시간순:                                                         │
│                                                                   │
│  JVM 시작 ─→ Context 초기화 ─→ Bean 생성 ─→ Ready!              │
│  │            │                  │              │                 │
│  │            │                  │              ▼                 │
│  │            │                  │        ReadyEvent 발행         │
│  │            │                  │        Readiness → ACCEPTING   │
│  │            │                  ▼                                │
│  │            │            ContextRefreshed                       │
│  │            │            Liveness → CORRECT                    │
│  │            ▼                                                  │
│  │       BROKEN / REFUSING                                       │
│  ▼                                                               │
│  앱 프로세스 시작                                                │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

셧다운 시퀀스

┌─────────────────────────────────────────────────────────────────┐
│          셧다운 시퀀스: 제로 다운타임의 열쇠                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌──────────────────┬──────────┬──────────────────┬───────────┐ │
│  │  단계            │ Liveness │ Readiness        │  상황     │ │
│  ├──────────────────┼──────────┼──────────────────┼───────────┤ │
│  │  SIGTERM 수신    │ CORRECT  │ REFUSING_TRAFFIC │  K8s가    │ │
│  │                  │          │                  │  즉시 LB  │ │
│  │                  │          │                  │  에서 제거│ │
│  ├──────────────────┼──────────┼──────────────────┼───────────┤ │
│  │  그레이스풀      │ CORRECT  │ REFUSING_TRAFFIC │  진행중인 │ │
│  │  드레인          │          │                  │  요청 완료│ │
│  │                  │          │                  │  대기     │ │
│  ├──────────────────┼──────────┼──────────────────┼───────────┤ │
│  │  컨텍스트 종료   │  무관    │  무관            │  Pod 종료│ │
│  └──────────────────┴──────────┴──────────────────┴───────────┘ │
│                                                                   │
│  핵심 포인트:                                                    │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  셧다운 시 readiness → REFUSING_TRAFFIC이                   │ │
│  │  제로 다운타임 배포의 열쇠다.                                │ │
│  │                                                              │ │
│  │  SIGTERM → readiness DOWN → LB 제거 → 새 요청 안 들어옴    │ │
│  │  → 기존 요청 완료 → 안전하게 종료                           │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

6. /actuator/health 내부 동작

응답 구조 (show-details: always)

{
  "status": "UP",
  "components": {
    "db": {
      "status": "UP",
      "details": {
        "database": "PostgreSQL",
        "validationQuery": "isValid()"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 107374182400,
        "free": 85899345920,
        "threshold": 10485760,
        "path": "/app/."
      }
    },
    "ping": {
      "status": "UP"
    },
    "redis": {
      "status": "UP",
      "details": {
        "version": "7.2.4"
      }
    }
  }
}

자동 구성된 HealthIndicator 목록

┌─────────────────────────────────────────────────────────────────┐
│          자동 등록되는 HealthIndicator 목록                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌──────────────────────────┬──────────────────────────────┐    │
│  │  HealthIndicator         │  활성화 조건                 │    │
│  ├──────────────────────────┼──────────────────────────────┤    │
│  │  DiskSpaceHealthIndicator│  항상 (디스크 여유 공간)     │    │
│  │  PingHealthIndicator     │  항상 (단순 ping 응답)       │    │
│  ├──────────────────────────┼──────────────────────────────┤    │
│  │  DataSourceHealth-       │  DataSource 빈 존재 시       │    │
│  │  Indicator               │  (JDBC DB 연결 확인)         │    │
│  │  RedisHealthIndicator    │  Spring Data Redis           │    │
│  │                          │  클래스패스에 존재 시        │    │
│  │  MongoHealthIndicator    │  Spring Data MongoDB 시      │    │
│  │  KafkaHealthIndicator    │  Spring Kafka 시             │    │
│  │  RabbitHealthIndicator   │  Spring AMQP 시              │    │
│  │  Elasticsearch-          │  Spring Data ES 시           │    │
│  │  HealthIndicator         │                              │    │
│  ├──────────────────────────┼──────────────────────────────┤    │
│  │  LivenessStateHealth-    │  K8s 환경 감지 시 또는       │    │
│  │  Indicator               │  probes.enabled: true        │    │
│  │  ReadinessStateHealth-   │  K8s 환경 감지 시 또는       │    │
│  │  Indicator               │  probes.enabled: true        │    │
│  └──────────────────────────┴──────────────────────────────┘    │
│                                                                   │
│  원리: 클래스패스에 라이브러리가 있으면 자동으로 등록됨          │
│  → Spring Boot의 자동 구성(Auto-Configuration) 마법              │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

내부 아키텍처

┌─────────────────────────────────────────────────────────────────┐
│          /actuator/health 내부 아키텍처                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  HTTP GET /actuator/health                                       │
│       │                                                          │
│       ▼                                                          │
│  HealthEndpoint                                                  │
│       │                                                          │
│       ▼                                                          │
│  CompositeHealthContributor (모든 contributor 순회)              │
│       │                                                          │
│       ├── DataSourceHealthIndicator ────→ DB 연결 확인           │
│       ├── RedisHealthIndicator ──────→ Redis 연결 확인           │
│       ├── DiskSpaceHealthIndicator ──→ 디스크 여유 확인          │
│       ├── LivenessStateHealthIndicator                           │
│       │   └── ApplicationAvailabilityBean 에서 상태 조회         │
│       └── [커스텀 HealthIndicator들]                             │
│                                                                   │
│       │  (각 indicator가 Health 객체 반환)                       │
│       ▼                                                          │
│  StatusAggregator                                                │
│       │                                                          │
│       ├── 모든 컴포넌트 UP → 전체 UP                             │
│       └── 하나라도 DOWN  → 전체 DOWN                             │
│                                                                   │
│       │                                                          │
│       ▼                                                          │
│  HTTP 200 (UP) 또는 HTTP 503 (DOWN)                              │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

상태 집계 규칙

┌─────────────────────────────────────────────────────────────────┐
│          StatusAggregator: 하나라도 DOWN이면 전체 DOWN           │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  예시 1: 모두 정상                                               │
│  ├── db: UP                                                      │
│  ├── redis: UP                                                   │
│  ├── diskSpace: UP                                               │
│  └── 전체: UP → HTTP 200                                         │
│                                                                   │
│  예시 2: Redis 장애                                              │
│  ├── db: UP                                                      │
│  ├── redis: DOWN  ← 이것 하나 때문에                             │
│  ├── diskSpace: UP                                               │
│  └── 전체: DOWN → HTTP 503                                       │
│                                                                   │
│  상태 우선순위:                                                  │
│  DOWN > OUT_OF_SERVICE > UP > UNKNOWN                            │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

7. Health Groups와 Kubernetes 프로브

Health Groups란?

┌─────────────────────────────────────────────────────────────────┐
│          Health Groups: HealthIndicator의 논리적 그룹            │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  개념:                                                           │
│  ├── HealthIndicator의 이름 있는 부분 집합(Subset)               │
│  ├── 각 그룹이 자체 URL로 접근 가능                              │
│  └── Liveness, Readiness는 사실 Health Group으로 구현됨          │
│                                                                   │
│  설정 예시:                                                      │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  management:                                                 │ │
│  │    endpoint:                                                 │ │
│  │      health:                                                 │ │
│  │        group:                                                │ │
│  │          readiness:                                          │ │
│  │            include: readinessState, db  # DB를 readiness에  │ │
│  │          liveness:                                           │ │
│  │            include: livenessState       # liveness는 최소한 │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  접근 URL:                                                       │
│  ├── /actuator/health/readiness → readinessState + db만 체크    │
│  └── /actuator/health/liveness  → livenessState만 체크          │
│                                                                   │
│  핵심:                                                           │
│  liveness 그룹에는 외부 시스템을 넣지 않는다!                    │
│  readiness 그룹에는 필요한 외부 시스템을 넣을 수 있다.          │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Kubernetes 자동 감지

┌─────────────────────────────────────────────────────────────────┐
│          K8s 자동 감지: 프로브 자동 활성화                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  Spring Boot의 자동 감지 메커니즘:                               │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  if (System.getenv("KUBERNETES_SERVICE_HOST") != null) {    │ │
│  │      // K8s 환경으로 판단                                    │ │
│  │      // liveness, readiness 프로브 자동 활성화               │ │
│  │  }                                                           │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  K8s 환경에서:                                                   │
│  → /actuator/health/liveness 자동 활성화                         │
│  → /actuator/health/readiness 자동 활성화                        │
│                                                                   │
│  K8s 밖에서 (로컬, Docker 단독):                                 │
│  → 수동 활성화 필요                                              │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  management:                                                 │ │
│  │    health:                                                   │ │
│  │      probes:                                                 │ │
│  │        enabled: true   # 수동으로 프로브 활성화              │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  Spring Boot 4부터:                                              │
│  → 기본적으로 모든 환경에서 프로브 활성화                        │
│  → K8s 감지 여부와 무관하게 동작                                 │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Startup Probe (Spring Boot 2.6+)

┌─────────────────────────────────────────────────────────────────┐
│          Startup Probe: 느린 시작을 위한 안전장치                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  문제:                                                           │
│  ├── 앱 시작이 오래 걸리는 경우 (대규모 Spring Context)          │
│  ├── liveness 체크가 시작 중에 실패                              │
│  └── K8s가 "죽었다"고 판단 → 무한 재시작                        │
│                                                                   │
│  Startup Probe의 역할:                                           │
│  ├── 성공할 때까지 liveness/readiness 체크 비활성화              │
│  ├── startup 성공 → liveness/readiness 체크 시작                 │
│  └── startup 실패 (timeout) → Pod 재시작                         │
│                                                                   │
│  최대 허용 시작 시간 계산:                                       │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  failureThreshold * periodSeconds = 최대 허용 시작 시간     │ │
│  │                                                              │ │
│  │  예: failureThreshold: 30, periodSeconds: 10                 │ │
│  │  → 30 * 10 = 300초 (5분) 이내에 시작해야 함                 │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  시간순:                                                         │
│  Pod 생성 → [startupProbe 반복 체크]                             │
│              │                                                   │
│              ├── 성공 → livenessProbe + readinessProbe 시작     │
│              └── 계속 실패 → threshold 초과 → Pod 재시작        │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

8. 커스텀 HealthIndicator 작성

기본 구현

@Component
public class ExternalApiHealthIndicator implements HealthIndicator {

    private final ExternalApiClient client;

    public ExternalApiHealthIndicator(ExternalApiClient client) {
        this.client = client;
    }

    @Override
    public Health health() {
        boolean reachable = client.ping();
        if (reachable) {
            return Health.up()
                    .withDetail("url", "https://api.example.com")
                    .withDetail("responseTime", "45ms")
                    .build();
        }
        return Health.down()
                .withDetail("url", "https://api.example.com")
                .withDetail("reason", "unreachable")
                .build();
    }
}
┌─────────────────────────────────────────────────────────────────┐
│          커스텀 HealthIndicator 등록 규칙                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  빈 이름 규칙:                                                   │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  클래스명: ExternalApiHealthIndicator                        │ │
│  │  접미사 "HealthIndicator" 자동 제거                          │ │
│  │  → /actuator/health 응답에 "externalApi"로 표시             │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  AbstractHealthIndicator 상속 (권장):                            │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  @Component                                                  │ │
│  │  public class ExternalApiHealthIndicator                     │ │
│  │      extends AbstractHealthIndicator {                       │ │
│  │                                                              │ │
│  │      @Override                                               │ │
│  │      protected void doHealthCheck(                           │ │
│  │              Health.Builder builder) throws Exception {      │ │
│  │          // 예외 발생 시 자동으로 DOWN + 에러 메시지         │ │
│  │          client.ping();                                      │ │
│  │          builder.up()                                        │ │
│  │                 .withDetail("url", "https://api.example.com")│ │
│  │                 ;                                            │ │
│  │      }                                                       │ │
│  │  }                                                           │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  AbstractHealthIndicator의 장점:                                 │
│  ├── 예외 발생 시 자동으로 DOWN 상태 + 에러 메시지 래핑         │
│  └── try-catch 보일러플레이트 제거                               │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

프로그래매틱 상태 변경

┌─────────────────────────────────────────────────────────────────┐
│          ApplicationAvailability로 상태 직접 변경                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  @Component                                                  │ │
│  │  public class CircuitBreakerListener {                       │ │
│  │                                                              │ │
│  │      private final ApplicationEventPublisher publisher;      │ │
│  │                                                              │ │
│  │      // 외부 서비스 장애 감지 시 readiness를 DOWN으로        │ │
│  │      public void onCircuitOpen() {                           │ │
│  │          AvailabilityChangeEvent.publish(                    │ │
│  │              publisher,                                      │ │
│  │              this,                                           │ │
│  │              ReadinessState.REFUSING_TRAFFIC                 │ │
│  │          );                                                  │ │
│  │      }                                                       │ │
│  │                                                              │ │
│  │      // 복구 시 readiness를 UP으로                           │ │
│  │      public void onCircuitClosed() {                         │ │
│  │          AvailabilityChangeEvent.publish(                    │ │
│  │              publisher,                                      │ │
│  │              this,                                           │ │
│  │              ReadinessState.ACCEPTING_TRAFFIC                │ │
│  │          );                                                  │ │
│  │      }                                                       │ │
│  │  }                                                           │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  활용 시나리오:                                                  │
│  ├── 서킷 브레이커 OPEN → readiness DOWN → 트래픽 차단         │
│  ├── 외부 API 할당량 초과 → readiness DOWN → 잠시 쉼           │
│  └── 복구 감지 → readiness UP → 트래픽 재개                     │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

9. 프로덕션 보안 고려사항

위험한 엔드포인트들

┌─────────────────────────────────────────────────────────────────┐
│          프로덕션에서 주의해야 할 위험 엔드포인트                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌───────────────────┬────────────────────────────────────────┐ │
│  │  엔드포인트       │  위험                                  │ │
│  ├───────────────────┼────────────────────────────────────────┤ │
│  │  /actuator/env    │  환경변수, 비밀번호, API 키 노출       │ │
│  │                   │  → DB 패스워드, 토큰 등 유출 가능      │ │
│  ├───────────────────┼────────────────────────────────────────┤ │
│  │  /actuator/       │  JVM 전체 메모리 덤프                  │ │
│  │  heapdump         │  → 메모리에 있는 비밀 정보 포함 가능   │ │
│  │                   │  → 파일 크기 수백 MB~수 GB             │ │
│  ├───────────────────┼────────────────────────────────────────┤ │
│  │  /actuator/       │  로그 레벨 실시간 변경 가능            │ │
│  │  loggers          │  → DEBUG로 변경 시 정보 노출           │ │
│  │                   │  → 과도한 로깅으로 성능 저하           │ │
│  ├───────────────────┼────────────────────────────────────────┤ │
│  │  /actuator/       │  앱 원격 종료 가능                     │ │
│  │  shutdown         │  → 기본 비활성화 (enabled: false)      │ │
│  │                   │  → 절대 인터넷에 노출 금지             │ │
│  └───────────────────┴────────────────────────────────────────┘ │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

프로덕션 권장 설정

┌─────────────────────────────────────────────────────────────────┐
│          프로덕션 보안 전략                                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  1. 필요한 것만 노출                                             │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  management:                                                 │ │
│  │    endpoints:                                                │ │
│  │      web:                                                    │ │
│  │        exposure:                                             │ │
│  │          include: health, prometheus  # 이 두 개만!          │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  2. 별도 관리 포트 사용                                          │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  management:                                                 │ │
│  │    server:                                                   │ │
│  │      port: 8081  # 메인 포트(8080)와 분리                   │ │
│  │                  # 인터넷에 노출하지 않음                    │ │
│  │                  # 내부 네트워크에서만 접근 가능             │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  3. Spring Security로 인증 추가                                  │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  @Configuration                                              │ │
│  │  public class ActuatorSecurityConfig {                       │ │
│  │      @Bean                                                   │ │
│  │      public SecurityFilterChain actuatorSecurity(            │ │
│  │              HttpSecurity http) throws Exception {           │ │
│  │          http.requestMatcher(                                │ │
│  │                  EndpointRequest.toAnyEndpoint())            │ │
│  │              .authorizeRequests(auth ->                      │ │
│  │                  auth.anyRequest().hasRole("ACTUATOR"))      │ │
│  │              .httpBasic();                                   │ │
│  │          return http.build();                                │ │
│  │      }                                                       │ │
│  │  }                                                           │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  요약:                                                           │
│  ├── 노출 최소화: health, prometheus만                           │
│  ├── 포트 분리: 8081 (내부 전용)                                 │
│  └── 인증 필수: Spring Security로 접근 제어                      │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

10. 완전한 Kubernetes 배포 설정 예시

application.yml (전체)

server:
  port: 8080
  shutdown: graceful                     # 그레이스풀 셧다운 활성화

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s      # 셧다운 시 최대 30초 대기

management:
  server:
    port: 8081                           # 별도 관리 포트
  endpoints:
    web:
      exposure:
        include: health, prometheus      # 필요한 것만 노출
  endpoint:
    health:
      probes:
        enabled: true                    # 프로브 활성화
      group:
        readiness:
          include: readinessState        # readiness 그룹
        liveness:
          include: livenessState         # liveness 그룹 (최소한)

Kubernetes Deployment YAML (전체)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1           # 한 번에 1개 추가 Pod 생성
      maxUnavailable: 0     # 항상 최소 replicas 유지 (Zero Downtime)
  template:
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: my-app
          image: my-app:latest
          ports:
            - containerPort: 8080
              name: http
            - containerPort: 8081
              name: management

          # Startup Probe: 앱 시작 완료 확인
          startupProbe:
            httpGet:
              path: /actuator/health/liveness
              port: management
            initialDelaySeconds: 5
            periodSeconds: 10
            failureThreshold: 30       # 30 * 10 = 300초(5분) 허용

          # Liveness Probe: 앱이 살아있는지 확인
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: management
            periodSeconds: 10
            failureThreshold: 3        # 3번 연속 실패 시 재시작

          # Readiness Probe: 트래픽 수신 가능 확인
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: management
            periodSeconds: 5
            failureThreshold: 3        # 3번 연속 실패 시 LB 제거

          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 5"]
                # preStop: K8s LB 업데이트 전 잠시 대기
                # iptables 전파 시간을 벌어주는 안전 장치

동작 시나리오

┌─────────────────────────────────────────────────────────────────┐
│          Rolling Update: Zero-Downtime 배포 흐름                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  [배포 시작] → 새 Pod 생성 (v2)                                  │
│       │                                                          │
│       ▼                                                          │
│  [startupProbe] → /liveness 체크                                 │
│       │           성공할 때까지 반복 (최대 5분)                   │
│       │                                                          │
│       ▼  (startup 성공)                                          │
│  [readinessProbe] → /readiness 체크                              │
│       │             UP → LB에 추가 → 트래픽 수신 시작           │
│       │                                                          │
│       ▼  (새 Pod v2 정상 운영)                                   │
│  ┌──────────────────────────────────────────────────┐            │
│  │  이 시점에서:                                      │            │
│  │  ├── 새 Pod (v2): 트래픽 받는 중 ✅                │            │
│  │  └── 구 Pod (v1): 아직 트래픽 받는 중              │            │
│  └──────────────────────────────────────────────────┘            │
│       │                                                          │
│       ▼  (구 Pod 종료 시작)                                      │
│  [구 Pod v1]                                                     │
│       │                                                          │
│       ├── preStop: sleep 5 (LB 전파 시간 확보)                   │
│       ├── readiness → REFUSING_TRAFFIC                           │
│       ├── LB에서 제거 → 새 요청 안 들어옴                        │
│       ├── 진행 중인 요청 완료 대기 (최대 30초)                   │
│       └── 안전하게 종료                                          │
│                                                                   │
│  결과:                                                           │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  어떤 순간에도 최소 1개 Pod이 트래픽 처리                    │ │
│  │  → Zero Downtime!                                            │ │
│  │                                                              │ │
│  │  maxUnavailable: 0 덕분에 구 Pod은 새 Pod이                  │ │
│  │  ready 된 후에만 종료 시작                                   │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

preStop과 Graceful Shutdown의 관계

┌─────────────────────────────────────────────────────────────────┐
│          preStop + Graceful Shutdown = 완벽한 종료                │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  시간순:                                                         │
│                                                                   │
│  K8s: "이 Pod 종료해"                                            │
│       │                                                          │
│       ▼                                                          │
│  [preStop: sleep 5]  (5초 대기)                                  │
│  │  → 이 동안 K8s가 iptables / Service 업데이트                  │
│  │  → 새 요청이 이 Pod으로 안 오게 됨                            │
│       │                                                          │
│       ▼                                                          │
│  [SIGTERM 전달]                                                  │
│  │  → Spring Boot: readiness → REFUSING_TRAFFIC                 │
│  │  → server.shutdown: graceful 활성화                           │
│       │                                                          │
│       ▼                                                          │
│  [Graceful Drain]  (최대 30초)                                   │
│  │  → 진행 중인 HTTP 요청 완료 대기                              │
│  │  → 새 요청은 503 거부                                         │
│       │                                                          │
│       ▼                                                          │
│  [Context Close]                                                 │
│  │  → 빈 소멸, DB 커넥션 풀 종료                                │
│       │                                                          │
│       ▼                                                          │
│  Pod 종료 완료                                                   │
│                                                                   │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  terminationGracePeriodSeconds (60초)                        │ │
│  │  = preStop(5초) + SIGTERM~종료(최대 55초)                    │ │
│  │                                                              │ │
│  │  이 시간 내에 종료 안 되면 SIGKILL (강제 종료)              │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

11. 정리

┌─────────────────────────────────────────────────────────────────┐
│                         요약                                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  Actuator란?                                                     │
│  └── 하나의 의존성으로 프로덕션 운영 기능을 제공하는             │
│      Spring Boot 모듈                                            │
│                                                                   │
│  설치:                                                           │
│  └── 의존성 하나 + 최소 설정                                     │
│      (Maven / Gradle / Bazel 모두 동일)                          │
│                                                                   │
│  Liveness:                                                       │
│  ├── "앱이 살아있나?"                                            │
│  ├── 실패 시 K8s가 Pod 재시작                                    │
│  └── 외부 시스템(DB, Redis 등) 절대 포함 금지!                   │
│                                                                   │
│  Readiness:                                                      │
│  ├── "트래픽 받을 수 있나?"                                      │
│  ├── 실패 시 LB에서 제거 (재시작 아님)                           │
│  └── 복구 시 자동 재개                                           │
│                                                                   │
│  왜 분리?                                                        │
│  └── 단일 /health에 모든 것 넣으면 캐스케이딩 실패 위험          │
│                                                                   │
│  핵심 원칙:                                                      │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  "DB 다운 = 트래픽 멈춤 (readiness의 역할)                  │ │
│  │           ≠ 앱 재시작 (liveness의 역할이 아님)"              │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  K8s 자동 감지:                                                  │
│  └── KUBERNETES_SERVICE_HOST 환경변수로 자동 활성화              │
│                                                                   │
│  Startup Probe:                                                  │
│  └── 앱 시작이 오래 걸릴 때 liveness 체크 지연                   │
│                                                                   │
│  프로덕션 보안:                                                  │
│  ├── 별도 관리 포트 (8081)                                       │
│  ├── 최소 엔드포인트 노출 (health, prometheus)                   │
│  └── Spring Security로 인증                                      │
│                                                                   │
│  Zero-Downtime 배포:                                             │
│  ├── Graceful Shutdown + preStop hook                            │
│  ├── readiness → REFUSING_TRAFFIC → LB 제거 → 드레인 → 종료    │
│  └── maxUnavailable: 0으로 항상 최소 Pod 수 유지                 │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

관련 키워드

Spring Boot Actuator, Health Endpoint, Liveness Probe, Readiness Probe, Startup Probe, Kubernetes, HealthIndicator, Health Groups, ApplicationAvailability, LivenessState, ReadinessState, StatusAggregator, Graceful Shutdown, Zero-Downtime, Rolling Update, preStop, terminationGracePeriodSeconds, Cascading Failure, Thundering Herd, Spring Boot 2.3, KUBERNETES_SERVICE_HOST, Prometheus, Micrometer, management.server.port, show-details, probes.enabled, AbstractHealthIndicator, AvailabilityChangeEvent, CompositeHealthContributor, Spring Security, 자가 치유, 캐스케이딩 실패