- Spring Boot Actuator - 4. Endpoint 커스텀2025년 09월 24일
- 알쓸개잡
- 작성자
- 2025.09.24.:42
Spring Boot Actuator는 운영 환경에서 애플리케이션을 모니터링하고 관리하기 위한 핵심 도구다. 제공되는 health, metrics, info 등의 엔드포인트들은 일반적인 모니터링 요구사항을 충족하지만, 실제 운영 환경에서는 비즈니스 로직에 특화된 더 세밀한 모니터링이 필요한 경우가 있다. 이러한 경우 엔드포인트를 커스텀하여 원하는 정보를 제공하도록 할 수 있다.
이번 포스팅에서는 health 엔드포인트에 대해서 외부 서비스에 대한 커스터마이징에 대해서 정리하고자 한다.
HealthIndicator
Spring Boot Actuator에서 HealthIndicator는 하나의 컴포넌트의 상태를 단순하게 표현하는 인터페이스다. 지원되는 외부 서비스 각각에 대해서 HealthIndicator를 제공한다.
예를 들어 애플리케이션에서 redis를 사용한다고 했을 때 spring boot actuator는 RedisHealthIndicator(RedisReactiveHealthIndicator)를 제공한다.
@ConditionalOnEnabledHealthIndicator( "<name>") 이해하기
위 내용에서 RedisHealthIndicator, MongoHealthIndicator는 RedisHealthContributorAutoConfiguration, MongoHealthContributorAutoConfiguration에 의해서 자동 등록되는데 각 XXXAutoConfiguration 클래스에는 @ConditionalOnEnabledHealthIndicator("redis")와 같이 이름이 정의되어 있다. (mongodb의 경우에는 "mongo")
이는 무엇을 의미하느냐 하면 management.health.<name>.enabled가 true 인 경우 활성화 하겠다는 의미다.
RedisHealthContributorAutoConfiguration 클래스를 보면
@ConditionalOnEnabledHealthIndicator("redis")
어노테이션이 지정되어 있다.
ConditionalOnEnabledHealthIndicator 클래스를 보면 다음과 같이 정의되어 있다.
@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) @Documented @Conditional(OnEnabledHealthIndicatorCondition.class) public @interface ConditionalOnEnabledHealthIndicator { /** * The name of the health indicator. * @return the name of the health indicator */ String value(); }
위 예에서 value 에는 "redis" 값이 전달될 것이다.
@Conditional 역할을 하는 클래스는 OnEnabledHealthIndicatorCondition.class로 지정되어 있다.
OnEnabledHealthIndicatorCondition.class를 따라가보자.
class OnEnabledHealthIndicatorCondition extends OnEndpointElementCondition { OnEnabledHealthIndicatorCondition() { super("management.health.", ConditionalOnEnabledHealthIndicator.class); } }
OnEndpointElementCondition 클래스를 상속받고 있다. OnEndpointElememtCondition 클래스를 따라가 보자.
/** * Base endpoint element condition. An element can be disabled globally through the * {@code defaults} name or individually through the name of the element. * * @author Stephane Nicoll * @author Madhura Bhave * @since 2.0.0 */ public abstract class OnEndpointElementCondition extends SpringBootCondition { private final String prefix; private final Class<? extends Annotation> annotationType; protected OnEndpointElementCondition(String prefix, Class<? extends Annotation> annotationType) { this.prefix = prefix; this.annotationType = annotationType; } @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { AnnotationAttributes annotationAttributes = AnnotationAttributes .fromMap(metadata.getAnnotationAttributes(this.annotationType.getName())); String endpointName = annotationAttributes.getString("value"); ConditionOutcome outcome = getEndpointOutcome(context, endpointName); if (outcome != null) { return outcome; } return getDefaultOutcome(context, annotationAttributes); } protected ConditionOutcome getEndpointOutcome(ConditionContext context, String endpointName) { Environment environment = context.getEnvironment(); String enabledProperty = this.prefix + endpointName + ".enabled"; if (environment.containsProperty(enabledProperty)) { boolean match = environment.getProperty(enabledProperty, Boolean.class, true); return new ConditionOutcome(match, ConditionMessage.forCondition(this.annotationType) .because(this.prefix + endpointName + ".enabled is " + match)); } return null; } /** * Return the default outcome that should be used if property is not set. By default * this method will use the {@code <prefix>.defaults.enabled} property, matching if it * is {@code true} or if it is not configured. * @param context the condition context * @param annotationAttributes the annotation attributes * @return the default outcome * @since 2.6.0 */ protected ConditionOutcome getDefaultOutcome(ConditionContext context, AnnotationAttributes annotationAttributes) { boolean match = Boolean .parseBoolean(context.getEnvironment().getProperty(this.prefix + "defaults.enabled", "true")); return new ConditionOutcome(match, ConditionMessage.forCondition(this.annotationType) .because(this.prefix + "defaults.enabled is considered " + match)); } }
OnEndpointElementCondition 클래스는 SpringBootCondition을 상속받고 있다. 여기서 중요한 메서드는 @Override 어노테이션이 붙은 getMatchOutcome() 메서드다. SpringBootCondition 클래스의 matchs() 메서드 내에서 getMatchOutcome() 메서드가 호출되는데 OnEndpointElementCondition에서 정의한 getMatchOutcome() 메서드에 의해서 Conditional이 결정되는 것이다.
또한 이를 통해 템플릿 메서드 패턴이 적용되었다는 것을 추가로 알 수 있다.
OnEndpointElementCondition 클래스 생성자를 통해서 prefix 정보와 Annotation 클래스가 전달되는데 OnEnabledHealthIndicatorCondition 클래스에서 prefix로 "management.health" 값을 전달하고 Annotation 클래스로는 ConditionalOnEnabledHealthIndicator.class가 전달된다.
OnEndpointElementCondition 클래스의 getMatchOutcome() 메서드는 지정된 Annotation 클래스의 value 속성 값을 가져와서 prefix와 결합하고 ". enabled"를 붙여 최종적으로 체크할 속성을 만든다.
getEndpointOutcome() 메서드에서
String enabledProperty = this.prefix + endpointName + ".enabled"; 이 코드가 체크할 속성을 만드는 코드다.prefix = "management.health"
endpointName = "redis"
이를 통해 최종적으로 management.health.redis.enabled 속성 값을 체크하는 것이다. getEndpointOutcome() 메서드에서 보면 Environment로부터 management.health.redis.enabled 속성을 찾는데 없는 경우 디폴트로 true로 동작한다.
만약 management.health.redis.enabled 속성 자체가 없다면 management.health.defaults.enabled 속성 값을 참조하는데 해당 속성 값이 없는 경우 true로 동작한다. (getDefaultOutcome() 메서드) 결국 management.health.redis.enabled가 지정되지 않으면 기본적으로 true로 동작하여 RedisHealthIndicator는 자동 등록된다.
health 엔드포인트 관련 설정
사용하는 외부 인프라 서비스에 대한 의존성에 따라 HealthIndicator가 적용되는 설정은 다음과 같다.
management: health: defaults: enabled: true|false <name>: # 서비스 이름 (ex. redis, mongo) enabled: true|false
management.health.defaults.enabled 설정은 HealthIndicator 활성화 옵션에 대한 전역 설정이다. (기본값: true)
management.health.defaults.enabled 가 false라면 management.health.<name>.enabled 설정을 true로 지정을 해줘야 해당 서비스에 대한 HealthIndicator가 활성화된다.
management.health.default.enabled가 true라면 기본적으로 외부 인프라 서비스 의존성이 있으면 해당 서비스에 대한 HealthIndicator는 자동으로 활성화된다.
HealthIndicator 적용 과정
Spring Boot AutoConfiguration에 의해서 HealthIndicator가 적용되는 흐름은 다음과 같다.
1. 애플리케이션 시작 -> 자동 설정 기능 동작
2. 자동 설정 로드 -> Health 관련 자동 설정이 클래스패스에 감지되고 활성화
3. 빈 검색 및 수집 -> 애플리케이션 콘텍스트에서 HealthIndicator 인터페이스를 구현한 모든 빈을 자동으로 찾아서 수집
4. 이름 매핑 및 등록 -> 각 HealthIndicator에 대해서 고유한 이름을 생성하고 중앙 레지스트리에 등록
5. 엔드포인트 구성 -> Health 엔드포인트가 생성되고 등록된 모든 HealthIndicator들과 연결
6. 요청 처리 -> /actuator/health 요청이 들어오면 (base-path가 /actuator라고 했을 때) 등록된 모든 HealthIndicator를 순차적으로 실행
7. 결과집계 및 응답 -> 각 HealthIndicator의 실행 결과를 수집하여 전체 애플리케이션의 건강 상태를 판단하고 JSON 형태로 응답
위 과정으로 인해서 개발자는 HealthIndicator를 구현한 빈을 생성하여 커스텀한 HealthIndicator를 동작시킬 수 있다.
RedisHealthIndicator 커스텀
Redis를 사용하는 애플리케이션에 대해서 health 엔드포인트에 대한 응답 정보를 커스텀해보자.
우선 AutoConfiguration에 의해서 생성되는 RedisHealthIndicator 클래스를 살펴보자.
public class RedisHealthIndicator extends AbstractHealthIndicator { private final RedisConnectionFactory redisConnectionFactory; public RedisHealthIndicator(RedisConnectionFactory connectionFactory) { super("Redis health check failed"); Assert.notNull(connectionFactory, "'connectionFactory' must not be null"); this.redisConnectionFactory = connectionFactory; } @Override protected void doHealthCheck(Health.Builder builder) throws Exception { RedisConnection connection = RedisConnectionUtils.getConnection(this.redisConnectionFactory); try { doHealthCheck(builder, connection); } finally { RedisConnectionUtils.releaseConnection(connection, this.redisConnectionFactory); } } private void doHealthCheck(Health.Builder builder, RedisConnection connection) { if (connection instanceof RedisClusterConnection clusterConnection) { RedisHealth.fromClusterInfo(builder, clusterConnection.clusterGetClusterInfo()); } else { RedisHealth.up(builder, connection.serverCommands().info()); } } }
RedisHealthIndicator 클래스는 AbstractHealthIndicator를 상속한다.
@Override가 지정된 doHealthCheck() 메서드가 주요 역할을 하는 것으로 보인다. doHealthCheck() 에는 health 정보를 세팅한 Health.Builder를 세팅하도록 하면 된다.
실제로 HealthIndicator 인터페이스를 구현한 AbstractHealthIndicator 클래스의 health() 메서드를 보면 내부적으로 doHealthCheck() 메서드를 호출한다. 즉, 각 서비스의 health에 대한 처리는 doHealthCheck() 메서드에서 오버라이딩 처리 하게 된다. 이 역시 템플릿 메서드 패턴을 사용하였다. 가만히 보면 템플릿 메서드 패턴이 참 많이 쓰이는 것 같다.
우선 RedisHealthIndicator는 RedisConnection을 통해서 연결이 문제없는지 우선 확인을 한다. 만약 연결 과정에서 예외가 발생하면 health() 메서드 내에서 예외를 잡아 DOWN으로 처리를 한다.
애플리케이션이 사용하는 Redis가 클러스터 구성의 경우에는 fromClusterInfo를 통해서 다음과 같은 정보를 세팅한다.
- cluster_size
- slots_up
- slots_fail
- version (redis version)
- cluster_state 정보를 통해서 down, up을 결정한다.
클러스터 구성이 아닌 경우에는
- version (redis version)
다음은 위 내용을 설명하는 클래스다.
final class RedisHealth { private RedisHealth() { } static Builder up(Health.Builder builder, Properties info) { builder.withDetail("version", info.getProperty("redis_version")); return builder.up(); } static Builder fromClusterInfo(Health.Builder builder, ClusterInfo clusterInfo) { builder.withDetail("cluster_size", clusterInfo.getClusterSize()); builder.withDetail("slots_up", clusterInfo.getSlotsOk()); builder.withDetail("slots_fail", clusterInfo.getSlotsFail()); if ("fail".equalsIgnoreCase(clusterInfo.getState())) { return builder.down(); } else { return builder.up(); } } }
CustomRedisHealthIndicator
이제 AbstractHealthIndicator를 직접 확장하여 커스텀한 RedisHealthIndicator를 만들어보자.
@Component public class CustomRedisHealthIndicator extends AbstractHealthIndicator { private final StringRedisTemplate redisTemplate; public CustomRedisHealthIndicator( StringRedisTemplate redisTemplate ) { this.redisTemplate = redisTemplate; } @Override protected void doHealthCheck( Health.Builder builder ) throws Exception { String pong = redisTemplate.getConnectionFactory() .getConnection().ping(); Properties info = redisTemplate.getConnectionFactory().getConnection().serverCommands().info(); String key = "health:probe"; String val = Long.toString( System.currentTimeMillis() ); redisTemplate.opsForValue().set( key, val, Duration.ofSeconds( 2 ) ); String read = redisTemplate.opsForValue().get( key ); builder.up() .withDetail( "ping", pong ) .withDetail( "roundTrip2Sec", ( read != null ) ) .withDetail( "version", info.getProperty( "redis_version") ); } }
자동 구성에 의해서 생성된 혹은 직접 생성한 StringRedisTemplate을 주입받아 ping 명령, 2초 이내로 응답을 잘 받고 있는지 체크하여 응답에 포함시키는 코드다.
@Component에 별도로 빈 이름을 지정하지 않으면 클래스 이름에서 "HealthIndicator"를 제외한 이름이 컴포넌트 이름이 된다. 위 예에서는 customRedis가 컴포넌트 이름이 된다. 만약 클래스 이름에 "HealthIndicator"가 없다면 그냥 클래스 이름이 컴포넌트 이름이 된다.
만약 @Component("redis")와 같이 별도 이름을 지정하면 해당 이름이 컴포넌트 이름이 된다.
적용을 위해서 우선 자동 구성이 생성하는 RedisHealthIndicator를 비활성화해줘야 한다.
management: health: redis: enabled: false
애플리케이션을 실행 후 health 엔드포인트를 호출해 보자. (상세 내용이 출력되도록 설정했다고 가정하자)
{ "status": "UP", "components": { "customRedis": { "status": "UP", "details": { "ping": "PONG", "roundTrip2Sec": true, "version": "7.2.4" } }, ... ... } }
위와 같이 Redis에 대한 health indicator가 출력됨을 확인할 수 있다.
이와 같은 형태로 각 서비스에 대한 HealthIndicator를 커스텀하게 적용할 수 있다.
env 엔드포인트의 경우 SanitizingFucntion을 이용하여 노출되는 값에 대한 커스텀을 적용할 수 있다.
metrics 엔드포인트의 경우 커스텀한 metric 항목을 등록할 수 있다.
자세한 내용은
2025.09.21 - [스프링부트] - Spring Boot Actuator - 2. 주요 Endpoint를 참고하기 바란다.
끝.
'스프링부트' 카테고리의 다른 글
Spring Boot Actuator - 5. 사용자 정의 Endpoint 만들기 (0) 2025.09.27 Spring Boot Actuator - 3. Actuator 보안 (0) 2025.09.23 Spring Boot Actuator - 2. 주요 Endpoint (0) 2025.09.21 Spring Boot Actuator - 1. 시작하기 (0) 2025.09.17 Spring Boot JSON 처리를 위한 자동 구성 및 설정 (0) 2025.01.04 다음글이전글이전 글이 없습니다.댓글