Spring Boot Actuator: 사용자 정의 엔드포인트 만들기 🚀

애플리케이션 모니터링과 관리를 위한 맞춤형 엔드포인트 구축하기
🌟 들어가며: Spring Boot Actuator가 뭐길래?
안녕! 오늘은 Spring Boot의 숨은 보석 같은 기능인 Actuator에 대해 알아보고, 특히 우리만의 커스텀 엔드포인트를 만드는 방법을 함께 살펴볼 거야. 😊
혹시 너의 애플리케이션이 지금 어떻게 돌아가고 있는지 궁금했던 적 없어? 메모리는 얼마나 사용하고 있지? 어떤 빈들이 등록되어 있지? 헬스 체크는 어떻게 하지? 이런 고민들, Spring Boot Actuator가 다 해결해 줄 수 있어!
2025년 현재, 마이크로서비스 아키텍처와 클라우드 네이티브 애플리케이션이 대세인 시대에 애플리케이션 모니터링과 관리는 더욱 중요해졌어. 특히 재능넷과 같은 다양한 서비스를 제공하는 플랫폼에서는 시스템 상태를 실시간으로 모니터링하는 것이 필수지!
오늘 배울 내용은 단순히 Spring Boot Actuator를 사용하는 것을 넘어서, 우리만의 특별한 엔드포인트를 만들어 애플리케이션을 더 효과적으로 관리하는 방법이야. 이 기술을 익히면 너의 개발 역량이 한층 업그레이드될 거야! 🚀
📚 Spring Boot Actuator 기본 개념
Actuator는 영어로 '작동 장치' 또는 '구동 장치'라는 의미를 가지고 있어. Spring Boot Actuator는 말 그대로 애플리케이션을 '작동'시키고 '관리'하는 도구라고 생각하면 돼. 🔧
Spring Boot Actuator는 애플리케이션의 운영 정보를 HTTP 엔드포인트나 JMX를 통해 노출시켜주는 기능을 제공해. 기본적으로 /actuator 경로 아래에 다양한 엔드포인트가 제공되지.
주요 기본 엔드포인트들을 살펴볼까?
- /actuator/health: 애플리케이션의 건강 상태를 확인할 수 있어
- /actuator/info: 애플리케이션의 정보를 제공해
- /actuator/metrics: 다양한 메트릭 정보를 볼 수 있어
- /actuator/env: 환경 설정 정보를 보여줘
- /actuator/beans: 등록된 모든 빈 목록을 확인할 수 있어
- /actuator/mappings: 모든 @RequestMapping 경로를 보여줘
- /actuator/threaddump: 스레드 덤프를 제공해
- /actuator/heapdump: 힙 덤프를 다운로드할 수 있어
- /actuator/loggers: 로깅 설정을 조회하고 변경할 수 있어
이런 기본 엔드포인트만으로도 정말 많은 정보를 얻을 수 있지만, 때로는 우리 애플리케이션에 특화된 정보가 필요할 때가 있어. 그럴 때 바로 사용자 정의 엔드포인트가 필요한 거지! 😎
🛠️ Spring Boot Actuator 설정하기
자, 이제 본격적으로 Actuator를 설정해보자! 먼저 의존성부터 추가해야 해.
1. Maven 의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2. Gradle 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-actuator'
의존성을 추가하면 기본적으로 몇 가지 엔드포인트만 활성화돼. 더 많은 엔드포인트를 활성화하려면 application.properties나 application.yml 파일에 설정을 추가해야 해.
3. application.properties 설정
# 모든 엔드포인트 활성화
management.endpoints.web.exposure.include=*
# 특정 엔드포인트만 활성화
# management.endpoints.web.exposure.include=health,info,metrics
# 특정 엔드포인트 비활성화
# management.endpoints.web.exposure.exclude=env,beans
# 엔드포인트 기본 경로 변경 (기본값은 /actuator)
# management.endpoints.web.base-path=/manage
# 상세한 헬스 정보 표시
management.endpoint.health.show-details=always
4. application.yml 설정
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
주의사항: 프로덕션 환경에서는 모든 엔드포인트를 노출하는 것은 보안 위험이 있어. 필요한 엔드포인트만 선택적으로 노출하고, 적절한 보안 설정을 추가하는 것이 좋아! 🔒
🔐 Actuator 보안 설정하기
Actuator 엔드포인트는 민감한 정보를 포함하고 있어서 보안 설정이 필수야. Spring Security를 사용해서 Actuator 엔드포인트에 접근 제어를 추가해보자.
1. Spring Security 의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. 보안 설정 클래스 작성
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(requests -> requests
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR_ADMIN")
.anyRequest().authenticated()
)
.httpBasic();
return http.build();
}
}
위 설정은 health와 info 엔드포인트는 모든 사용자에게 허용하고, 나머지 엔드포인트는 ACTUATOR_ADMIN 역할을 가진 사용자만 접근할 수 있도록 제한하는 설정이야.
추가로 사용자 정보도 설정해줘야 해:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public InMemoryUserDetailsManager userDetailsManager() {
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("adminPassword"))
.roles("ACTUATOR_ADMIN")
.build();
return new InMemoryUserDetailsManager(admin);
}
}
이제 Actuator 엔드포인트에 접근할 때 인증이 필요하게 됐어. 물론 실제 프로덕션 환경에서는 인메모리 사용자 대신 데이터베이스나 LDAP 같은 외부 인증 시스템을 연동하는 것이 좋겠지! 🔑
🎯 사용자 정의 엔드포인트 만들기
이제 본격적으로 우리만의 커스텀 엔드포인트를 만들어볼 차례야! Spring Boot 2.0부터는 @Endpoint 어노테이션을 사용해서 사용자 정의 엔드포인트를 쉽게 만들 수 있어.
사용자 정의 엔드포인트를 만드는 방법은 크게 두 가지가 있어:
- @Endpoint: JMX와 HTTP를 모두 지원하는 기술 중립적인 엔드포인트
- @WebEndpoint: HTTP만 지원하는 웹 특화 엔드포인트
우선 간단한 사용자 정의 엔드포인트를 만들어보자! 이 엔드포인트는 애플리케이션의 커스텀 상태 정보를 제공할 거야.
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
@Endpoint(id = "app-status")
public class AppStatusEndpoint {
@ReadOperation
public Map<String, Object> status() {
Map<String, Object> details = new HashMap<>();
details.put("name", "My Awesome Application");
details.put("version", "1.0.0");
details.put("status", "UP and RUNNING");
details.put("startTime", System.currentTimeMillis());
details.put("activeUsers", 42);
details.put("pendingTasks", 7);
return details;
}
}
위 코드에서 @Endpoint(id = "app-status")는 엔드포인트의 ID를 지정해. 이 ID는 URL 경로의 일부가 되어 /actuator/app-status로 접근할 수 있게 돼.
@ReadOperation 어노테이션은 HTTP GET 요청에 매핑되는 메서드를 표시해. 이외에도 @WriteOperation(POST), @DeleteOperation(DELETE) 어노테이션을 사용할 수 있어.
이제 애플리케이션을 실행하고 http://localhost:8080/actuator/app-status 경로로 접속하면 우리가 정의한 상태 정보를 JSON 형식으로 볼 수 있어! 🎉
🔄 동적 데이터를 제공하는 엔드포인트
정적인 데이터만 제공하는 것은 별로 흥미롭지 않지? 이번에는 실시간으로 변하는 동적 데이터를 제공하는 엔드포인트를 만들어보자.
예를 들어, 애플리케이션의 현재 메모리 사용량, 활성 사용자 수, 캐시 상태 등을 모니터링하는 엔드포인트를 만들 수 있어. 이런 정보는 재능넷과 같은 대규모 서비스 플랫폼에서 시스템 상태를 실시간으로 파악하는 데 매우 유용해! 📊
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@Component
@Endpoint(id = "system-metrics")
public class SystemMetricsEndpoint {
private final AtomicInteger activeUsers = new AtomicInteger(0);
private final AtomicInteger totalRequests = new AtomicInteger(0);
// 활성 사용자 수 증가 메서드 (로그인 시 호출)
public void incrementActiveUsers() {
activeUsers.incrementAndGet();
}
// 활성 사용자 수 감소 메서드 (로그아웃 시 호출)
public void decrementActiveUsers() {
activeUsers.decrementAndGet();
}
// 요청 수 증가 메서드 (요청 인터셉터에서 호출)
public void incrementRequests() {
totalRequests.incrementAndGet();
}
@ReadOperation
public Map<String, Object> getMetrics() {
Map<String, Object> metrics = new HashMap<>();
// JVM 메모리 정보 수집
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapMemory = memoryBean.getHeapMemoryUsage();
MemoryUsage nonHeapMemory = memoryBean.getNonHeapMemoryUsage();
// 메모리 정보
Map<String, Object> memory = new HashMap<>();
memory.put("heapUsed", bytesToMB(heapMemory.getUsed()));
memory.put("heapMax", bytesToMB(heapMemory.getMax()));
memory.put("heapUsagePercentage", calculatePercentage(heapMemory.getUsed(), heapMemory.getMax()));
memory.put("nonHeapUsed", bytesToMB(nonHeapMemory.getUsed()));
// 시스템 정보
Map<String, Object> system = new HashMap<>();
system.put("processors", Runtime.getRuntime().availableProcessors());
system.put("uptime", ManagementFactory.getRuntimeMXBean().getUptime() / 1000 + " seconds");
// 애플리케이션 정보
Map<String, Object> application = new HashMap<>();
application.put("activeUsers", activeUsers.get());
application.put("totalRequests", totalRequests.get());
application.put("startTime", ManagementFactory.getRuntimeMXBean().getStartTime());
metrics.put("memory", memory);
metrics.put("system", system);
metrics.put("application", application);
metrics.put("timestamp", System.currentTimeMillis());
return metrics;
}
private double bytesToMB(long bytes) {
return bytes / (1024.0 * 1024.0);
}
private double calculatePercentage(long used, long max) {
if (max == -1) return 0; // max가 undefined인 경우
return ((double) used / max) * 100;
}
}
이 엔드포인트는 JVM 메모리 사용량, 시스템 정보, 애플리케이션 정보 등 다양한 메트릭을 제공해. 실제 애플리케이션에서는 incrementActiveUsers(), decrementActiveUsers(), incrementRequests() 메서드를 적절한 곳에서 호출해야 해.
예를 들어, 로그인/로그아웃 서비스나 인터셉터에서 이 메서드들을 호출하면 실시간으로 활성 사용자 수와 요청 수를 추적할 수 있어! 👥
📝 쓰기 작업을 지원하는 엔드포인트
지금까지는 데이터를 읽어오는 엔드포인트만 만들었어. 이번에는 @WriteOperation을 사용해서 데이터를 변경할 수 있는 엔드포인트를 만들어보자!
예를 들어, 애플리케이션의 로그 레벨을 동적으로 변경하거나 캐시를 초기화하는 기능을 제공할 수 있어.
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Endpoint(id = "features")
public class FeaturesEndpoint {
private final Map<String, FeatureDetails> features = new ConcurrentHashMap<>();
public FeaturesEndpoint() {
// 초기 기능 설정
features.put("user-registration", new FeatureDetails(true, "사용자 등록 기능"));
features.put("payment-processing", new FeatureDetails(true, "결제 처리 기능"));
features.put("recommendation-engine", new FeatureDetails(false, "추천 엔진 기능"));
features.put("dark-mode", new FeatureDetails(false, "다크 모드 UI"));
}
@ReadOperation
public Map<String, FeatureDetails> features() {
return features;
}
@ReadOperation
public FeatureDetails feature(@Selector String name) {
return features.get(name);
}
@WriteOperation
public Map<String, FeatureDetails> configureFeature(@Selector String name, boolean enabled) {
FeatureDetails feature = features.get(name);
if (feature != null) {
features.put(name, new FeatureDetails(enabled, feature.getDescription()));
}
return features;
}
public static class FeatureDetails {
private boolean enabled;
private String description;
public FeatureDetails(boolean enabled, String description) {
this.enabled = enabled;
this.description = description;
}
public boolean isEnabled() {
return enabled;
}
public String getDescription() {
return description;
}
}
}
이 엔드포인트는 애플리케이션의 다양한 기능(feature)을 활성화하거나 비활성화할 수 있는 기능을 제공해. 이런 기능은 Feature Flag 또는 Feature Toggle이라고 불리며, 배포 없이 기능을 켜고 끌 수 있어서 매우 유용해! 🔄
사용 방법은 다음과 같아:
- 모든 기능 조회: GET /actuator/features
- 특정 기능 조회: GET /actuator/features/{name}
- 기능 활성화/비활성화: POST /actuator/features/{name}?enabled=true|false
이렇게 만든 엔드포인트를 실제 애플리케이션 코드에서 활용하는 방법을 살펴볼까?
@Service
public class UserService {
private final FeaturesEndpoint featuresEndpoint;
public UserService(FeaturesEndpoint featuresEndpoint) {
this.featuresEndpoint = featuresEndpoint;
}
public void registerUser(UserDto userDto) {
// 기능이 활성화되어 있는지 확인
if (featuresEndpoint.feature("user-registration").isEnabled()) {
// 사용자 등록 로직 실행
// ...
} else {
throw new FeatureDisabledException("사용자 등록 기능이 비활성화되어 있습니다.");
}
}
}
이런 방식으로 애플리케이션의 기능을 동적으로 제어할 수 있어. 예를 들어, 시스템 부하가 높을 때 일시적으로 추천 엔진을 비활성화하거나, 새로운 기능을 점진적으로 활성화하는 등의 작업을 할 수 있지! 🎛️
🔍 파라미터를 받는 엔드포인트
이번에는 파라미터를 받아 처리하는 엔드포인트를 만들어보자. @Selector 어노테이션을 사용하면 URL 경로의 일부를 파라미터로 받을 수 있고, 메서드 파라미터에 직접 어노테이션을 추가하면 쿼리 파라미터를 받을 수 있어.
아래 예제는 애플리케이션의 캐시를 관리하는 엔드포인트야:
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
@Component
@Endpoint(id = "caches")
public class CacheEndpoint {
private final CacheManager cacheManager;
public CacheEndpoint(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@ReadOperation
public Map<String, Object> caches() {
// 모든 캐시의 정보를 반환
Map<String, Object> result = new HashMap<>();
Map<String, Integer> cacheSizes = cacheManager.getCacheNames().stream()
.collect(Collectors.toMap(
cacheName -> cacheName,
cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
return estimateCacheSize(cache);
}
));
result.put("cacheNames", cacheManager.getCacheNames());
result.put("cacheSizes", cacheSizes);
return result;
}
@ReadOperation
public Map<String, Object> cache(@Selector String cacheName) {
// 특정 캐시의 상세 정보를 반환
Map<String, Object> result = new HashMap<>();
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
result.put("name", cacheName);
result.put("size", estimateCacheSize(cache));
result.put("status", "active");
} else {
result.put("error", "Cache not found: " + cacheName);
}
return result;
}
@DeleteOperation
public Map<String, Object> clearCache(@Selector String cacheName) {
// 특정 캐시를 초기화
Map<String, Object> result = new HashMap<>();
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
cache.clear();
result.put("status", "Cache '" + cacheName + "' cleared successfully");
} else {
result.put("error", "Cache not found: " + cacheName);
}
return result;
}
@DeleteOperation
public Map<String, Object> clearAllCaches() {
// 모든 캐시를 초기화
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
cache.clear();
}
});
Map<String, Object> result = new HashMap<>();
result.put("status", "All caches cleared successfully");
return result;
}
// 캐시 크기를 추정하는 메서드 (실제 구현은 캐시 종류에 따라 다를 수 있음)
private int estimateCacheSize(Cache cache) {
// 이 부분은 실제 캐시 구현체에 따라 다르게 구현해야 함
// 여기서는 예시로 랜덤 값을 반환
return (int) (Math.random() * 1000);
}
}
이 엔드포인트는 다음과 같은 기능을 제공해:
- 모든 캐시 정보 조회: GET /actuator/caches
- 특정 캐시 정보 조회: GET /actuator/caches/{cacheName}
- 특정 캐시 초기화: DELETE /actuator/caches/{cacheName}
- 모든 캐시 초기화: DELETE /actuator/caches
캐시 관리는 성능 최적화에 매우 중요한 부분이야. 특히 재능넷과 같이 많은 사용자가 이용하는 플랫폼에서는 캐시를 효율적으로 관리하는 것이 서비스 응답 시간을 크게 개선할 수 있어! 🚀
📊 메트릭 수집 엔드포인트
Spring Boot Actuator는 기본적으로 다양한 메트릭을 제공하지만, 우리 애플리케이션에 특화된 메트릭을 수집하고 싶을 때가 있어. 이번에는 Micrometer를 활용해서 커스텀 메트릭을 수집하는 방법을 알아보자!
Micrometer는 Spring Boot 2.0부터 기본으로 포함된 메트릭 수집 라이브러리야. 다양한 모니터링 시스템(Prometheus, Graphite, DataDog 등)과 연동할 수 있어서 매우 유용해.
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class OrderService {
private final Counter orderCounter;
private final Counter failedOrderCounter;
private final Timer orderProcessingTimer;
public OrderService(MeterRegistry registry) {
// 주문 수를 카운팅하는 메트릭
this.orderCounter = Counter.builder("app.orders.total")
.description("주문 총 개수")
.register(registry);
// 실패한 주문 수를 카운팅하는 메트릭
this.failedOrderCounter = Counter.builder("app.orders.failed")
.description("실패한 주문 개수")
.register(registry);
// 주문 처리 시간을 측정하는 메트릭
this.orderProcessingTimer = Timer.builder("app.orders.processing.time")
.description("주문 처리 시간")
.register(registry);
}
public void processOrder(Order order) {
// 타이머로 주문 처리 시간 측정
orderProcessingTimer.record(() -> {
try {
// 주문 처리 로직
// ...
// 성공한 주문 카운트 증가
orderCounter.increment();
} catch (Exception e) {
// 실패한 주문 카운트 증가
failedOrderCounter.increment();
throw e;
}
});
}
// 수동으로 시간 측정이 필요한 경우
public void processComplexOrder(Order order) {
Timer.Sample sample = Timer.start();
try {
// 복잡한 주문 처리 로직
// ...
// 성공한 주문 카운트 증가
orderCounter.increment();
} catch (Exception e) {
// 실패한 주문 카운트 증가
failedOrderCounter.increment();
throw e;
} finally {
// 처리 시간 기록
sample.stop(orderProcessingTimer);
}
}
}
위 코드에서는 Counter와 Timer라는 두 가지 타입의 메트릭을 사용했어:
- Counter: 증가만 가능한 단순 카운터. 주문 수, 에러 수 등을 카운팅할 때 사용
- Timer: 특정 작업의 수행 시간을 측정. 주문 처리 시간, API 응답 시간 등을 측정할 때 사용
이외에도 Gauge(현재 값을 측정), DistributionSummary(값의 분포를 측정) 등 다양한 메트릭 타입을 사용할 수 있어.
이렇게 수집된 메트릭은 /actuator/metrics 엔드포인트를 통해 확인할 수 있어:
- 모든 메트릭 목록: GET /actuator/metrics
- 특정 메트릭 상세 정보: GET /actuator/metrics/{metric.name}
예를 들어, GET /actuator/metrics/app.orders.total을 호출하면 주문 총 개수에 대한 메트릭 정보를 볼 수 있어! 📈
🔧 복합 엔드포인트 만들기
지금까지는 단일 기능을 제공하는 엔드포인트를 만들었어. 이번에는 여러 작업을 지원하는 복합 엔드포인트를 만들어보자!
예를 들어, 애플리케이션의 다양한 관리 작업을 수행할 수 있는 엔드포인트를 만들 수 있어:
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
@Endpoint(id = "app-admin")
public class AppAdminEndpoint {
private final CacheService cacheService;
private final ConfigService configService;
private final MaintenanceService maintenanceService;
public AppAdminEndpoint(
CacheService cacheService,
ConfigService configService,
MaintenanceService maintenanceService
) {
this.cacheService = cacheService;
this.configService = configService;
this.maintenanceService = maintenanceService;
}
@ReadOperation
public Map<String, Object> info() {
Map<String, Object> info = new HashMap<>();
info.put("caches", cacheService.getCacheInfo());
info.put("configs", configService.getCurrentConfigs());
info.put("maintenanceMode", maintenanceService.isInMaintenanceMode());
info.put("availableOperations", new String[] {
"GET /app-admin",
"GET /app-admin/cache/{cacheName}",
"DELETE /app-admin/cache/{cacheName}",
"GET /app-admin/config/{configKey}",
"POST /app-admin/config/{configKey}",
"POST /app-admin/maintenance?enabled=true|false"
});
return info;
}
// 캐시 관련 작업
@ReadOperation
public Map<String, Object> getCache(@Selector String cacheName) {
return cacheService.getCacheDetails(cacheName);
}
@DeleteOperation
public Map<String, Object> clearCache(@Selector String cacheName) {
cacheService.clearCache(cacheName);
Map<String, Object> result = new HashMap<>();
result.put("status", "Cache '" + cacheName + "' cleared");
return result;
}
// 설정 관련 작업
@ReadOperation
public Map<String, Object> getConfig(@Selector String configKey) {
Map<String, Object> result = new HashMap<>();
result.put(configKey, configService.getConfig(configKey));
return result;
}
@WriteOperation
public Map<String, Object> updateConfig(@Selector String configKey, String value) {
configService.updateConfig(configKey, value);
Map<String, Object> result = new HashMap<>();
result.put("status", "Config '" + configKey + "' updated to '" + value + "'");
return result;
}
// 유지보수 모드 관련 작업
@WriteOperation
public Map<String, Object> setMaintenanceMode(boolean enabled) {
if (enabled) {
maintenanceService.enableMaintenanceMode();
} else {
maintenanceService.disableMaintenanceMode();
}
Map<String, Object> result = new HashMap<>();
result.put("maintenanceMode", maintenanceService.isInMaintenanceMode());
return result;
}
}
위 코드에서는 @Selector 어노테이션을 사용해서 URL 경로의 일부를 파라미터로 받고 있어. 또한 다양한 서비스를 주입받아 여러 관리 작업을 수행할 수 있도록 했지.
이런 복합 엔드포인트는 관련된 작업들을 논리적으로 그룹화하여 관리하기 편리해. 예를 들어, 애플리케이션 관리자는 하나의 엔드포인트를 통해 캐시 관리, 설정 변경, 유지보수 모드 전환 등 다양한 작업을 수행할 수 있어! 🛠️
🔐 엔드포인트 보안 강화하기
사용자 정의 엔드포인트는 매우 강력한 기능을 제공하기 때문에 보안에 특별히 신경써야 해. 이번에는 엔드포인트 보안을 강화하는 방법을 알아보자!
1. 세분화된 접근 제어
Spring Security를 사용하여 엔드포인트별로 다른 권한을 설정할 수 있어:
@Configuration
@EnableWebSecurity
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(requests -> requests
// 기본 엔드포인트 설정
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
// 읽기 전용 엔드포인트 설정
.requestMatchers(EndpointRequest.to("metrics", "env")).hasRole("ACTUATOR_READ")
// 쓰기 가능 엔드포인트 설정
.requestMatchers(EndpointRequest.to("caches", "features", "app-admin")).hasRole("ACTUATOR_ADMIN")
// 민감한 엔드포인트 설정
.requestMatchers(EndpointRequest.to("shutdown")).hasRole("SUPER_ADMIN")
.anyRequest().authenticated()
)
.httpBasic();
return http.build();
}
}
2. 요청 제한 설정
특정 IP에서만 접근 가능하도록 설정할 수 있어:
@Configuration
public class ActuatorIpRestrictionConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new IpAddressInterceptor())
.addPathPatterns("/actuator/**");
}
};
}
public static class IpAddressInterceptor implements HandlerInterceptor {
private static final List<String> ALLOWED_IPS = Arrays.asList(
"127.0.0.1", "192.168.1.100", "10.0.0.5"
);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String clientIp = request.getRemoteAddr();
if (!ALLOWED_IPS.contains(clientIp)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("Access denied: Your IP is not allowed to access Actuator endpoints");
return false;
}
return true;
}
}
}
3. 감사 로깅 추가
모든 Actuator 엔드포인트 접근을 로깅하는 기능을 추가할 수 있어:
@Component
public class ActuatorAuditLogger implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(ActuatorAuditLogger.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getRequestURI().startsWith("/actuator")) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth != null ? auth.getName() : "anonymous";
logger.info("Actuator access: {} {} by user '{}' from IP {}",
request.getMethod(),
request.getRequestURI(),
username,
request.getRemoteAddr());
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if (request.getRequestURI().startsWith("/actuator")) {
logger.info("Actuator response: {} {} - status {}",
request.getMethod(),
request.getRequestURI(),
response.getStatus());
}
}
}
@Configuration
public class ActuatorAuditConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ActuatorAuditLogger())
.addPathPatterns("/actuator/**");
}
}
이런 보안 설정은 재능넷과 같은 서비스에서 특히 중요해. 관리 엔드포인트를 통해 시스템 설정을 변경하거나 민감한 정보를 조회할 수 있기 때문에, 적절한 인증과 권한 검사는 필수야! 🔒
🔄 Spring Boot 2.x vs 3.x 차이점
Spring Boot 버전에 따라 Actuator 사용법에 약간의 차이가 있어. 2025년 현재 Spring Boot 3.x가 널리 사용되고 있지만, 아직 2.x 버전을 사용하는 프로젝트도 많아. 두 버전의 주요 차이점을 알아보자!
주요 변경 사항
- Java 버전 요구사항: Spring Boot 3.x는 Java 17 이상이 필요해
- Observability: 3.x에서는 Micrometer Tracing을 통한 향상된 관찰성 API를 제공해
- 보안 설정: Spring Security 6.x에서는 람다 기반 설정이 권장돼
- 가상 스레드: Java 21 이상에서는 가상 스레드를 지원해 성능이 크게 향상돼
Spring Boot 3.x에서의 Actuator 설정 예시
@Configuration
@EnableWebSecurity
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(requests -> requests
.requestMatchers("/actuator/health/**", "/actuator/info/**").permitAll()
.requestMatchers("/actuator/**").hasRole("ACTUATOR_ADMIN")
.anyRequest().authenticated()
)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/actuator/**")
)
.httpBasic(withDefaults());
return http.build();
}
}
Spring Boot 3.x로 업그레이드하면 성능과 보안이 향상되고, 최신 Java 기능을 활용할 수 있어. 특히 가상 스레드를 사용하면 높은 동시성을 처리할 때 성능이 크게 향상돼. 이는 재능넷과 같이 많은 사용자가 동시에 접속하는 서비스에 큰 이점이 될 수 있어! 🚀
📱 Actuator UI 연동하기
Actuator는 기본적으로 JSON 형식의 데이터를 제공하지만, 이를 시각적으로 보기 좋게 표현하는 UI 도구들도 있어. 대표적인 도구로는 Spring Boot Admin이 있어!
Spring Boot Admin 설정하기
Spring Boot Admin은 Actuator 엔드포인트를 시각화해주는 웹 애플리케이션이야. 설정 방법을 알아보자!
1. Admin 서버 설정
// Maven 의존성
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>3.1.6</version>
</dependency>
// Gradle 의존성
implementation 'de.codecentric:spring-boot-admin-starter-server:3.1.6'
메인 애플리케이션 클래스에 @EnableAdminServer 어노테이션을 추가해:
import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableAdminServer
public class AdminServerApplication {
public static void main(String[] args) {
SpringApplication.run(AdminServerApplication.class, args);
}
}
2. 클라이언트 설정
// Maven 의존성
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>3.1.6</version>
</dependency>
// Gradle 의존성
implementation 'de.codecentric:spring-boot-admin-starter-client:3.1.6'
application.properties 또는 application.yml에 Admin 서버 주소를 설정해:
# application.properties
spring.boot.admin.client.url=http://localhost:8080
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
# application.yml
spring:
boot:
admin:
client:
url: http://localhost:8080
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
Spring Boot Admin은 다음과 같은 기능을 제공해:
- 모든 애플리케이션 인스턴스의 상태 대시보드
- 상세한 헬스 정보 및 메트릭 시각화
- 로그 레벨 실시간 변경
- JVM & 메모리 정보 모니터링
- 환경 설정 및 빈 정보 조회
- 스레드 덤프 및 힙 덤프 다운로드
Spring Boot Admin은 여러 마이크로서비스로 구성된 시스템을 모니터링할 때 특히 유용해. 모든 서비스의 상태를 한눈에 파악할 수 있고, 문제가 발생했을 때 빠르게 대응할 수 있어! 📊
🔍 실전 활용 사례
지금까지 배운 내용을 바탕으로 실제 비즈니스 상황에서 Actuator와 사용자 정의 엔드포인트를 어떻게 활용할 수 있는지 알아보자!
1. 서비스 상태 모니터링
온라인 쇼핑몰이나 재능넷과 같은 서비스 플랫폼에서는 시스템의 상태를 실시간으로 모니터링하는 것이 중요해. 다음과 같은 엔드포인트를 만들 수 있어:
@Component
@Endpoint(id = "service-status")
public class ServiceStatusEndpoint {
private final UserService userService;
private final OrderService orderService;
private final PaymentService paymentService;
private final NotificationService notificationService;
// 생성자 주입
@ReadOperation
public Map<String, Object> status() {
Map<String, Object> status = new HashMap<>();
// 각 서비스의 상태 확인
status.put("user", checkServiceStatus(userService));
status.put("order", checkServiceStatus(orderService));
status.put("payment", checkServiceStatus(paymentService));
status.put("notification", checkServiceStatus(notificationService));
// 전체 서비스 상태 결정
boolean allServicesUp = ((Map<String, Map<String, Object>>) status)
.values().stream()
.allMatch(serviceStatus -> "UP".equals(serviceStatus.get("status")));
status.put("overall", allServicesUp ? "UP" : "DEGRADED");
status.put("timestamp", System.currentTimeMillis());
return status;
}
private Map<String, Object> checkServiceStatus(Object service) {
Map<String, Object> serviceStatus = new HashMap<>();
try {
// 서비스별 상태 확인 로직
// 예: 데이터베이스 연결, 외부 API 연결 등을 확인
serviceStatus.put("status", "UP");
serviceStatus.put("details", "Service is running normally");
} catch (Exception e) {
serviceStatus.put("status", "DOWN");
serviceStatus.put("details", e.getMessage());
serviceStatus.put("exception", e.getClass().getName());
}
return serviceStatus;
}
}
2. 캐시 관리
성능 최적화를 위해 캐시를 사용하는 애플리케이션에서는 캐시를 관리하는 엔드포인트가 유용해:
@Component
@Endpoint(id = "cache-manager")
public class CacheManagerEndpoint {
private final CacheManager cacheManager;
public CacheManagerEndpoint(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@ReadOperation
public Map<String, Object> getCacheStats() {
Map<String, Object> stats = new HashMap<>();
for (String cacheName : cacheManager.getCacheNames()) {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
stats.put(cacheName, getCacheDetails(cache));
}
}
return stats;
}
@DeleteOperation
public Map<String, String> clearAllCaches() {
for (String cacheName : cacheManager.getCacheNames()) {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
cache.clear();
}
}
Map<String, String> result = new HashMap<>();
result.put("result", "All caches cleared successfully");
return result;
}
@DeleteOperation
public Map<String, String> clearCache(@Selector String cacheName) {
Cache cache = cacheManager.getCache(cacheName);
Map<String, String> result = new HashMap<>();
if (cache != null) {
cache.clear();
result.put("result", "Cache '" + cacheName + "' cleared successfully");
} else {
result.put("error", "Cache '" + cacheName + "' not found");
}
return result;
}
private Map<String, Object> getCacheDetails(Cache cache) {
Map<String, Object> details = new HashMap<>();
// 캐시 구현체에 따라 다양한 정보를 수집
// 예: 크기, 히트율, 미스율 등
// 여기서는 예시로 간단한 정보만 반환
details.put("name", cache.getName());
details.put("type", cache.getClass().getSimpleName());
return details;
}
}
3. 데이터베이스 모니터링
데이터베이스 연결 상태와 성능을 모니터링하는 엔드포인트:
@Component
@Endpoint(id = "database")
public class DatabaseEndpoint {
private final DataSource dataSource;
private final JdbcTemplate jdbcTemplate;
public DatabaseEndpoint(DataSource dataSource, JdbcTemplate jdbcTemplate) {
this.dataSource = dataSource;
this.jdbcTemplate = jdbcTemplate;
}
@ReadOperation
public Map<String, Object> getDatabaseInfo() {
Map<String, Object> info = new HashMap<>();
try {
// 데이터베이스 연결 정보
try (Connection conn = dataSource.getConnection()) {
info.put("url", conn.getMetaData().getURL());
info.put("databaseProduct", conn.getMetaData().getDatabaseProductName());
info.put("databaseVersion", conn.getMetaData().getDatabaseProductVersion());
info.put("driverName", conn.getMetaData().getDriverName());
info.put("driverVersion", conn.getMetaData().getDriverVersion());
info.put("username", conn.getMetaData().getUserName());
}
// 연결 풀 정보 (HikariCP 사용 시)
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
Map<String, Object> poolInfo = new HashMap<>();
poolInfo.put("activeConnections", hikariDataSource.getHikariPoolMXBean().getActiveConnections());
poolInfo.put("idleConnections", hikariDataSource.getHikariPoolMXBean().getIdleConnections());
poolInfo.put("totalConnections", hikariDataSource.getHikariPoolMXBean().getTotalConnections());
poolInfo.put("maxConnections", hikariDataSource.getMaximumPoolSize());
info.put("connectionPool", poolInfo);
}
// 간단한 데이터베이스 통계
Map<String, Object> stats = new HashMap<>();
// 테이블별 레코드 수 (예시)
List<Map<String, Object>> tableStats = jdbcTemplate.queryForList(
"SELECT table_name, table_rows FROM information_schema.tables WHERE table_schema = DATABASE()"
);
stats.put("tables", tableStats);
info.put("statistics", stats);
info.put("status", "UP");
} catch (Exception e) {
info.put("status", "DOWN");
info.put("error", e.getMessage());
}
return info;
}
@ReadOperation
public Map<String, Object> testConnection() {
Map<String, Object> result = new HashMap<>();
long startTime = System.currentTimeMillis();
try {
// 간단한 쿼리로 연결 테스트
String dbTime = jdbcTemplate.queryForObject("SELECT NOW()", String.class);
long responseTime = System.currentTimeMillis() - startTime;
result.put("status", "UP");
result.put("responseTime", responseTime + "ms");
result.put("databaseTime", dbTime);
} catch (Exception e) {
result.put("status", "DOWN");
result.put("error", e.getMessage());
}
return result;
}
}
이런 사용자 정의 엔드포인트를 통해 애플리케이션의 다양한 측면을 모니터링하고 관리할 수 있어. 특히 마이크로서비스 아키텍처에서는 각 서비스의 상태를 중앙에서 모니터링하는 것이 중요한데, Actuator와 Spring Boot Admin을 활용하면 이를 효과적으로 구현할 수 있어! 🔍
📈 성능 최적화 팁
Actuator 엔드포인트는 매우 유용하지만, 잘못 사용하면 애플리케이션 성능에 영향을 줄 수 있어. 여기 몇 가지 성능 최적화 팁을 소개할게!
- 지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
재능넷은 최신 AI 기술과 법률에 기반하여 자사의 지적 재산권을 적극적으로 보호하며,
무단 사용 및 침해 행위에 대해 법적 대응을 할 권리를 보유합니다.
© 2025 재능넷 | All rights reserved.
댓글 0개