본문 바로가기

상태·정보·로그 제어

생존·준비 상태와 HealthIndicator를 구성하고 빌드 정보·로그 수준·HTTP 기록의 노출을 관리합니다.

Actuator의 health, info, loggers, httpexchanges 네 엔드포인트는 모두 JSON을 돌려주지만 목적이 다릅니다. 상태(health)는 트래픽을 받아도 되는지 판단하고, 정보(info)는 어떤 산출물이 배포됐는지 알려 주며, loggers는 실행 중에 진단 수준을 바꾸고, HTTP 교환 기록은 최근 요청의 흔적을 보여 줍니다.

네 엔드포인트에 같은 노출 정책과 보존 기간을 적용하면, 준비 상태 응답이 비밀 정보를 드러내거나 디버그 기록이 메모리를 잠식할 수 있습니다. 이 글에서는 엔드포인트마다 어떤 질문에 답하고 어떤 복구 행동으로 이어지는지 나눠 살펴봅니다.

OPERATIONS · QUESTION AND RECOVERY

네 엔드포인트는 질문과 복구 행동이 다르다

health는 트래픽 수용, info는 배포 신원, loggers는 실행 중 변경, httpexchanges는 최근 요청 진단을 제공합니다. 같은 노출·보존 정책으로 묶지 않습니다.

프로세스 생존과 대기량 준비 상태의 두 단계원 ProbeDecision.backlog의 분기만 그린다. 프로세스 비정상이면 진단 뒤 재시작 후보, 정상이고 pending>10000이면 생존 true·준비 false로 트래픽 제거와 복구, 나머지는 생존·준비 true다. Health 상세의 high와 전체 상태를 혼동하지 않는다. info/loggers/교환 기록은 보조 패널에 별도 보존한다. 모바일도 같은 5노드4관계이다.프로세스 정상?재시작 후보진단 먼저대기량10000 초과?트래픽 제거대기 원인 복구트래픽 수용SERVE아니요예예아니요
프로세스 생존과 대기량 준비 상태의 두 단계원 ProbeDecision.backlog의 분기만 그린다. 프로세스 비정상이면 진단 뒤 재시작 후보, 정상이고 pending>10000이면 생존 true·준비 false로 트래픽 제거와 복구, 나머지는 생존·준비 true다. Health 상세의 high와 전체 상태를 혼동하지 않는다. info/loggers/교환 기록은 보조 패널에 별도 보존한다. 모바일도 같은 5노드4관계이다.프로세스정상?재시작 후보진단 먼저대기량10000 초과?트래픽 제거대기 원인 복구트래픽 수용SERVE아니요예예아니요

health · 생존과 준비

생존 실패는 프로세스 재시작 판단, 준비 실패는 새 요청 수용 판단입니다. 일시적 DB 지연으로 정상 JVM을 반복 재시작하지 않습니다.

핵심 쓰기 실패는 트래픽 제거/회복을 검토하고, 선택적 전송 장애는 별도 신호로 둡니다. 공개 상태와 제한된 상세를 분리합니다.

info · 산출물 신원

기대 커밋·빌드 버전·스키마 세대와 실제 배포를 비교합니다. 환경 전체와 비밀 정보를 복사하지 않습니다.

인스턴스마다 바뀌는 요청 ID·현재 시각은 배포 신원에 넣지 않습니다.

loggers · 임시 변경

원래 수준·변경자·만료 시각을 남기고 좁은 로거를 변경합니다. 복귀 작업과 감사 기록은 애플리케이션이 구현해야 합니다.

본문 TemporaryLogLevel은 변경 기록 모델입니다. 실제 변경·자동 복귀 스케줄러·Clock 주입은 구현하지 않습니다.

httpexchanges · 제한 버퍼

리포지토리 빈과 보존 개수를 확인합니다. 메모리 버퍼는 재시작 때 사라지며 전체 트래픽의 영속 감사 로그가 아닙니다.

본문·인가/Cookie 헤더·쿼리 토큰을 수집하지 않고 크기·표본 정책을 둡니다.

HTTP 성공 코드만 보지 않고 실제 상태 상세 권한, 적용된 로그 수준과 만료 복귀, 기록 내용과 보존을 확인합니다.

그림의 위쪽은 이 글의 연습 문제에 나오는 ProbeDecision.backlog의 분기입니다. 프로세스가 비정상이면 진단한 뒤 재시작할 후보가 되고, 정상인데 대기량이 10000을 넘으면 생존은 참·준비는 거짓으로 두어 트래픽을 빼고 대기의 원인을 복구하며, 나머지는 생존과 준비 모두 참으로 트래픽을 받습니다. 아래 네 칸은 health, info, loggers, httpexchanges가 각각 답하는 질문과, 공개 범위와 보존 기간을 어떻게 다르게 정하는지를 정리합니다.


상태로 트래픽 제어하기

상태 지표(health indicator)는 의존성을 확인해 UP, DOWN, OUT_OF_SERVICE, UNKNOWN 같은 상태를 내놓습니다. 여러 지표의 상태를 모은 결과가 우선순위와 HTTP 상태 매핑을 거쳐 최종 점검 응답이 됩니다. 모든 의존성의 장애를 DOWN으로 만들면, 선택 기능 하나 때문에 인스턴스 전체가 로드 밸런서에서 빠질 수 있습니다.

생존 상태(liveness)는 프로세스가 스스로 회복할 수 없는 상태인지를, 준비 상태(readiness)는 지금 새 요청을 받을 준비가 됐는지를 답합니다. 데이터베이스의 일시적 지연을 생존 상태의 실패로 연결하면, 오케스트레이터가 정상적인 프로세스를 거듭 재시작해 장애를 키울 수 있습니다. 의존성마다 어느 그룹에 넣을지를 서비스의 성능 저하 정책과 함께 정합니다.

src/main/java/board/operations/BacklogHealthIndicator.java
package board.operations;

import java.util.function.IntSupplier;

import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.stereotype.Component;

@Component("postBacklog")
public class BacklogHealthIndicator implements HealthIndicator {
    private final IntSupplier pendingCount;

    public BacklogHealthIndicator(IntSupplier pendingCount) {
        this.pendingCount = pendingCount;
    }

    @Override
    public Health health() {
        int pending = pendingCount.getAsInt();
        if (pending > 10_000) {
            return Health.outOfService()
                    .withDetail("pendingBucket", "over-10000")
                    .build();
        }
        return Health.up()
                .withDetail("pendingBucket", pending > 1_000 ? "high" : "normal")
                .build();
    }
}

이 지표는 대기 건수를 돌려주는 IntSupplier 빈이 같은 컨텍스트에 등록되어 있다고 전제합니다. 앞 글의 PostQueueEndpoint를 쓴다면, 같은 인스턴스의 pending 값을 읽는 공급자를 애플리케이션 구성에서 빈으로 등록합니다. 공급자가 없으면 상태 값을 확인하기 전에 생성자 의존성부터 해결해야 합니다.

대기 건수가 10000을 넘으면 OUT_OF_SERVICE, 그 이하이면 UP이며, 1000을 넘는지에 따라 상세에 high나 normal을 붙입니다. 상세에는 대기열 항목, SQL 예외의 원문, 인증 정보를 넣지 않습니다. 원격 의존성을 점검할 때는 짧은 타임아웃과 격벽을 적용해, 상태 확인 호출이 새로운 부하의 원인이 되지 않게 합니다.


상태 그룹

애플리케이션이 시작하는 동안 준비 상태는 아직 요청을 받지 않는 상태일 수 있습니다. 마이그레이션과 캐시 준비가 끝난 뒤 트래픽을 받는 상태로 바꾸고, 단계적 종료가 시작되면 먼저 준비 상태를 내린 뒤 처리 중인 요청을 마칩니다. 점검 간격과 실패 임계값이 이 전환에 걸리는 시간보다 너무 짧지 않은지 확인합니다.

사용자 정의 그룹으로 데이터베이스 쓰기, 메트릭의 외부 전송, 대기량을 나눌 수 있습니다. 핵심 데이터의 쓰기가 실패하면 준비 상태를 내리지만, 선택적인 분석용 메트릭 전송의 실패는 UP의 상세나 별도의 메트릭으로 경보할 수 있습니다. 상태를 초록색으로 보이게 하려고 실패를 숨기지 말고, 트래픽을 빼는 것이 올바른 복구인지를 기준으로 정합니다.

더 깊이: Boot의 생존·준비 그룹에는 처음에 무엇이 들어 있을까

Spring Boot의 Actuator는 생존과 준비 점검을 /actuator/health/liveness와 /actuator/health/readiness라는 상태 그룹으로 제공합니다. Actuator 문서에 따르면 두 그룹에는 애플리케이션의 생존·준비 상태를 그대로 보여 주는 LivenessStateHealthIndicator와 ReadinessStateHealthIndicator만 들어 있고, Boot는 다른 상태 지표를 이 그룹에 넣지 않습니다. 그래서 데이터베이스 지표가 DOWN이 되어도 기본 준비 그룹은 그대로이며, 이 글의 대기량 지표처럼 트래픽 제어에 쓰려는 지표는 management.endpoint.health.group.readiness.include=readinessState,<지표 이름>처럼 직접 더해야 합니다. 그룹은 자동으로 켜지며, management.endpoint.health.probes.enabled로 끌 수 있습니다.

외부 시스템을 어느 그룹에 넣을지에 대해서도 같은 문서가 판단 기준을 줍니다. 생존 점검은 외부 시스템에 기대면 안 되는데, 데이터베이스 하나가 실패했을 때 Kubernetes가 모든 인스턴스를 재시작해 연쇄 장애를 만들 수 있기 때문입니다. 준비 점검은 개발자가 신중히 고를 문제로, 모든 인스턴스가 함께 쓰는 외부 시스템을 넣으면 그 시스템이 내려갈 때 모든 인스턴스가 트래픽에서 빠진다는 점을 감수해야 합니다. 응답 코드의 기본 매핑도 알아 둘 만합니다. DOWN과 OUT_OF_SERVICE는 503, UP과 UNKNOWN은 200이므로, 이 글의 대기량 지표가 OUT_OF_SERVICE를 내면 그 지표를 포함한 그룹의 점검은 503을 돌려줍니다.


빌드와 출처 정보

빌드 정보에는 산출물 이름, 버전, 빌드 시간을, Git 정보에는 커밋 ID와 브랜치 같은 출처를 담을 수 있습니다. 자바와 운영체제의 정보는 런타임이 어긋났을 때 진단하는 데 쓸모 있습니다. 환경 변수 전체를 정보에 복사하면 비밀 정보와 내부 구성이 새어 나가므로, 허용 목록에 있는 값만 씁니다.

src/main/java/board/operations/BoardInfoContributor.java
package board.operations;

import java.util.Map;

import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;

@Component
public class BoardInfoContributor implements InfoContributor {
    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("board", Map.of(
                "curriculumRevision", "2026-07",
                "schemaGeneration", 4,
                "runtime", Runtime.version().feature()));
    }
}

인스턴스마다 달라지는 요청 ID나 타임스탬프는 정보에 넣지 않습니다. 배포 컨트롤러는 기대한 커밋과 정보 엔드포인트에 표시된 커밋을 비교해, 이전 버전의 파드가 남아 있지 않은지 확인합니다.


로그 수준 변경과 만료

실행 중에 로그 수준을 바꾸면 재배포 없이 문제를 좁힐 수 있지만, 페이로드나 SQL, 토큰이 담긴 로그가 급격히 늘어날 수 있습니다. 쓰기 작업은 운영자 역할에만 허용하고, 누가 어떤 로거를 어느 수준으로 바꿨는지 감사 로그에 기록합니다. 자동으로 되돌릴 기한을 두고, 장애가 끝난 뒤 원래 수준으로 돌아왔는지 확인합니다.

패키지 로거를 DEBUG로 올리기 전에는 예상되는 이벤트 비율과 저장소 예산을 계산합니다. 루트 로거 전체를 TRACE로 바꾸지 말고 가장 좁은 패키지를 고릅니다. 애플리케이션을 재시작하면 영구 설정으로 돌아가는지, 중앙의 로깅 제어가 값을 다시 덮어쓰는지도 운영 문서에 적습니다.

src/main/java/board/operations/TemporaryLogLevel.java
package board.operations;

import java.time.Duration;
import java.time.Instant;

public record TemporaryLogLevel(
        String loggerName,
        String originalLevel,
        String temporaryLevel,
        Instant expiresAt
) {
    public TemporaryLogLevel {
        if (loggerName.isBlank() || expiresAt.isBefore(Instant.now())) {
            throw new IllegalArgumentException("invalid temporary logger change");
        }
    }

    public static TemporaryLogLevel forWindow(
            String loggerName,
            String originalLevel,
            String temporaryLevel,
            Instant now,
            Duration window
    ) {
        return new TemporaryLogLevel(
                loggerName, originalLevel, temporaryLevel, now.plus(window));
    }
}

TemporaryLogLevel은 원래 수준, 임시 수준, 만료 시각을 담는 기록입니다. 이 객체를 만들거나 expiresAt이 지났다는 사실만으로 loggers 엔드포인트의 수준이 바뀌거나 되돌아오지는 않으므로, 변경을 적용하고 만료되면 되돌리며 실패를 기록하는 실행 주체가 따로 있어야 합니다. 또 forWindow에 넘긴 now와 생성자 안의 Instant.now()는 서로 다른 시각이므로, 과거의 고정 시각을 넣은 시험은 유효성 검사에서 실패할 수 있습니다. 실제 구현에서는 시계를 주입해 생성자가 현재 시각에 의존하지 않게 하고, 스케줄러의 실패도 감사 기록에 남깁니다.


HTTP 교환 기록의 보존 정책

HTTP 교환 기록은 최근 요청과 응답의 메서드, URI, 상태, 소요 시간, 일부 헤더를 보여 줍니다. 기록을 저장할 HttpExchangeRepository 빈이 없으면 기록 자체가 켜지지 않고 httpexchanges 엔드포인트도 만들어지지 않으므로, 노출 목록에 넣어도 HTTP로 읽을 수 없습니다. 메모리 기반 리포지토리는 제한된 수의 최근 항목만 두고 프로세스가 재시작하면 사라지는 진단용 버퍼로 씁니다.

Authorization 헤더, Cookie, Set-Cookie, 쿼리 문자열의 토큰, 요청 본문은 수집하지 않습니다. 이 기록은 모든 트래픽을 무기한 저장하는 감사 로그가 아니며, 부하가 큰 환경에서는 표본 추출이나 전용 트레이싱 백엔드가 알맞습니다. URI에 ID가 그대로 들어가면 카디널리티와 개인정보 문제가 생기므로, 경로 템플릿을 기준으로 집계합니다.


엔드포인트별 실패 해석

같은 운영 실패라도 엔드포인트마다 뜻하는 바와 다음 행동이 다릅니다. 아래 예의 각 줄은 화살표 왼쪽이 관측한 상태이고, 오른쪽이 그 상태가 가리키는 원인 후보나 다음 행동입니다. 예를 들어 생존 상태가 DOWN이면 프로세스 자체를 의심해 재시작을 검토하지만, 준비 상태가 OUT_OF_SERVICE이면 프로세스는 살아 있으므로 트래픽을 빼고 의존성이 돌아오기를 기다립니다.

운영 관측 실패 예
liveness DOWN -> process deadlock 진단, restart 후보
readiness OUT_OF_SERVICE -> traffic drain, dependency recovery 대기
info commit mismatch -> stale rollout instance
logger DEBUG expired but still active -> LOG_LEVEL_RESTORE_FAILED
httpexchanges retained=10000 -> DIAGNOSTIC_BUFFER_UNBOUNDED

상태의 상세를 볼 수 있는 권한과, 상태만 확인하는 공개 점검을 나눕니다. 로거 변경 요청이 성공했다는 응답만 믿지 말고, 그 뒤에 실제로 적용된 수준과 만료 후 복귀를 감사 기록으로 확인합니다.


정리

  • 생존 상태는 재시작이 필요한지를, 준비 상태는 지금 트래픽을 받아도 되는지를 답하며, 일시적인 의존성 지연을 생존 실패로 연결하지 않습니다.
  • 정보 엔드포인트에는 커밋, 버전, 스키마 세대 같은 배포의 신원만 허용 목록으로 담습니다.
  • 로그 수준 변경은 원래 수준, 변경자, 만료 시각을 기록하고, 되돌리는 실행 주체를 따로 둡니다.
  • HTTP 교환 기록은 크기가 제한된 진단 버퍼이며, 인증 헤더와 쿠키, 토큰, 본문은 수집하지 않습니다.

연습 문제

1. 대기량, 정보, 로거의 구성

회원 게시판의 대기량 지표를 준비 상태 그룹에만 넣고, 커밋 ID와 스키마 버전을 정보 엔드포인트에 제공하세요. board.import 로거를 15분 동안 DEBUG로 올렸다가 자동으로 되돌리는 변경 기록을 설계하고, HTTP 교환 기록에 Authorization 헤더와 쿼리 토큰이 남지 않는지 테스트하세요.

해설 보기

지표가 실패할 때 재시작이 해결책인지, 트래픽 차단이 해결책인지를 먼저 적습니다. 다음 결정 모델은 프로세스 상태와 대기량을 보고 생존·준비 상태와 다음 행동을 고릅니다.

package board.operations;

public record ProbeDecision(boolean live, boolean ready, String action) {
    public static ProbeDecision backlog(boolean processHealthy, int pending) {
        if (!processHealthy) {
            return new ProbeDecision(false, false, "RESTART_AFTER_DIAGNOSIS");
        }
        if (pending > 10_000) {
            return new ProbeDecision(true, false, "DRAIN_AND_RECOVER");
        }
        return new ProbeDecision(true, true, "SERVE");
    }
}

운영 엔드포인트로 값을 바꿀 때는 기존 값, 변경자, 만료 시각을 반드시 함께 남깁니다. 선택적인 메트릭 전송 기능의 장애는 이 결정과 별도의 메트릭으로 표현합니다.

2. 세 입력의 결정

ProbeDecision.backlog에 A는 processHealthy=true, pending=10001을, B는 false, pending=0을, C는 true, pending=10000을 넣습니다. 각각의 live, ready, action을 구하세요. A를 재시작하면 대기량의 원인이 저절로 사라지나요?

해설 보기

A는 true, false, DRAIN_AND_RECOVER이고, B는 false, false, RESTART_AFTER_DIAGNOSIS이며, C는 true, true, SERVE입니다. 첫 분기가 프로세스 상태를 먼저 보고, 정상인 프로세스에서는 대기량이 10000을 넘는지만 봅니다. C의 10000은 넘지 않으므로 트래픽을 받습니다.

A는 트래픽을 빼고 원인을 복구하는 쪽을 고르는 모델입니다. 재시작한다고 외부 의존성의 문제나 영속 대기열에 쌓인 양이 해결된다는 보장은 없습니다.

3. 상태 지표의 경곗값

BacklogHealthIndicator에 대기 건수 1000, 1001, 10000, 10001을 차례로 주면, 각각의 상태와 pendingBucket 상세는 무엇인가요? 상세가 high이면 전체 상태가 DOWN이 되나요?

해설 보기

1000은 UP과 normal, 1001은 UP과 high, 10000은 UP과 high, 10001은 OUT_OF_SERVICE와 over-10000입니다. 비교가 각각 > 1_000과 > 10_000이므로, 임계값과 같은 값은 아래 구간에 남습니다.

high는 상세에 붙은 문자열일 뿐이며, 상태는 여전히 UP입니다. 상세 값으로 경보를 내고 싶다면 별도의 메트릭이나 규칙을 두어야 하며, 상세 문자열이 트래픽 차단을 일으키지는 않습니다.

다음 글 Actuator 보안에서는 이 엔드포인트들에 누가 접근할 수 있는지를 Spring Security로 나누고, 공개 점검과 운영자 작업을 분리하는 방법을 다룹니다.