📢 장정우님이 지음,
[스프링부트 핵심가이드 : 스프링 부트를 활용한 애플리케이션 개발 실무] 책을 읽고 정리한 글입니다.
애플리케이션을 개발하는 단계를 지나 운영 단계에 접어들면 애플리케이션이 정상적으로 동작하는지 모니터링하는 환경을 구축하는 것이 매우 중요해진다. 스프링 부트 액추에이터는 HTTP 엔드포인트나 JMX를 활용해 애플리케이션을 모니터링하고 관리할 수 있는 기능을 제공한다. 이번 장에서는 액추에이터의 환경을 설정하고 활용하는 방법을 다룰 예정이다.
💡 JMX?
JMX(Java Management Extensions)는 실행 중인 애플리케이션의 상태를 모니터링하고 설정을 변경할 수 있게 해주는 APP이다.
이번 장에서 사용할 새로운 프로젝트를 생성하겠다. 스프링 부트 버전은 이전과 같은 2.5.6 버전으로 진행하며, 다음과 같은 내용을 설정한다.
groupId : com.springboot
artifactId : actuator
name : actuator
Developer Tools : Spring Configuration Processor
Web : Spring Web
그리고 이전 장에서 사용한 SwaggerConfiguration 클래스를 가져오고 그에 따른 의존성을 추가한다.
액추에이터 기능을 사용하려면 애플리케이션에 spring-boot-starter-actuator 모듈의 종속성을 추가해야 한다. 아래와 같이 pom.xml파일에 추가하면 된다.
<<dependencies>
... 생략 ...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
... 생략 ...
</dependencies>
액추에이터의 엔드포인트는 애플리케이션의 모니터링을 사용하는 경로이다. 스프링 부트에는 여러 내장 엔드포인트가 포함돼 있으며, 커스텀 엔드포인트를 추가할 수도 있다. 액푸에이터를 추가하면 기본적으로 엔드포인트 URL로 /actuator가 추가되며 이 뒤에 경로를 추가해 상세 내역에 접근한다. 만약 /actuator 경로가 아닌 다른 경로를 사용하고 싶다면 아래와 같이 application.properties파일에 작성한다.
management.endpoints.web.base-path=/custom-path
엔드포인트 활성화 여부와 노출 여부를 설정할 수 있다. 활성화는 기능 자체를 활성화할 것인지를 결정하는 것으로, 비활성화된 엔트포인트는 애플리케이션 컨텍스트에서 완전히 제거된다. 엔트포인트를 활성화하려면 application.propertis 파일에 속성을 추가하면 된다. 간단한 예로 아래와 같이 작성할 수 있다.
management.endpoint.shutdown.enabled=true
management.endpoint.caches.enabled=true
위 예제의 설정은 엔드포인트의 shutdown기능은 활성화하고 caches 기능은 비활성화하겠다는 의미이다.
또한 액추에이터 설정을 통해 기능 활성화/비활성화가 아니라 엔드포인트의 노출 여부만 설정하는 것도 가능하다. 노출 여부는 JMX를 통한 노출과 HTTP를 통한 노출이 있어 아래와 같이 설정이 구분된다.
## 엔드포인트 노출 설정
## HTTP 설정
management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.exclude=threaddump, heapdump
## JMX 설정
management.endpoints.jmx.exposure.include=*
management.endpoints.jmx.exposure.exclude=threaddump, heapdump
위 설정을 해석하면 web과 jmx 환경에서 엔드포인트를 전체적으로 노출하며, 스레드 덤프(thread dump)와 힙 덤프(heap dump) 기능은 제외하겠다는 의미이다.
액추에이터를 활성화하고 노출 지점도 설정하고 나면 애플리케이션에서 해당 기능을 사용할 수 있다. 모든 기능을 살펴보기 위해서는 다른 의존성을 추가하거나 몇 가지 설정을 추가해야 하기 때문에 이번 절에서는 기능 추가 없이 액추에이터 설정만으로 볼 수 있는 기능 위주로 살펴보겠다.
액추에이터의 /info 엔드포인트를 활용하면 가동 중인 애플리케이션의 정보를 볼 수 있다. 제공하는 정보의 범위는 애플리케이션에서 몇 가지 방법을 거쳐 제공할 수 있으나 application.properties파일에 ‘info.’ 로 시작하는 속성 값들을 정의하는 것이 가장 쉬운 방법이다. 간단한 예로 아래와 같이 애플리케이션의 정보를 작성할 수 있다.
info.organization.name=wikibooks
info.contact.email=thinkground.flature@email.com
info.contact.phoneNumber=010-1234-5678
그러고 나서 애플리케이션을 가동한 후 브라우저에서 아래 URL에 접근하면 아래와 같은 결괏값이 확인된다.
http://localhost:8080/actuator/info
{
"organization":{
"name":"wikibooks"
},
"contact":{
"email":"thinkground.flature@email.com",
"phoneNumber":"010-1234-5678"
}
}
참고로 출력 결과가 한 줄로 나와 보기 힘들 경우에는 JSON Formatter 사이트(https://jsonformatter.curiousconcept.com/)를 통해 출력 결과를 좀 더 보기 쉽게 확인할 수 있다.
/health 엔드포인트를 활용하면 애플리케이션의 상태를 확인할 수 있다. 별도의 설정 없이 다음 URL에 접근하면 아래와 같은 결과를 확인할 수 있다.
http://localhost:8080/actuator/health
{"status":"UP"}
이 결과는 주로 네트워크 계층 중 L4(Loadbalancing) 레벨에서 애플리케이션의 상태를 확인하기 위해 사용된다. 상세 상태를 확인하고 싶다면 아래와 같이 설정하면 된다.
management.endpoint.health.show-details=always
액추애이터의 /beans 엔드포인트를 사용하면 스프링 컨테이너에 등록된 스프링 빈의 전체 목록을 표시할 수 있다. 이 엔드포인트는 JSON 형식으로 빈의 정보를 반환한다. 다만 스프링은 워낙 많은 빈이 자동으로 등록되어 운영되기 때문에 실제로 내용을 출력해서 육안으로 내용을 파악하기는 어렵다. 간단하게 출력된 내용을 보면 아래와 같다.
http://localhost:8080/actuator/beans
{
"contexts":{
"application":{
"beans":{
"endpointCachingOperationInvokerAdvisor":{
"aliases":[],
"scope":"singleton",
"type":"org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor", ...
},
"parentId":null
}
}
}
스프링 부트의 자동설정(AutoConfiguration) 조건 내역을 확인하려면 ‘/conditions’ 엔드포인트를 사용한다. 다음 URL로 접근하면 아래와 같은 내용을 확인할 수 있다.
http://localhost:8080/actuator/conditions
{
"contexts":{
"application":{
"positiveMatches":{
"AuditEventsEndpointAutoConfiguration":[
{
"condition":"OnAvailableEndpointCondition",
"message":"@ConditionalOnAvailableEndpoint no property management.endpoint.auditevents.enabled found so using endpoint default; @ConditionalOnAvailableEndpoint marked as exposed by a 'management.endpoints.jmx.exposure' property"
}
],
"BeansEndpointAutoConfiguration":[
{
"condition":"OnAvailableEndpointCondition",
"message":"@ConditionalOnAvailableEndpoint no property management.endpoint.beans.enabled found so using endpoint default; @ConditionalOnAvailableEndpoint marked as exposed by a 'management.endpoints.jmx.exposure' property"
}
],
"BeansEndpointAutoConfiguration#beansEndpoint":[
{
"condition":"OnBeanCondition",
"message":"@ConditionalOnMissingBean (types: org.springframework.boot.actuate.beans.BeansEndpoint; SearchStrategy: all) did not find any beans"
}
],...
}
}
}
}
출력 내용은 크게 positiveMatches 와 negativeMatches 속성으로 구분되는데, 자동설정의 @Conditional에 따라 평가된 내용을 표시한다.
/env 엔드포인트는 스프링의 환경변수 정보를 확인하는 데 사용된다. 기본적으로 application.properties 파일의 변수들이 표시되며, OS, JVM의 환경변수도 함께 표시된다. 다음 URL로 접근하면 아래와 같은 결과를 확인할 수 있다. 참고로 /env 엔드포인트의 출력값은 내용이 매우 복잡하기 때문에 일부 내용만 발췌했다.
http://localhost:8080/actuator/env
{
"activeProfiles":[
],
"propertySources":[
{
"name":"server.ports",
"properties":{
"local.server.port":{
"value":8080
}
}
},
{
"name":"servletContextInitParams",
"properties":{
}
},
{
"name":"systemProperties",
"properties":{
"sun.desktop":{
"value":"windows"
},
"awt.toolkit":{
"value":"sun.awt.windows.WToolkit"
},
"java.specification.version":{
"value":"11"
},
"sun.cpu.isalist":{
"value":"amd64"
},
"sun.jnu.encoding":{
"value":"MS949"
},
...
}
}
}
}
만약 일부 내용에 포함된 민감한 정보를 가리기 위해서는 management.endpoint.env.keys-to-sanitize 속성을 사용하면 된다. 해당 속성에 넣을 수 있는 값은 단순 문자열이나 정규식을 활용한다.
애플리케이션의 로깅 레벨 수준이 어떻게 설정돼 있는지 확인하려면 /loggers 엔드포인트를 사용할 수 있다. 다음 URL에 접근하면 아래와 같은 결과가 출력된다. 참고로 출력 결과가 매우 길기 때문에 일부 내용만 발췌했다.
http://localhost:8080/actuator/loggers
{
"levels":[
"OFF",
"ERROR",
"WARN",
"INFO",
"DEBUG",
"TRACE"
],
"loggers":{
"ROOT":{
"configuredLevel":"INFO",
"effectiveLevel":"INFO"
},
"_org":{
"configuredLevel":null,
"effectiveLevel":"INFO"
},
"_org.springframework":{
"configuredLevel":null,
"effectiveLevel":"INFO"
},
"_org.springframework.web":{
"configuredLevel":null,
"effectiveLevel":"INFO"
},
"_org.springframework.web.servlet":{
"configuredLevel":null,
"effectiveLevel":"INFO"
},
"groups":{
"web":{
"configuredLevel":null,
"members":[
"org.springframework.core.codec",
"org.springframework.http",
"org.springframework.web",
"org.springframework.boot.actuate.endpoint.web",
"org.springframework.boot.web.servlet.ServletContextInitializerBeans"
]
},
"sql":{
"configuredLevel":null,
"members":[
"org.springframework.jdbc.core",
"org.hibernate.SQL",
"org.jooq.tools.LoggerListener"
]
}
}
}
위 예제는 GET 메서드로 호출한 결과이며, POST 형식으로 호출하면 로깅 레벨을 변경하는 것도 가능하다.
앞에서 살펴봤듯이 액추에이터는 다양한 정보를 가공해서 제공한다. 그 밖에 개발자의 요구사항에 맞춘 커스텀 기능 설정도 제공한다. 커스텀 기능을 개발하는 방식에는 크게 두 가지가 있다. 첫 번째는 기존 기능에 내용을 추가하는 방식이고, 두 번째는 새로운 엔드포인트를 개발하는 방식이다.
액추에이터를 커스터마이징하는 가장 간단한 방법은 앞에서 /info 엔드포인트의 내용을 추가한 것처럼 application.properties 파일 내에 내용을 추가하는 것이다. 그러나 이 방법은 많은 내용을 담을 때는 관리 측면이 좋지 않다.
그래서 커스텀 기능을 설정할 때는 별도의 구현체 클래스를 작성해서 내용을 추가하는 방법을 많이 활용된다. 액추에이터에서는 InfoContributor 인터페이스를 제공하고 있는데, 이 인터페이스를 구현하는 클래스를 생성하면 된다. 아래와 같이 InfoContributor 인터페이스에 대한 구현 클래스를 생성한다.
@Component
public class CustomInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
Map<String, Object> content = new HashMap<>();
content.put("code-info", "InfoContributor 구현체에서 정의한 정보입니다.");
builder.withDetail("custom-info-contributor", content);
}
}
새로 생성한 클래스를 InfoContributor 인터페이스의 구현체로 설정하면 contributor 메서드를 오버라이딩할 수 있게 된다. 이 메서드에서 파라미터로 받은 Builder객체는 액추에이터 패키지의 Info클래스 안에 정의돼 있는 클래스로서 info 엔드포인트에서 보여줄 내용을 담는 역할을 수행한다. 이렇게 객체를 가져와 6~8번 줄처럼 콘텐츠를 담아 builder에 포함하면 엔드포인트 출력 결과에서 확인할 수 있다. 아래와 같이 설정한 후 애플리케이션을 재가동해서 엔드포인트를 호출하면 아래와 같은 결과를 볼 수 있다.
{
"organization":{
"name":"wikibooks"
},
"contact":{
"email":"thinkground.flature@email.com",
"phoneNumber":"010-1234-5678"
},
"custom-info-contributor":{
"code-info":"InfoContributor 구현체에서 정의한 정보입니다."
}
}
보다시피 기존 application.properties 에서 정의했던 속성값을 비롯해 구현체 클래스에서 포함한 내용이 추가된 것을 볼 수 있다.
@EndPoint 어노테이션으로 빈에 추가된 객체들은 @ReadOperation, @WriteOperation, @DeleteOperation 어노테이션을 사용해 JMX나 HTTP를 통해 커스텀 엔드포인트를 노출시킬 수 있다. 만약 JMX에서만 사용하거나 HTTP에서만 사용하는 것을 제한하고 싶다면 @JmxEndpoint, @WebEndpoint 어노테이션을 사용하면 된다.
이 책에서는 간단하게 애플리케이션에 메모 기록을 남길 수 있는 기능을 엔드포인트로 생성하겠다. 아래와 같이 엔드포인트 클래스를 생성한다.
@Component
@Endpoint(id = "note")
public class NoteEndpoint {
private Map<String, Object> noteContent = new HashMap<>();
@ReadOperation
public Map<String, Object> getNote(){
return noteContent;
}
@WriteOperation
public Map<String, Object> writeNote(String key, Object value){
noteContent.put(key, value);
return noteContent;
}
@DeleteOperation
public Map<String, Object> deleteNote(String key){
noteContent.remove(key);
return noteContent;
}
}
위 코드에서 @Endpoint 어노테이션을 사용하고 있다. 이 어노테이션을 선언하면 액추에이터에 엔드포인트로 자동으로 등록되며 id 속성값으로 경로를 정의할 수 있다. 또한 enableByDefault라는 속성으로 현재 생성하는 엔드포인트의 기본 활성화 여부도 설정 가능한다. enableByDefault 속성의 기본값은 true로서 값을 별도로 설정하지 않으면 활성화된다.
엔드포인트를 설정하는 클래스에는 @ReadOpertation, @WriteOperation, @DeleteOperation 어노테이션을 사용해 각 동작 메서드를 생성할 수 있다.
@ReadOperation 어노테이션을 정의해 HTTP, GET 요청을 반응하는 메서드를 생성했다. 이 클래스에서는 noteContent라고 하는 Map타입의 객체를 전달하고 있다. 애플리케이셔을 재가동한 후 다음 엔드포인트를 호출해 보겠다.
http://localhost:8080/actuator/note
{}
보다시피 아직 값을 넣지 않은 상태라서 JSON 형태의 빈 값이 표현된다.
👀 TIP
스프링 부트 액추에이터의 자세한 내용은 공식 페이지에서 확인할 수 있다.
https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html