프로덕션 준비 기능
개발자는 애플리케이션을 개발할 때 기능 요구사항만 개발하지 않는다.
서비스를 실제 운영 단계에 올리게 되면 서비스에 문제가 없는지 모니터링하고 지표를 심어 감시해 대처해야 한다.
이렇게 서비스 운영 단계에서 필요한 기능들을 프로덕션 준비 기능이라고 한다. 배포할 때 준비해야 하는 비 기능적 요소들을 뜻한다.
- 지표(metric), 추적(trace), 감사(auditing)
- 모니터링
애플리케이션이 현재 정상 동작하는지, 로그 정보는 정상 설정 되어있는지, 커넥션 풀은 얼마나 사용되고 있는지 등을 확인할 수 있어야 한다.
스프링에서의 프로덕션 준비 기능
스프링 부트는 액츄에이터라는 것을 제공한다. 액츄에이터는 시스템을 움직이거나 제어하는 데 쓰이는 기계 장치라는 뜻이다.
액츄에이터는 프로덕션 준비 기능을 편리하게 사용할 수 있는 편의 기능들을 제공해준다. 더해 마이크로미터, 프로메테우스, 그라파나와 같은 흔히 사용하는 모니터링 시스템과 매우 쉽게 연동할 수 있도록 지원한다.
액츄에이터
1
implementation 'org.springframework.boot:spring-boot-starter-actuator'
이제 액츄에이터 의존성을 추가하고 사용해보면서 액츄에이터에 대해 이해해보자.
동작 확인
우선 의존성이 제대로 추가되었고, 제대로 실행되는지 확인해보자.
애플리케이션을 실행 후 http://localhost:8080/actuator 요청을 하게 되면 아래와 같이 응답이 나타난다.
액츄에이터는 /actuator 경로를 통해 기능을 제공한다.
1
management.endpoints.web.exposure.include=*
만약 위 속성을 추가한다면 아래와 같이 모든 기능을 웹에 노출하게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
{
"_links": {
"self": {
"href": "http://localhost:8080/actuator",
"templated": false
},
"beans": {
"href": "http://localhost:8080/actuator/beans",
"templated": false
},
"caches-cache": {
"href": "http://localhost:8080/actuator/caches/{cache}",
"templated": true
},
"caches": {
"href": "http://localhost:8080/actuator/caches",
"templated": false
},
"health": {
"href": "http://localhost:8080/actuator/health",
"templated": false
},
"health-path": {
"href": "http://localhost:8080/actuator/health/{*path}",
"templated": true
},
"info": {
"href": "http://localhost:8080/actuator/info",
"templated": false
},
"conditions": {
"href": "http://localhost:8080/actuator/conditions",
"templated": false
},
"configprops": {
"href": "http://localhost:8080/actuator/configprops",
"templated": false
},
"configprops-prefix": {
"href": "http://localhost:8080/actuator/configprops/{prefix}",
"templated": true
},
"env": {
"href": "http://localhost:8080/actuator/env",
"templated": false
},
"env-toMatch": {
"href": "http://localhost:8080/actuator/env/{toMatch}",
"templated": true
},
"loggers-name": {
"href": "http://localhost:8080/actuator/loggers/{name}",
"templated": true
},
"loggers": {
"href": "http://localhost:8080/actuator/loggers",
"templated": false
},
"heapdump": {
"href": "http://localhost:8080/actuator/heapdump",
"templated": false
},
"threaddump": {
"href": "http://localhost:8080/actuator/threaddump",
"templated": false
},
"metrics-requiredMetricName": {
"href": "http://localhost:8080/actuator/metrics/{requiredMetricName}",
"templated": true
},
"metrics": {
"href": "http://localhost:8080/actuator/metrics",
"templated": false
},
"scheduledtasks": {
"href": "http://localhost:8080/actuator/scheduledtasks",
"templated": false
},
"mappings": {
"href": "http://localhost:8080/actuator/mappings",
"templated": false
}
}
}
- 빈 등록 정보
- 헬스 정보 (서버 상태)
- 컨트롤러 매핑 정보 (mappings)
- 기타 등등
웬만한 정보를 전부 제공해준다.
엔드포인트 설정
액츄에이터가 제공하는 기능 하나하나를 엔드포인트라고 한다.
health 같은 경우 헬스 정보인 것 처럼 각각의 엔드포인트는 /actuator/엔드포인트 와 같은 형식으로 접근할 수 있다.
엔드포인트를 사용하기 위해 아래 2가지 과정이 필요하다.
- 엔드포인트 활성화
- 해당 기능 자체를 사용할지 말지 on/off 선택을 하는 것
- 엔드포인트 노출
- 활성화된 엔드포인트를 HTTP에 노출할지 / JMX에 노출할지 선택하는 것
- 두 부분에 모두 노출할 수도 있다.
- 노출은 활성화가 전제되어야 한다. 활성화 되어있지 않으면 노출이 되지 않는다.
엔드포인트는 대부분 기본으로 활성화되어있다.
따라서 어떤 엔드포인트를 어디에 노출할 지 선택하면 된다. 보통 JMX는 잘 사용하지 않아 HTTP에 어떤 엔드포인트를 노출할 지 선택하게 된다.
엔드포인트 활성화/노출
- 엔드포인트 활성화
엔드포인트 활성화 방법을 보자.
엔드포인트는 대부분 기본으로 활성화되어있다고 했는데, shutdown 엔드포인트는 기본으로 활성화되어있지 않다.
shutdown 엔드포인트를 활성화하는 예시이다.
1
management.endpoint.shutdown.enabled=true
활성화하고 /actuator/shutdown을 POST 요청하면 서버가 다운된다.
- 엔드포인트 노출
이제 엔드포인트를 노출하는 방법을 보자.
1
2
3
management.endpoints.web.exposure.include=* //엔드포인트 전체 노출
management.endpoints.web.exposure.include="health,info" //health, info를 노출
management.endpoints.web.exposure.exclude="env,beans" //env, beans는 제외
엔드포인트 목록
각각의 엔드포인트를 통해 개밸자는 애플리케이션 내부의 수 많은 기능을 관리하고 모니터링 할 수 있다.
자주 사용하는 기능들의 엔드포인트를 알아보자.
- beans: 스프링 컨테이너에 등록된 스프링 빈을 보여준다.
- conditions: condition을 통해 빈을 등록할 때 평가 조건과 일치하거나 일치하지 않는 이유를 보여준다.
- configprops: @ConfigurationProperties를 보여준다.
- env: Environment 정보를 보여준다. (환경 변수)
- health: 애플리케이션 헬스 정보를 보여준다.
- httpexchanges: HTTP 호출 응답 정보를 보여준다. HttpExchangeRepository를 구현한 빈을 별도로 등록해야 한다.
- info: 애플리케이션 정보를 보여준다.
- loggers: 애플리케이션 로거 설정을 보여주고 변경도 가능하다.
- metrics: 애플리케이션의 메트릭 정보를 보여준다.
- mappings: @RequestMapping 정보를 보여준다.
- threaddump: 쓰레드 덤프를 실행해서 보여준다.
엔드포인트에 대한 추가 정보는 공식 사이트를 참고하자. (https://docs.spring.io/spring-boot/reference/actuator/endpoints.html#actuator.endpoints)
헬스 정보
헬스 정보를 활용하면 애플리케이션에 문제가 발생했을 때 문제를 빠르게 인지할 수 있다.
헬스 정보는 단순히 애플리케이션이 요청에 응답을 할 수 있는지 판단하는 것을 넘어 애플리케이션이 사용하는 데이터베이스가 응답하는지, 디스크 사용량에는 문제가 없는지 같은 다양한 정보들을 포함해 만들어진다.
1
2
management.endpoint.health.show-details=always
management.endpoint.health.show-components=always //아래 응답에서 각 요소 status만 보여준다.
위 설정을 통해 헬스 정보를 더 자세하게 볼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"status": "UP",
"components": {
"db": {
"status": "UP", //DB 상태 정상
"details": {
"database": "H2",
"validationQuery": "isValid()"
}
},
"diskSpace": { //디스크 공간
"status": "UP",
"details": {
"total": 511247314944,
"free": 156407230464,
"threshold": 10485760,
"path": "C:\\Users\\h8\\Desktop\\study\\springSourceCode\\monitor\\start\\actuator\\.",
"exists": true
}
},
"ping": {
"status": "UP"
}
}
}
- validationQuery: JDBC 스펙에 해당 DB 서버가 살아있는지 검증하는 로직이 있는데, DB에서 살아있다고 응답을 주었다는 여부를 확인한다.
만약 각 status 중 하나라도 DOWN이면 전체 상태도 DOWN이 된다.
액츄에이터는 db, mongo, redis, diskspace, ping과 같은 수 많은 헬스 기능을 기본으로 제공한다.
애플리케이션 정보
info 엔드포인트는 애플리케이션의 기본 정보를 노출한다.
- java: 자바 런타임 정보
- os: OS 정보
- env: Environment에서 info. 로 시작하는 정보를 제공한다.
- build: 빌드 정보를 제공한다.
- META-INF/build-info.properties 파일이 필요하다.
- git: git 정보를 제공한다.
- git.properties 파일이 필요하다.
java, os, env는 기본으로 비활성화 되어있다.
기본적으로 실행하면 정보들이 보이지 않는다. 설정을 통해 활성화해보자.
1
2
management.info.java.enabled=true
management.info.os.enabled=true
위 설정으로 보여질 부분들을 활성화할 수 있다. 결과는 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"java": {
"version": "17.0.9",
"vendor": {
"name": "Oracle Corporation"
},
"runtime": {
"name": "Java(TM) SE Runtime Environment",
"version": "17.0.9+11-LTS-201"
},
"jvm": {
"name": "Java HotSpot(TM) 64-Bit Server VM",
"vendor": "Oracle Corporation",
"version": "17.0.9+11-LTS-201"
}
},
"os": {
"name": "Windows 11",
"version": "10.0",
"arch": "amd64"
}
}
env 사용
1
2
3
management.info.env.enabled=true
info.app.name=hello-actuator
info.app.company=ldh
위와 같이 설정해주면 info.로 시작하는 정보를 출력해준다.
1
2
3
4
5
"app": {
"name": "hello-actuator",
"company": "ldh"
},
//...
build 사용
빌드 정보를 노출하기 위해서는 빌드 시점에 META-INF/build-info.properteis 파일을 만들어야 한다.
gradle을 사용한다면 아래 내용을 추가하면 된다.
1
2
3
springBoot {
buildInfo()
}
gradle에 위 내용을 추가하고 build해보면 build > resources > main > META-INF > build-info.properties가 자동으로 생성되어 있는 것을 볼 수 있다.
1
2
3
4
5
build.artifact=actuator
build.group=hello
build.name=actuator
build.time=2024-10-27T08\:29\:06.827212100Z
build.version=0.0.1-SNAPSHOT
위 정보가 나타나고 애플리케이션의 기본 정보, 버전, 그리고 빌드 시간을 알 수 있다.
git 사용
build와 마찬가지로 git 정보를 노출하기 위해서는 git.properties가 필요한데 직접 만드는 것이 아니다.
1
2
3
4
plugins {
...
id "com.gorylenko.gradle-git-properties" version "2.4.1"
}
gradle에 해당 부분을 추가해주면 된다. 단, 해당 프로젝트가 git에 관리되어 있어야 한다. 그렇지 않으면 에러가 발생한다.
1
2
3
4
5
6
7
8
9
10
{
"git":{
"branch":"main",
"commit":{
"id":"754bc78",
"time":"2023-01-01T00:00:00Z"
}
}
...
}
설정을 하고 info를 실행하게 되면 브랜치 정보, 커밋 id, 커밋 시간 등을 알 수 있게 된다.
이 빌드는 main 브랜치와 754bc78 커밋에서 만들어졌다는 뜻이다.
애플리케이션을 배포할 때 가끔 기대와 전혀 다르게 동작하는 경우가 있는데 확인해보면 다른 커밋이나 다른 브랜치의 내용이 배포된 경우인 것을 이를 통해 확인해볼 수 있다.
1
management.info.git.mode="full"
git에 대한 더 자세한 정보를 보고 싶다면 위 옵션을 적용해볼 수 있다.
loggers 사용
loggers 엔드포인트를 사용하면 로깅과 관련된 정보를 확인하고, 또 실시간으로 변경도 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@RestController
public class LogController {
@GetMapping("/log")
public String log() {
log.trace("trace log");
log.debug("debug log");
log.info("info log");
log.warn("warn log");
log.error("error log");
return "ok";
}
}
간단한 로그 컨트롤러를 만들어서 테스트해보도록 하자.
실행해보면 info, warn, error 로그가 남게 되는데 기본 설정이 info 이상의 로그만 보여지게 되도록 되어 있어서 그렇다.
1
logging.level.hello.controller=debug
위 설정을 통해 해당 패키지의 로깅 레벨을 설정해줄 수 있다.
1
2
3
4
5
6
7
8
9
"hello.controller": {
"configuredLevel": "DEBUG",
"effectiveLevel": "DEBUG"
},
"hello.controller.LogController": {
"effectiveLevel": "DEBUG"
},
//...
}
그리고 실행해보면 위와 같은 결과를 볼 수 있다.
이렇게 로깅 정보를 확인하고 왜 이 부분에 DEBUG 로그가 나오지 않는지 등을 확인할 수 있는 것이다.
1
2
http://localhost:8080/actuator/loggers/{로거 이름}
http://localhost:8080/actuator/loggers/hello.controller
더 자세하게 조회하고 싶다면 위와 같이 접근할 수도 있다.
1
2
3
4
{
"configuredLevel": "DEBUG",
"effectiveLevel": "DEBUG"
}
이런 기능을 제공하는 이유와 활용 방법에 대해 알아보자.
loggers로 실시간 로그 레벨 변경하기
개발 서버는 보통 ‘DEBUG’ 로그를 사용한다. 그런데 운영 서버는 보통 요청이 아주 많기 때문에 로그가 많이 남는다. DEBUG 로그까지 모두 출력하게 되면 성능이나 디스크에 영향을 끼친다.
따라서 운영서버는 중요하다고 판단되는 INFO 로그 레벨을 사용한다.
그런데 서비스 운영 중 문제가 있어 급하게 DEBUG나 TRACE 로그를 남겨 확인해야 하는 경우 어떻게 할 수 있을까?
일반적으로 로깅 설정을 변경하고 서버를 재시작해야 한다. 매우 불편하다.
그러나 loggers 엔드포인트를 활용하면 애플리케이션을 재시작하지 않고 실시간으로 로그 레벨을 변경할 수 있다.
1
http://localhost:8080/actuator/loggers/hello.controller
Postman 같은 도구로 POST 요청을 위 url로 보낸다.
이 때 Body에 변경하고 싶은 레벨을 JSON 형태로 담아주면 된다.
1
2
3
{
"configuredLevel": "TRACE"
}
이렇게 하면 재시작을 하지 않고도 TRACE 레벨로 로그 레벨을 바꿀 수 있다.
단, 이런 부분들이 어디에 저장되는 부분은 아니므로 만약 이렇게 변경 후 서버를 재시작하면 해당 레벨은 유지되지 않는다. 기존 설정으로 돌아가게 된다.
httpexchanges 사용 (HTTP 요청 응답 기록)
HTTP 요청과 응답의 과거 기록을 확인하고 싶을 때 사용한다.
HttpExchangeRepository 인터페이스의 구현체를 빈으로 등록하면 httpexchanges 엔드포인트를 사용할 수 있다.
이 기능은 많이 사용하지는 않지만 개발 단계에서 종종 편할 수 있다.
스프링 부트는 기본으로 InMemoryHttpExchangeRepository 구현체를 제공한다.
1
2
3
4
public class InMemoryHttpExchangeRepository implements HttpExchangeRepository {
private int capacity = 100;
//...
}
- HTTP 요청이 올 때 마다 히스토리를 모은다. 100개까지 모으는 것을 알 수 있다.
- 메모리는 무한하지 않기 때문에 용량이 정해져 있다.
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootApplication
public class ActuatorApplication {
public static void main(String[] args) {
SpringApplication.run(ActuatorApplication.class, args);
}
@Bean
public InMemoryHttpExchangeRepository httpExchangeRepository() {
return new InMemoryHttpExchangeRepository();
}
}
log 컨트롤러의 GET 요청을 한 뒤 httpexchanges 엔드포인트를 확인해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
{
"exchanges": [
{
"timestamp": "2024-10-27T08:56:35.463521900Z",
"request": {
"uri": "http://localhost:8080/log",
"method": "GET",
"headers": {
"content-type": [
"application/json"
],
"user-agent": [
"PostmanRuntime/7.42.0"
],
"accept": [
"*/*"
],
"postman-token": [
"4bb21d51-8e9f-4348-863c-82c7a64096dc"
],
"host": [
"localhost:8080"
],
"accept-encoding": [
"gzip, deflate, br"
],
"connection": [
"keep-alive"
],
"content-length": [
"36"
]
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": [
"text/plain;charset=UTF-8"
],
"Content-Length": [
"2"
],
"Date": [
"Sun, 27 Oct 2024 08:56:35 GMT"
],
"Keep-Alive": [
"timeout=60"
],
"Connection": [
"keep-alive"
]
}
},
"timeTaken": "PT0.0875681S"
}
]
}
- url, headers, 응답 정보, 소요 시간 등등을 확인할 수 있다.
- 최대 요청이 넘어가면 과거 요청을 삭제한다.
- setCapacity() 메서드를 통해 저장하는 최대 요청 수를 변경할 수 있다.
참고로 이 기능은 매우 단순하고 기능에 제한이 많아 개발 단계에서만 활용하고, 실제 운영 서비스에서는 모니터링 툴이나 핀포인트(추천 - 네이버 오픈소스), Zipkin 같은 다른 기술을 사용하는 것이 좋다.
액츄에이터의 보안 문제
액츄에이터가 제공하는 기능들을 확인해봤다.
보면 우리의 애플리케이션 내부 정보를 너무 많이 노출하는 것을 볼 수 있다.
따라서 외부 인터넷 망이 공개된 곳에 액츄에이터의 엔드포인트를 공개하는 것은 보안상 좋은 방식이 아니다. 액츄에이터의 엔드포인트들은 외부 인터넷에서 접근이 불가능하게 막고, 내부에서만 접근 가능한 내부망을 사용하는 것이 안전하다.
- 액츄에이터를 다른 포트에서 실행
- 예를 들어 외부 인터넷 망을 통해 8080 포트에만 접근할 수 있고, 다른 포트는 내부 망에서만 접근할 수 있다면 액츄에이터에 다른 포트를 설정하면 된다.
- 액츄에이터의 기능을 애플리케이션 서버와는 다른 포트에서 실행하려면 아래 설정을 추가하면 된다.
- management.server.port=9292
- 액츄에이터 URL 경로에 인증 설정
- 포트를 분리하는 것이 어렵고 어쩔 수 없이 외부 인터넷 망을 통해 접근해야 한다면 /actuator 경로에 스프링 시큐리티 등을 통해 인증된 사용자만 접근 가능하도록 추가 개발을 할 수도 있다.
별개로 엔드포인트의 기본 경로를 변경하려면 아래와 같이 설정하면 된다.
1
2
3
management.endpoints.web.base-path="/manage"
# /actuator/{엔드포인트} 대신 /manage/{엔드포인트}로 변경된다.