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

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 엔드포인트는 매우 유용하지만, 잘못 사용하면 애플리케이션 성능에 영향을 줄 수 있어. 여기 몇 가지 성능 최적화 팁을 소개할게!
-
필요한 엔드포인트만 활성화하기
모든 엔드포인트를 활성화하면 메모리 사용량이 증가하고 성능이 저하될 수 있어. 필요한 엔드포인트만 선택적으로 활성화하는 것이 좋아:management.endpoints.web.exposure.include=health,info,metrics,prometheus -
캐싱 활용하기
자주 변경되지 않는 데이터는 캐싱하여 반복적인 계산을 줄일 수 있어:@Component @Endpoint(id = "system-info") public class SystemInfoEndpoint { private Map<String, Object> cachedInfo; private long lastUpdateTime; private static final long CACHE_TTL = 60000; // 1분 @ReadOperation public Map<String, Object> getSystemInfo() { long currentTime = System.currentTimeMillis(); // 캐시가 유효한지 확인 if (cachedInfo == null || currentTime - lastUpdateTime > CACHE_TTL) { // 캐시 갱신 cachedInfo = computeSystemInfo(); lastUpdateTime = currentTime; } return cachedInfo; } private Map<String, Object> computeSystemInfo() { // 시스템 정보 계산 (비용이 많이 드는 작업) // ... } } -
비동기 처리 활용하기
시간이 오래 걸리는 작업은 비동기로 처리하여 응답 시간을 개선할 수 있어:@Component @Endpoint(id = "async-task") public class AsyncTaskEndpoint { private final TaskExecutor taskExecutor; private final Map<String, TaskStatus> taskStatuses = new ConcurrentHashMap<>(); public AsyncTaskEndpoint(TaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } @WriteOperation public Map<String, Object> startTask() { String taskId = UUID.randomUUID().toString(); TaskStatus status = new TaskStatus(taskId, "STARTED"); taskStatuses.put(taskId, status); // 비동기 작업 시작 taskExecutor.execute(() -> { try { status.setStatus("RUNNING"); // 시간이 오래 걸리는 작업 수행 // ... status.setStatus("COMPLETED"); status.setResult("Task completed successfully"); } catch (Exception e) { status.setStatus("FAILED"); status.setResult("Error: " + e.getMessage()); } }); Map<String, Object> response = new HashMap<>(); response.put("taskId", taskId); response.put("status", "STARTED"); return response; } @ReadOperation public Map<String, Object> getTaskStatus(@Selector String taskId) { TaskStatus status = taskStatuses.get(taskId); if (status == null) { Map<String, Object> error = new HashMap<>(); error.put("error", "Task not found: " + taskId); return error; } return status.toMap(); } @ReadOperation public Map<String, Object> getAllTasks() { Map<String, Object> allTasks = new HashMap<>(); allTasks.put("tasks", taskStatuses.values().stream() .map(TaskStatus::toMap) .collect(Collectors.toList())); return allTasks; } static class TaskStatus { private final String taskId; private String status; private String result; private final long startTime; public TaskStatus(String taskId, String status) { this.taskId = taskId; this.status = status; this.startTime = System.currentTimeMillis(); } // Getter, Setter 생략 public Map<String, Object> toMap() { Map<String, Object> map = new HashMap<>(); map.put("taskId", taskId); map.put("status", status); map.put("startTime", startTime); map.put("duration", System.currentTimeMillis() - startTime); if (result != null) { map.put("result", result); } return map; } } } -
데이터 양 제한하기
대용량 데이터를 반환할 때는 페이징이나 필터링을 적용하여 데이터 양을 제한하는 것이 좋아:@ReadOperation public Map<String, Object> getLogs( @Selector String logLevel, @Nullable Integer page, @Nullable Integer size ) { int pageNum = page != null ? page : 0; int pageSize = size != null ? size : 20; List<LogEntry> allLogs = logService.getLogs(logLevel); int total = allLogs.size(); // 페이징 적용 int fromIndex = pageNum * pageSize; int toIndex = Math.min(fromIndex + pageSize, total); List<LogEntry> pagedLogs = fromIndex < total ? allLogs.subList(fromIndex, toIndex) : Collections.emptyList(); Map<String, Object> result = new HashMap<>(); result.put("logs", pagedLogs); result.put("page", pageNum); result.put("size", pageSize); result.put("total", total); result.put("totalPages", (int) Math.ceil((double) total / pageSize)); return result; }
이런 최적화 기법들은 재능넷과 같은 대규모 서비스에서 특히 중요해. 모니터링 기능이 실제 서비스 성능에 영향을 주지 않도록 주의해야 해! 🚀
🧪 테스트 작성하기
사용자 정의 엔드포인트도 다른 코드와 마찬가지로 테스트가 필요해. Spring Boot는 Actuator 엔드포인트를 테스트하기 위한 도구를 제공해. 테스트 작성 방법을 알아보자!
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
public class CustomEndpointTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "ACTUATOR_ADMIN")
public void testAppStatusEndpoint() throws Exception {
mockMvc.perform(get("/actuator/app-status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("My Awesome Application"))
.andExpect(jsonPath("$.version").value("1.0.0"))
.andExpect(jsonPath("$.status").value("UP and RUNNING"))
.andExpect(jsonPath("$.activeUsers").isNumber())
.andExpect(jsonPath("$.pendingTasks").isNumber());
}
@Test
@WithMockUser(roles = "ACTUATOR_ADMIN")
public void testFeaturesEndpoint() throws Exception {
// 모든 기능 조회
mockMvc.perform(get("/actuator/features"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.['user-registration'].enabled").isBoolean())
.andExpect(jsonPath("$.['payment-processing'].enabled").isBoolean());
// 특정 기능 조회
mockMvc.perform(get("/actuator/features/user-registration"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.enabled").isBoolean())
.andExpect(jsonPath("$.description").isString());
// 기능 활성화/비활성화
mockMvc.perform(post("/actuator/features/user-registration?enabled=false"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.['user-registration'].enabled").value(false));
// 변경 확인
mockMvc.perform(get("/actuator/features/user-registration"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.enabled").value(false));
}
@Test
public void testUnauthorizedAccess() throws Exception {
// 인증 없이 접근
mockMvc.perform(get("/actuator/app-status"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "USER") // ACTUATOR_ADMIN 권한 없음
public void testInsufficientPermissions() throws Exception {
mockMvc.perform(get("/actuator/app-status"))
.andExpect(status().isForbidden());
}
}
위 테스트 코드에서는 @WithMockUser 어노테이션을 사용하여 인증된 사용자로 테스트를 수행하고 있어. 또한 jsonPath를 사용하여 응답 JSON의 특정 필드를 검증하고 있지.
테스트에서는 다음과 같은 상황을 검증하는 것이 중요해:
- 정상적인 접근 시 올바른 데이터를 반환하는지
- 인증되지 않은 사용자의 접근이 차단되는지
- 권한이 부족한 사용자의 접근이 차단되는지
- 쓰기 작업이 데이터를 올바르게 변경하는지
테스트를 작성하면 엔드포인트의 기능과 보안이 의도대로 동작하는지 확인할 수 있어. 특히 중요한 관리 기능을 제공하는 엔드포인트는 철저한 테스트가 필수야! 🧪
🌐 Actuator와 마이크로서비스
마이크로서비스 아키텍처에서는 여러 서비스의 상태를 모니터링하고 관리하는 것이 중요해. Actuator는 이런 환경에서 특히 유용해!
서비스 디스커버리와 연동
Eureka나 Consul과 같은 서비스 디스커버리 시스템과 Actuator를 연동하면, 모든 서비스 인스턴스의 상태를 중앙에서 모니터링할 수 있어:
# application.properties
eureka.client.healthcheck.enabled=true
# 또는 application.yml
eureka:
client:
healthcheck:
enabled: true
이 설정을 추가하면 Eureka는 Actuator의 health 엔드포인트를 사용하여 서비스의 상태를 확인해. 서비스에 문제가 생기면 Eureka는 해당 서비스를 라우팅 대상에서 제외할 수 있어!
분산 추적과 연동
Spring Cloud Sleuth와 Zipkin을 사용하면 마이크로서비스 간의 요청 흐름을 추적할 수 있어:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
Spring Boot 3.x에서는 Micrometer Tracing을 사용해:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
이런 도구들을 사용하면 여러 서비스에 걸친 요청의 흐름을 추적하고, 성능 병목 지점을 찾아낼 수 있어!
중앙 집중식 로깅
ELK 스택(Elasticsearch, Logstash, Kibana)이나 Graylog와 같은 도구를 사용하여 모든 서비스의 로그를 중앙에서 수집하고 분석할 수 있어:
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
logback.xml 설정:
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>logstash-server:5000</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdc>true</includeMdc>
<customFields>{"app_name":"my-service"}</customFields>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="LOGSTASH" />
</root>
이런 설정을 통해 모든 서비스의 로그를 중앙에서 수집하고, 특정 요청 ID로 여러 서비스에 걸친 로그를 조회할 수 있어!
마이크로서비스 환경에서는 이런 모니터링과 관리 도구들이 필수적이야. Actuator와 함께 이런 도구들을 활용하면 복잡한 시스템도 효과적으로 운영할 수 있어! 🌐
🔮 Spring Boot Actuator의 미래
Spring Boot Actuator는 계속해서 발전하고 있어. 2025년 현재의 트렌드와 앞으로의 발전 방향을 살펴보자!
1. 클라우드 네이티브 모니터링 강화
쿠버네티스와 같은 클라우드 네이티브 환경에서의 모니터링이 더욱 강화될 거야. 특히 Prometheus와의 통합이 더욱 발전할 것으로 예상돼:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
이 의존성을 추가하면 /actuator/prometheus 엔드포인트가 활성화되어 Prometheus가 메트릭을 수집할 수 있어.
2. 관찰성(Observability) 향상
단순한 모니터링을 넘어 로그, 메트릭, 트레이스를 통합한 관찰성이 중요해질 거야. Spring Boot 3.x에서 도입된 Micrometer Tracing이 더욱 발전할 것으로 예상돼:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
OpenTelemetry는 로그, 메트릭, 트레이스를 통합하는 표준으로 자리잡고 있어. Spring Boot는 이를 더욱 적극적으로 지원할 것으로 예상돼.
3. 보안 강화
Actuator 엔드포인트의 보안이 더욱 강화될 것으로 예상돼. 특히 OAuth2와 같은 현대적인 인증 방식과의 통합이 발전할 거야:
@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/**").hasAuthority("SCOPE_actuator.read")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
return http.build();
}
}
4. AI 기반 이상 탐지
머신러닝과 AI를 활용한 이상 탐지 기능이 추가될 것으로 예상돼. 정상적인 패턴에서 벗어난 메트릭 변화를 자동으로 감지하고 알림을 보내는 기능이 발전할 거야.
5. 서버리스 환경 지원
AWS Lambda나 Azure Functions와 같은 서버리스 환경에서도 Actuator를 효과적으로 활용할 수 있는 방법이 발전할 거야. 특히 콜드 스타트 문제를 해결하기 위한 최적화가 이루어질 것으로 예상돼.
이런 발전 방향은 재능넷과 같은 현대적인 서비스 플랫폼에 큰 도움이 될 거야. 더 효과적인 모니터링과 관리를 통해 서비스 안정성을 높이고, 사용자 경험을 개선할 수 있을 거야! 🔮
🎯 결론: 나만의 Actuator 엔드포인트로 애플리케이션 관리 마스터하기
지금까지 Spring Boot Actuator와 사용자 정의 엔드포인트에 대해 깊이 있게 알아봤어. 이제 정리해볼까?
Spring Boot Actuator는 애플리케이션의 모니터링과 관리를 위한 강력한 도구야. 기본 제공되는 엔드포인트만으로도 많은 정보를 얻을 수 있지만, 사용자 정의 엔드포인트를 만들면 우리 애플리케이션에 특화된 정보와 기능을 제공할 수 있어!
사용자 정의 엔드포인트를 만들 때는 다음 사항을 고려해야 해:
- 보안: 민감한 정보와 관리 기능은 적절한 인증과 권한 검사로 보호해야 해
- 성능: 엔드포인트가 애플리케이션 성능에 영향을 주지 않도록 최적화해야 해
- 유용성: 실제로 도움이 되는 정보와 기능을 제공해야 해
- 확장성: 시스템이 성장함에 따라 쉽게 확장할 수 있도록 설계해야 해
특히 마이크로서비스 아키텍처에서는 Actuator가 더욱 중요해져. 여러 서비스의 상태를 모니터링하고, 문제가 발생했을 때 빠르게 대응할 수 있도록 도와주지.
2025년 현재, 클라우드 네이티브 환경과 관찰성(Observability)이 중요해지면서 Actuator의 역할도 더욱 확대되고 있어. 로그, 메트릭, 트레이스를 통합하여 시스템의 상태를 종합적으로 파악할 수 있는 도구로 발전하고 있지.
이 글에서 배운 내용을 활용하면, 너만의 커스텀 엔드포인트를 만들어 애플리케이션을 더 효과적으로 모니터링하고 관리할 수 있을 거야. 특히 재능넷과 같은 대규모 서비스 플랫폼에서는 이런 모니터링과 관리 기능이 서비스 안정성과 사용자 경험에 큰 영향을 미치지!
마지막으로, Actuator는 단순한 모니터링 도구를 넘어 애플리케이션의 운영과 관리를 위한 필수 도구라는 점을 기억해. 개발 단계부터 Actuator를 고려하여 설계하면, 운영 단계에서 많은 도움을 받을 수 있을 거야! 🚀
관련 키워드
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.