본문 바로가기

Actuator 엔드포인트

Actuator 엔드포인트의 활성화와 노출을 구분하고 관리 포트·사용자 엔드포인트를 안전하게 설정합니다.

개발 환경에서는 브라우저에 페이지가 보이면 성공한 것처럼 느껴집니다. 하지만 운영 환경에서는 애플리케이션이 살아 있는지, 데이터베이스에 연결되는지, 요청이 얼마나 느린지까지 확인해야 합니다. 이렇게 실행 중인 시스템의 상태를 바깥에서 이해할 수 있게 만드는 성질을 관측 가능성(observability)이라고 합니다.

Spring Boot Actuator는 상태, 정보, 메트릭 같은 운영용 엔드포인트를 제공합니다. 엔드포인트(endpoint)는 운영 정보를 읽거나 일부 설정을 바꾸는 접근 지점이고, Micrometer는 카운터와 타이머 같은 메트릭을 여러 관측 시스템에 전달하는 공통 계층입니다. Actuator 스타터를 추가했다고 모든 정보가 저절로 공개되지는 않습니다. 엔드포인트에 접근할 수 있게 하는 것과 그 엔드포인트를 HTTP나 JMX에 노출하는 것은 서로 다른 결정입니다.

ACTUATOR · FOUR INDEPENDENT CONDITIONS

HTTP 운영 점검은 네 조건을 모두 만족해야 한다

Boot 4의 작업 access, 웹 노출, 실제 관리 경로, 요청 권한은 별도 조건입니다. 네 조건이 충족돼도 작업 자체가 실패하거나 health가 503을 반환할 수 있습니다.

웹 운영 호출을 위한 네 조건의 논리 관계작업 접근, 웹 노출, 실제 관리 경로, 요청 권한은 별도 조건이며 모두 충족해야 HTTP 작업 호출 후보가 된다. 화살표는 논리 입력 관계이고 요청의 처리 순서가 아니다. 작업 자체의 실패와 Health 503은 별도로 판단한다. 같은 6노드5관계를 모바일에서도 유지한다.작업 access웹 노출관리 경로요청 권한네 조건 모두 충족작업 호출결과는 별도
웹 운영 호출을 위한 네 조건의 논리 관계작업 접근, 웹 노출, 실제 관리 경로, 요청 권한은 별도 조건이며 모두 충족해야 HTTP 작업 호출 후보가 된다. 화살표는 논리 입력 관계이고 요청의 처리 순서가 아니다. 작업 자체의 실패와 Health 503은 별도로 판단한다. 같은 6노드5관계를 모바일에서도 유지한다.작업접근웹노출관리경로요청권한네 조건모두 충족작업 호출결과는 별도

1 · 작업 access

none은 접근 불가, read-only는 읽기 작업, unrestricted는 전체 작업을 허용합니다. 엔드포인트별 설정과 전역 max-permitted 상한을 함께 확인합니다.

2 · 웹 노출

HTTP의 include/exclude 허용 목록을 확인합니다. JMX 노출은 별도 설정입니다. 웹에 이름을 적어도 access가 허용하지 않으면 나타나지 않습니다.

3 · 실제 관리 경로

관리 주소·포트·base path·TLS·프록시 라우팅을 실제 배포에서 확인합니다. 별도 포트도 프로세스와 JVM을 공유합니다.

4 · 요청 주체와 작업

익명 점검·수집기·운영자의 읽기/변경 권한을 구분합니다. 탐색 링크가 보호되거나 숨겨졌다면 알려진 엔드포인트를 직접 점검합니다.

이 번호는 진단 항목이며 요청 처리 순서를 뜻하지 않습니다. 404는 미생성·미노출·경로/포트 등을 다시 좁힙니다. 사용자 엔드포인트는 Clock을 주입하고 집계 DTO만 반환합니다.

그림은 HTTP로 운영 작업을 호출하려면 만족해야 할 네 조건입니다. 작업 접근(access) 설정이 허용해야 하고, 웹 노출 목록에 들어 있어야 하며, 실제 관리 주소와 포트, 경로로 요청이 닿아야 하고, 요청한 주체에게 권한이 있어야 합니다. 화살표는 이 조건들이 함께 충족되어야 한다는 논리 관계이며, 요청이 처리되는 순서를 뜻하지는 않습니다. 네 조건을 모두 충족해도 작업 자체가 실패하거나 health가 503을 돌려줄 수 있으므로, 그 결과는 따로 판단합니다.


Actuator의 구성

spring-boot-starter-actuator를 애플리케이션의 의존성에 추가하면 상태, 정보, 메트릭 같은 엔드포인트 인프라와 Micrometer 통합이 자동 구성됩니다. 엔드포인트마다 필요한 클래스와 빈 조건이 다르므로, 의존성 그래프, 조건 평가 보고서, 엔드포인트 목록을 함께 확인합니다.

src/main/java/board/BoardApplication.java
package board;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BoardApplication {
    public static void main(String[] args) {
        SpringApplication.run(BoardApplication.class, args);
    }
}

Actuator를 쓰려고 시작 코드에 애노테이션을 더할 필요는 없습니다. @SpringBootApplication이 켠 자동 구성이 클래스 경로의 Actuator를 찾아 엔드포인트 인프라를 등록하기 때문입니다. 메인 클래스는 그대로 두고, Boot 4의 Web MVC 스타터와 Actuator 스타터를 의존성에 둡니다. 두 줄 모두 버전을 적지 않았으므로, 버전은 프로젝트의 Boot BOM이 정합니다.

build.gradle 의존성
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-actuator'

애플리케이션이 정상적으로 시작하면 /actuator 탐색 문서에서 지금 웹에 노출된 링크를 볼 수 있습니다. 엔드포인트 빈이 없거나 보안이 거부한 결과를, 경로를 잘못 적어 생긴 404와 구별합니다.


접근 설정과 노출의 구분

Boot 4 계열에서는 management.endpoint.<id>.access에 none, read-only, unrestricted 가운데 하나를 지정해 작업에 대한 접근을 제어합니다. 접근이 none인 엔드포인트는 컨텍스트에서 제거되며, 전역 상한인 management.endpoints.access.max-permitted도 함께 적용됩니다.

노출 설정은 만들어진 엔드포인트를 웹이나 JMX에 연결할지 정합니다. metrics에 접근할 수 있어도 웹 노출 목록에 없으면 HTTP로 읽을 수 없고, 반대로 웹 노출 목록에 이름을 적어도 접근이 막혀 있으면 나타나지 않습니다.

기본 노출은 보수적이어서, 웹에는 기본으로 health 하나만 노출됩니다. *로 모두 열어 두고 네트워크 방화벽에만 기대지 말고, 실제 운영자가 필요로 하는 ID만 허용 목록으로 적습니다. YAML에서 *는 문법상 의미가 있으므로 따옴표로 감싸고, 포함·제외 설정이 어떤 결과를 내는지 시작 테스트로 확인합니다.

Actuator의 노출 규칙에 따라 exclude가 include보다 우선합니다. 예를 들어 include에 health, info, metrics가 있고 exclude에 metrics가 있으면, metrics는 웹 노출 대상에서 빠집니다. 접근 설정이 읽기를 허용하는지, 엔드포인트 빈이 만들어지는지는 이 노출 목록과 별도로 확인합니다.

src/main/resources/application.yml
management:
  endpoints:
    web:
      exposure:
        include: "health,info,metrics"
  endpoint:
    health:
      show-details: never

이 설정은 웹에 health, info, metrics 세 엔드포인트만 노출합니다. show-details: never는 /actuator/health 응답에 전체 상태만 담고, 데이터베이스나 디스크 같은 개별 상태 지표의 세부 내용은 보여 주지 않게 합니다.


엔드포인트 ID와 경로

health, info, metrics, loggers, configprops, env, beans, mappings, threaddump, heapdump, httpexchanges, prometheus, shutdown은 민감도와 비용이 서로 다릅니다. 엔드포인트의 이름만 보고 공개하지 말고, 응답 내용, 값을 바꿀 수 있는지, 페이로드 크기, 호출 빈도를 목록에 기록합니다.

기반 경로를 바꾼다고 보안이 생기지는 않습니다. /internal/manage처럼 추측하기 어려운 경로라도, 인증과 네트워크 정책이 없다면 공개된 엔드포인트입니다. 리버스 프록시가 경로에 접두사를 덧붙인다면, 준비 상태 점검과 대시보드의 대상 주소도 함께 바꿉니다.

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

import java.util.Set;

public record EndpointPolicy(
        Set<String> enabled,
        Set<String> webExposed,
        Set<String> mutating
) {
    public EndpointPolicy {
        enabled = Set.copyOf(enabled);
        webExposed = Set.copyOf(webExposed);
        mutating = Set.copyOf(mutating);
        if (!enabled.containsAll(webExposed)) {
            throw new IllegalArgumentException("exposed endpoint must be enabled");
        }
        if (!webExposed.containsAll(mutating)) {
            throw new IllegalArgumentException("mutating endpoint must be inventoried");
        }
    }

    public Set<String> internalOnly() {
        var result = new java.util.HashSet<>(enabled);
        result.removeAll(webExposed);
        return Set.copyOf(result);
    }
}

이 레코드는 애플리케이션이 정한 엔드포인트 목록이 서로 맞는지 검사합니다. 노출한 엔드포인트는 접근할 수 있어야 하고, 값을 바꾸는 엔드포인트는 노출 목록에 기록되어 있어야 하며, internalOnly()는 접근할 수 있지만 웹에는 노출하지 않는 ID를 돌려줍니다. 실제 Boot 속성이나 보안 필터를 적용하는 코드는 아닙니다. 이 정책과 배포 후 실제 탐색 결과를 비교하면, 속성의 오타나 버전 업그레이드로 엔드포인트가 조용히 늘어난 상황을 잡을 수 있습니다.


관리 서버의 분리

management.server.port를 애플리케이션 포트와 다르게 두면, 별도의 커넥터가 Actuator를 제공합니다. 비공개 인터페이스에 바인딩하고 인그레스, 서비스, 방화벽을 따로 두면 공개 요청 경로에 운영 엔드포인트가 섞이지 않습니다.

다만 프로세스와 JVM은 공유하므로, 애플리케이션이 GC의 일시 정지(stop-the-world pause)에 빠지면 관리 포트도 응답하지 못할 수 있습니다. 별도 포트의 TLS, 접근 로그, 단계적 종료, 포트 충돌도 운영할 대상입니다.

management.server.address=127.0.0.1은 같은 호스트의 사이드카가 수집하는 경우에는 맞지만, 원격에서 점검하는 경우에는 닿지 않을 수 있습니다. 배포 구성에 맞춰 주소를 정하고, 로컬 개발의 기본값을 운영 환경에 그대로 쓰지 않습니다.


사용자 정의 엔드포인트

사용자 정의 엔드포인트는 HTTP와 JMX에서 같은 작업 모델을 제공하고 싶을 때 쓸모 있습니다. 주문 생성 같은 업무 명령은 Actuator에 넣지 않고, 대기열 상태나 캐시 재생성처럼 자주 호출하지 않는 운영 제어 기능을 둡니다. 선택자와 파라미터를 자유 텍스트로 받지 말고 값의 범위를 제한합니다.

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

import java.time.Instant;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;

@Component
@Endpoint(id = "postqueue")
public class PostQueueEndpoint {
    private final AtomicInteger pending = new AtomicInteger();

    @ReadOperation
    public Map<String, Object> state() {
        return Map.of(
                "pending", pending.get(),
                "observedAt", Instant.now().toString());
    }

    public void accepted() {
        pending.incrementAndGet();
    }

    public void completed() {
        pending.updateAndGet(value -> Math.max(0, value - 1));
    }
}

@ReadOperation인 state()는 대기 중인 건수와 조회 시각을 돌려줍니다. 동적인 타임스탬프는 캐시와 테스트를 어렵게 만들 수 있으므로, 실제 코드에서는 Clock을 주입합니다. 응답에는 회원 ID나 대기열 항목의 내용을 넣지 않고 집계 값만 제공합니다.

더 깊이: 읽기 작업의 응답 캐시는 언제 켜질까

Actuator 문서는 파라미터가 없는 읽기 작업의 응답을 엔드포인트가 캐시하고, 그 시간을 management.endpoint.<id>.cache.time-to-live로 정한다고 설명합니다. 그런데 Spring Boot 4.1.2의 CachingOperationInvokerAdvisor를 보면, 이 속성이 있고 값이 0보다 클 때만 작업을 캐시하는 호출기로 감쌉니다. 속성을 정하지 않으면 state() 같은 읽기 작업도 요청마다 실행되고, 예를 들어 management.endpoint.postqueue.cache.time-to-live=10s를 정하면 그때부터 10초 동안 같은 응답을 돌려줍니다.

캐시가 켜져도 모든 요청이 한 응답을 나눠 쓰지는 않습니다. CachingOperationInvoker는 요청한 주체(Principal), API 버전, 그리고 요청이 주 서버와 관리 서버 가운데 어디로 왔는지를 캐시 키로 삼으므로, 다른 사용자에게 다른 사용자의 응답이 나가지 않습니다. 또 실제 인자가 하나라도 넘어온 호출은 캐시를 거치지 않고 바로 실행됩니다. 캐시를 켠 엔드포인트라면 응답에 담은 조회 시각이 실제 요청 시각보다 최대 그 시간만큼 오래되었을 수 있으므로, 앞에서 말한 Clock 주입과 함께 이 시각을 신선도의 표시로 읽게 하면 운영자가 혼동하지 않습니다.


가용성과 인가의 확인

404는 엔드포인트가 없거나, 노출되지 않았거나, 경로가 틀린 경우일 수 있습니다. 401은 인증이 없는 경우, 403은 권한이 부족한 경우, 503은 상태가 서비스 사용 불가로 매핑된 결과일 수 있습니다. 자동화 도구는 이 상태들을 하나의 Actuator 실패로 묶어 처리하지 않습니다.

다음은 노출, 보안, 관리 포트 설정이 서로 다른 경우를 모은 진단 예시이며, 위 YAML 하나로 모두 재현한 결과는 아닙니다.

배포 smoke 결과
GET /actuator -> 200, links=[health,info,metrics]
GET /actuator/env -> 404, not exposed
GET /actuator/health -> 200, status=UP
GET /actuator/loggers -> 403, operator role required
GET application-port/actuator -> 404, management port separated

탐색 문서 자체를 숨기거나 보안으로 보호한다면, 알려진 엔드포인트를 직접 점검합니다. 응답 본문뿐 아니라 콘텐츠 타입, 지연 시간 예산, 인증 경로도 확인합니다.


정리

  • Actuator는 상태, 정보, 메트릭 같은 운영 엔드포인트를 제공하며, 스타터를 추가해도 웹에는 기본으로 health만 노출됩니다.
  • 접근 설정(access)과 웹·JMX 노출은 별개이며, 노출 목록에서는 exclude가 include보다 우선합니다.
  • 관리 포트를 분리하면 공개 경로와 운영 경로를 나눌 수 있지만, JVM은 공유하므로 함께 멈출 수 있습니다.
  • 404, 401, 403, 503은 서로 다른 원인이므로 따로 분류하고, 노출 정책과 실제 탐색 결과를 비교합니다.

연습 문제

1. 관리 포트와 postqueue 엔드포인트

회원 게시판에 별도의 관리 포트를 열고 상태, 정보, 메트릭만 웹에 노출하세요. postqueue 읽기 엔드포인트를 추가하되 항목의 원문은 공개하지 마세요. 엔드포인트가 없는 경우, 노출되지 않은 경우, 인증되지 않은 경우, 정상인 경우라는 네 분기의 상태와 탐색 링크를 표로 검증하세요.

해설 보기

웹 노출 목록은 health, info, metrics 세 ID이므로, postqueue 빈을 추가해도 이 목록에 넣지 않으면 HTTP로는 노출되지 않습니다. 이 연습은 읽기 엔드포인트를 만드는 것과 웹에 공개하는 것을 구분하는 과제이며, 웹에서도 읽는 변형을 시험하려면 허용 목록에 postqueue를 추가한다는 조건을 먼저 적습니다.

설정 파일만 검토하지 말고, 실행 중인 서버의 탐색 결과를 정책의 허용 목록과 비교합니다. 별도 포트가 공개 인그레스로 라우팅되지 않았는지도 확인합니다. 상태 코드를 분류하는 보조 타입은 다음과 같이 둘 수 있습니다.

package board.operations;

public record EndpointObservation(int status, boolean linked, String meaning) {
    public static EndpointObservation classify(int status, boolean linked) {
        return switch (status) {
            case 200 -> new EndpointObservation(200, linked, "AVAILABLE");
            case 401 -> new EndpointObservation(401, linked, "AUTHENTICATION_REQUIRED");
            case 403 -> new EndpointObservation(403, linked, "AUTHORIZATION_DENIED");
            case 404 -> new EndpointObservation(404, linked, "DISABLED_OR_NOT_EXPOSED");
            default -> new EndpointObservation(status, linked, "INVESTIGATE");
        };
    }
}

404의 원인은 시작 시 조건 평가 보고서와 엔드포인트 목록으로 다시 좁힙니다. 이 분류는 상태 코드만 보고 이름을 붙이므로, 404가 접근 차단인지 미노출인지 경로 오류인지는 따로 확인해야 합니다.

2. 대기 건수의 변화

새 PostQueueEndpoint에 accepted()를 두 번 호출한 뒤 completed()를 세 번 호출합니다. 매 호출 뒤의 pending 값을 차례로 적으세요. 이 값만으로 어떤 항목이 완료됐는지나, 영속 대기열이 비었는지 알 수 있나요?

해설 보기

0에서 시작해 1, 2, 1, 0, 0이 됩니다. 마지막 completed()는 값을 0 아래로 내리지 않게 하는 Math.max 때문에 0에 머뭅니다.

이 카운터는 항목의 ID나 실제 처리 이력을 보관하지 않으므로, 어떤 항목이 완료됐는지 알 수 없습니다. 또 프로세스 메모리의 집계가 0이라는 것과 영속 대기열이 비었다는 것은 다른 사실입니다. 운영 집계의 원본을 무엇으로 할지와, 같은 항목이 두 번 완료로 보고될 때의 처리 방법을 따로 정해야 합니다.

3. 노출 정책의 정합성

EndpointPolicy의 enabled가 health, info, metrics, postqueue이고 webExposed가 health, info, metrics, mutating이 빈 집합입니다. internalOnly()는 무엇을 돌려주나요? 별도로 webExposed에 enabled에 없는 env를 더하면 어떻게 되나요?

해설 보기

internalOnly()는 enabled에서 webExposed를 뺀 postqueue 하나를 담은 집합을 돌려줍니다. 접근은 가능하지만 웹에는 노출하지 않는 엔드포인트입니다.

webExposed에 env를 더하면 enabled.containsAll(webExposed)가 거짓이 되어, 생성자가 IllegalArgumentException("exposed endpoint must be enabled")를 던집니다. 이 검사는 애플리케이션이 정한 목록이 서로 맞는지를 볼 뿐이며, 실제 Boot 속성이나 보안 필터가 그렇게 동작하는지는 배포된 서버의 탐색 결과로 따로 확인합니다.

다음 글 상태·정보·로그 제어에서는 health, info, loggers 엔드포인트가 각각 무엇을 보여 주고, 상태 그룹과 로그 레벨 변경을 어떻게 다루는지 살펴봅니다.