Health Check 제대로 이해하기

·2026년 2월 10일

study

목록 보기
7/7

배경

readiness를 체크하지 않고 있어서 이 부분을 추가해야한다는 데브옵스분의 요청이 들어왔다. 근데 readiness, liveness가 뭐가 다른지, health 하나로 하면 안되는 이유가 뭔지. 더 나아가서 health check 자체에 의문이 생겼다.

그렇게 health check를 "제대로" 추가하기 위해 공부한 내용에 대해서 정리해본다.


Health Check란 무엇인가

Health Check는 "서비스가 정상인지 자동으로 확인하는 것"이다.

실제 운영 환경에서는 이런 흐름으로 동작한다:

문제 상황:
DB 연결 끊김 → 애플리케이션은 실행 중 → 사용자 요청 실패 → 에러 발생

Health Check 적용:
DB 연결 끊김 → Health Check 감지 → 트래픽 차단 → 사용자는 정상 서버로 라우팅

결국 장애를 자동으로 감지하고, 자동으로 대응하기 위한 것이다.


Liveness vs Readiness

둘 다 "서비스가 정상인지" 체크하는 건데 왜 나눠놨을까?
실제로 확인을 해 보니 목적이 완전히 달랐다.

Liveness Probe (생존 확인)

"애플리케이션이 살아있나?"

데드락이나 무한 루프 같은 내부 문제로 애플리케이션이 응답 불가 상태가 되었는지만 확인한다. 실패하면 Pod를 재시작한다.

💡 여기서 중요한 건 외부 의존성을 체크하지 않는다는 것이다. DB 연결, 외부 API 같은 건 확인하지 않는다. 왜냐하면 DB가 죽었다고 Pod를 재시작해봤자 소용없기 때문이다. 오히려 계속 재시작만 반복하는 무한 루프에 빠진다.

Readiness Probe (준비 상태 확인)

"애플리케이션이 트래픽을 받을 준비가 되었나?"

DB 연결, 필수 외부 서비스 같은 외부 의존성을 포함해서 요청을 처리할 수 있는 상태인지 확인한다. 실패하면 트래픽만 차단하고 Pod는 재시작하지 않는다.

이게 핵심이다. DB가 죽었다면 재시작할 게 아니라 일단 트래픽을 받지 않는 것이 맞다. DB가 복구되면 자동으로 다시 트래픽을 받으면 된다.

정리

  • Liveness: 애플리케이션 서버의 상태만 확인 → 실패 시 재시작
  • Readiness: 외부 의존성 포함하여 실제로 서비스가 돌아갈 수 있는지 확인 → 실패 시 트래픽 차단

Backend에서의 처리

Spring Boot는 DB 같은 기본적인 것들은 자동으로 체크해준다. 하지만 외부 서비스 같은 건 직접 만들어야 한다.

Custom Health Indicator 구조

CLASS CustomDbHealthIndicator:

// 1. DataSource 주입
INJECT dataSource

// 2. health() 메서드 구현
FUNCTION health():
  TRY:
      // DB 연결 시도
      connection = dataSource.getConnection()

      // 간단한 쿼리로 확인
      connection.execute("SELECT 1")

      connection.close()

      // 성공 시 UP 반환
      RETURN Health.up()
          .withDetail("database", "MySQL")
          .withDetail("status", "connected")

  CATCH error:
      // 실패 로깅 (중요!)
      LOG("DB health check failed: " + error)

      // 실패 시 DOWN 반환
      RETURN Health.down()
          .withDetail("database", "MySQL")
          .withDetail("error", error.message)

외부 서비스 Health Indicator

CLASS CustomURLHealthIndicator:

INJECT 외부URL

FUNCTION health():
    TRY:
        // 타임아웃 3초 설정 (중요!)
        restTemplate.setTimeout(3_seconds)

        // Health endpoint 호출
        response = restTemplate.get(외부URL + "/actuator/health")

        IF response.status == 200:
            RETURN Health.up()
                .withDetail("service", "외부URL")
                .withDetail("status", "connected")
        ELSE:
            RETURN Health.down()

    CATCH timeout OR connection_error:
        LOG("health check failed")
        RETURN Health.down()
            .withDetail("service", "외부URL")
            .withDetail("error", "Connection failed")

💡 주의할 점

타임아웃을 반드시 설정해야 한다. Health Check 자체가 타임아웃 나면 안 되기 때문이다. 3초 정도가 적당하다.

그리고 실패 시 로깅을 남겨야 한다. 나중에 왜 Readiness가 실패했는지 확인할 때 필요하다.


설정 파일

management:
endpoint:
  health:
    probes:
      enabled: true
    group:
      # Liveness: 내부 상태만
      liveness:
        include: livenessState

      # Readiness: 외부 의존성 포함
      readiness:
        include: readinessState, customDB, 외부서비스

이름이 정확히 매칭되어야 한다. @Component("외부서비스")로 등록했으면 설정 파일에도 "외부서비스"로 써야 한다.


실제 시나리오

  1. DB 연결 끊김
  2. Readiness Probe 실패 감지
  3. 2번째 실패
  4. Kubernetes가 해당 Pod로 트래픽 차단
  5. 사용자 요청은 정상 Pod로 자동 라우팅
  6. DB 복구
  7. Readiness Probe 성공
  8. 다시 트래픽 수신

이 과정이 자동으로 이루어진다. 사용자는 에러를 보지 않는다.


Frontend에서의 처리

주기적 체크

30초마다 Health Check API를 호출해서 서버 상태를 확인한다.

FUNCTION monitorHealth():
    EVERY 30_seconds DO:
        TRY:
            response = fetch("/actuator/health", timeout: 5_seconds)

            IF response.status == 200:
                hideErrorBanner()
                enableUserActions()
            ELSE:
                showErrorBanner("서버 점검 중입니다")
                disableUserActions()

        CATCH error:
            showErrorBanner("서버 연결 실패")

서버가 다운되면 배너를 띄워서 사용자에게 알린다.

재시도 로직

503 에러가 나면 바로 실패로 처리하지 말고, 지수 백오프 방식으로 재시도한다.

FUNCTION apiCallWithRetry(endpoint, data, maxRetries: 3):
    FOR attempt FROM 1 TO maxRetries:
        TRY:
            response = apiCall(endpoint, data)
            RETURN response

        CATCH error:
            IF error.status == 503:  // Service Unavailable
                IF attempt < maxRetries:
                    // 지수 백오프: 2초, 4초, 8초
                    wait(2_seconds * attempt)
                    CONTINUE
                ELSE:
                    showError("서비스가 일시적으로 불안정합니다")
                    RETURN null
            ELSE:
                // 다른 에러는 재시도 안 함
                THROW error

일시적인 장애는 이렇게 자동으로 복구될 수 있다.


실수하기 쉬운 것들

1. Liveness에 DB 넣기

이건 절대 하면 안 된다. DB 장애 시 Pod가 무한 재시작에 빠진다. 재시작해봤자 DB는 살아나지 않기 때문이다.

❌ 잘못된 예:

liveness:
   include: livenessState, db  # DB 장애 시 무한 재시작

✅ 올바른 예:

liveness:
   include: livenessState      # 내부 상태만
readiness:
   include: readinessState, db # DB는 여기에

2. 타임아웃 설정 안 하기

외부 서비스가 응답이 없으면 Health Check 자체가 타임아웃 난다. 3초 정도로 설정해야 한다.

❌ 타임아웃 없음:

restTemplate.get(url)  # 무한 대기 가능

✅ 타임아웃 설정:

restTemplate.setTimeout(3_seconds)
restTemplate.get(url)

3. 너무 많은 것 체크하기

선택적인 외부 서비스까지 체크하면 불필요하게 트래픽이 차단된다.

❌ 너무 많이 체크:

readiness:
  include: db, auth, mail, sms, analytics, logging, ...

✅ 필수만 체크:

readiness:
  include: db, auth  # 없으면 서비스가 안 돌아가는 것만

결론

  • Liveness는 내부 상태만 체크해서 데드락 같은 문제를 감지한다.
  • Readiness는 외부 의존성까지 체크해서 요청 처리 가능 여부를 확인한다.

이 둘을 제대로 구분해서 설정하면 장애를 자동으로 감지하고 복구하는 시스템을 만들 수 있다. DB가 죽어도 사용자는 에러를 보지 않고, 복구되면 자동으로 다시 트래픽을 받는다.

profile
개발 공부를 하고, 작업을 합니다.

0개의 댓글