[Spring Boot Core] 1. SpringApplication

2024. 10. 22. 16:33Spring/Basic

반응형

Spring Appplication

  1. startup 실패
  2. 지연 초기화
  3. 커스텀 배너
  4. SpringApplication 커스텀
  5. 애플리케이션 가용성
  6. 웹 환경
  7. application arguments 접근
  8. Application 또는 CommandLineRunner
  9. 애플리케이션 종료
  10. 관리자 기능
  11. 애플리케이션 시작 추적
    1. 시작 시간 분석
    1. 특정 startup event만 필터링하여 분석
    1. jq json processor 를 이용한 빈 초기화 기간 조회

SpringApplication 클래스는 main()메서드를 이용하여 편리하게 Spring 애플리케이션 부팅을 제공합니다.

SpringApplication.run 정적 메서드에게 위임하여 애플리케이션을 실행합니다.

@SpringBootApplication
public class SpringBootApp {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootApp.class, args);
	}

}

최초로 프로젝트를 생성한 후, 실행했을 때 아래와 같이 로그가 출력됩니다.

.   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/  ___)| |_)| | | | | || (_| |  ) ) ) )
'  |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/

:: Spring Boot ::                (v3.3.3)

2024-09-30T14:25:01.031+09:00  INFO 38382 --- [spring-boot] [  restartedMain] me.jiniworld.springboot.SpringBootApp    : Starting SpringBootApp using Java 22.0.2 with PID 38382 (/User/jini/jini_box/jinispaces/java/spring-playground/spring-boot/build/classes/java/main started by jini in /User/jini/jini_box/jinispaces/java/spring-playground)
2024-09-30T14:25:01.033+09:00  INFO 38382 --- [spring-boot] [  restartedMain] me.jiniworld.springboot.SpringBootApp    : The following 1 profile is active: "local"
2024-09-30T14:25:01.052+09:00  INFO 38382 --- [spring-boot] [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2024-09-30T14:25:01.052+09:00  INFO 38382 --- [spring-boot] [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2024-09-30T14:25:01.411+09:00  INFO 38382 --- [spring-boot] [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2024-09-30T14:25:01.418+09:00  INFO 38382 --- [spring-boot] [  restartedMain] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2024-09-30T14:25:01.418+09:00  INFO 38382 --- [spring-boot] [  restartedMain] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.28]
2024-09-30T14:25:01.435+09:00  INFO 38382 --- [spring-boot] [  restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2024-09-30T14:25:01.435+09:00  INFO 38382 --- [spring-boot] [  restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 383 ms
2024-09-30T14:25:04.900+09:00  INFO 38382 --- [spring-boot] [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2024-09-30T14:25:04.914+09:00  INFO 38382 --- [spring-boot] [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2024-09-30T14:25:04.919+09:00  INFO 38382 --- [spring-boot] [  restartedMain] me.jiniworld.springboot.SpringBootApp    : Started SpringBootApp in 4.048 seconds (process running for 9.333)

기본적으로 INFO 로깅메시지가 출력됩니다.

만일 로깅 설정을 직접 설정하고 싶다면, Logging 페이지를 참조하여 알맞게 커스텀 하면 됩니다.


로깅 정보의 맨 첫번째로는 spring 기본 배너와 함께, Spring Boot 의 메인 버전(spring-boot.formatted-version)이 출력되고,

그 다음에 애플리케이션 실행시 시작 정보 로깅을 출력합니다.
시작 정보 로깅에는 애플리케이션을 시작한 사용자에 대한 세부 정보 등이 포함되어있는데, 이 정보를 끄고 싶다면

spring.main.log-startup-info 값을 false 로 설정하면 됩니다.

spring:
  main:
    log-startup-info: false

1. startup 실패

애플리케이션 시작에 실패했을 때, 등록된 FailureAnalyzers는 오류 메시지와 함께 문제 해결을 위한 조치 방안을 제공해줍니다.

아래는 8080포트를 이용하여 애플리케이션을 시작했으나, 다른 곳에서 이미 해당 포트를 사용하고 있을 경우에 대한 에러 로그입니다.

***************************
APPLICATION FAILED TO START
***************************

Description:

Web server failed to start. Port 8080 was already in use.

Action:

Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.

Disconnected from the target VM, address: '127.0.0.1:51544', transport: 'socket'

Process finished with exit code 0

Spring Boot에서는 다양한 FailureAnalyzers 구현체를 제공하고 있으며, 직접 커스텀한 FailureAnalyzers 구현체를 이용할 수 도 있습니다. 참고


2. 지연 초기화

Spring Application은 지연 초기화(Lazy Initialization)를 허용합니다.

지연 초기화가 활성화 되어있을 경우, bean은 애플리케이션 시작시가 아닌, 필요시에 생성됩니다.
빈 생성이 애플리케이션 실행시에 생성되는 것이 아니기 떄문에, 애플리케이션 시작에 걸리는 시간을 줄일 수 있습니다.

  • 웹 애플리케이션에서 지연 초기화를 활성화할 경우, http 요청을 받기 전까지 웹 관련 bean이 초기화 되지 않습니다.

지연 초기화를 활성화시킬 경우, 문제가 있는 빈이 사용되는 시점에 초기화 되기 때문에 애플리케이션의 문제점 발견이 늦어질 수 있다는 단점이 있습니다.

또, 시작시 초기화되는 빈 뿐안아니라 이후에 초기화될 모든 빈들을 수용할만큼 JVM 메모리가 충분한지 체크하기가 어려운 점이 있어 기본적으로 지연 초기화를 활성화 하지 않습니다.
지연 초기화 기능을 활성화할거라면 사전에 JVM 힙 크기를 미세 조정하는 것이 좋습니다.

지연 초기화를 활성화하고 싶다면 spring.main.lazy-initialization 값을 true 로 설정하면 됩니다. (기본값: false)

spring:
  main:
    lazy-initialization: true

기본적으로 지연초기화를 활성화한 상태에서, 특정 빈은 지연초기화를 비활성화하고 싶다면, 해당 빈에 @Lazy(false) 설정을 추가해주면 됩니다.


3. 커스텀 배너

[Spring Boot] Custom Banners 참고


4. SpringApplication 커스텀

기본 SpringApplication에서 커스텀을 하고 싶다면, SpringApplication 인스턴스를 직접 생성하여 커스텀하여 실행시킬 수 있습니다.



@SpringBootApplication
public class SpringBootApp {

	public static void main(String[] args) {
		SpringApplication app = new SpringApplication(SpringBootApp.class);
		app.setBannerMode(Banner.Mode.OFF);
		app.run(args);
	}

}

SpringApplicationBuilder를 이용하여 빌더 패턴으로 SpringApplication 커스텀이 가능하고,

@SpringBootApplication
public class SpringBootApp {

	public static void main(String[] args) {
		new SpringApplicationBuilder().sources(SpringBootApp.class)
			.bannerMode(Banner.Mode.OFF)
			.run(args);
	}

}

.child() 메서드로 parent-child 계층 구조 설정도 가능합니다.


5. 애플리케이션 가용성

Spring 애플리케이션은 플랫폼에 배포할때 인프라를 사용한 플랫폼 가용성에 대한 정보를 알려줍니다. (쿠버네티스의 Probes와 같은)

Spring Boot에는 Liveness, readiness 가용성 상태에 대한 기본적인 지원이 포함되어있습니다.
만약 Spring Boot의 actuator를 사용하는 경우, 이러한 상태는 health endpoint group으로 노출됩니다.
또, ApplicationAvailability 인터페이스를 통해 당신의 빈에 주입하여 가용성 상태를 얻을 수 있습니다.

5.1. Liveness status

애플리케이션의 Liveness(활성) 상태는 내부상태가 올바르게 작동될 수 있거나 실패중인 상태에서 스스로 복구할 수 있는지에 대해 알려줍니다.

Liveness 상태가 문제가 있다면, 애플리케이션이 복구할 수 없는 상태에 있다는 뜻으로, 인프라가 애플리케이션을 새로 시작해야 한다는 의미입니다.

memo

일반적으로, Liveness 상태를 외부 검사 기반으로 하는 것을 권장하지 않습니다.
만일, 외부 검사 기반으로 설정한다면, 외부 시스템(DB, 외부 web api, 외장 캐시)의 실패로 인해 대규모 재시작이나 플랫폼 전반에 걸친 연쇄 실패를 유발할 수 있습니다.

Spring Boot의 내부상태는 대부분 ApplicationContext 로 표현됩니다.
만약 애플리케이션 컨텍스트가 성공적으로 시작된다면, Spring Boot는 애플리케이션을 유효한 상태로 가정하고, 컨텍스트가 새로 고쳐질 때 애플리케이션은 활성 상태로 간주합니다.


5.2. Readiness status

애플리케이션의 Readiness(준비) 상태는 애플리케이션이 트래픽을 처리할 준비가 되었는지 여부를 알려줍니다.

실패한 readiness status는 플랫폼에게 현재 애플리케이션으로 트래픽을 라우팅해서는 안된다는 것을 알려줍니다.

이것을 일반적으로 시작 중에 발생됩니다.
CommandLineRunnerApplicationRunner 컴포넌트가 처리되는 동안 또는 애플리케이션이 너무 바빠서 추가 트래픽을 처리할 수 없다고 판단되는 경우에 언제든지 발생합니다.

애플리케이션은 CommandLineRunner가 실행되자마자 준비된 것으로 간주합니다.

note

시작중에 실행되어야하는 작업들은 @PostContruct 와같은 Spring component lifecycle callback 대신 CommandLineRunner 및 ApplicationRunner 컴포넌트에서 실행해야 합니다.


5.3. 애플리케이션 가용성 상태 관리

애플리케이션 구성 요소는 언제든지 ApplicationAvailability 인터페이스를 통해 현재 가용성 상태를 검색할 수 있습니다.

애플리케이션은 상태 업데이트를 수신하거나 애플리케이션 상태를 업데이트하고자할 때가 많습니다.

쿠버네티스의 exec Probe가 아래의 파일을 볼 수 있도록 애플리케이션의 Readiness 상태를 내보낼 수 있습니다.


6. 웹 환경

SpringApplication 은 알맞는 ApplicationContext 타입을 생성하려고 시도합니다.

  • 만일 Spring MVC 를 사용하는 경우에는 AnnotationConfigServletWebServerApplicationContext 가 사용되고
  • Spring MVC 는 없고 Spring WebFlux 가 있다면 AnnotationConfigReactiveWebServerApplicationContext 가 사용됩며
  • 그 외의 경우에는 AnnotationConfigApplicationContext 가 사용됩니다.

즉, Spring MCP 와 Spring WebFlux의 WebClient 를 사용하고 있다면, 기본적으로 Spring MVC가 사용됩니다.

만일, 애플리케이션 타입을 변경하고 싶다면 setWebApplicationType(웹애플리케이션타입) 으로 override 하면 됩니다.

또는 setApplicationContextFactory() 로도 설정 가능합니다.

tip

JUnit 테스트 내에서 SpringApplication 을 사용하는 경우에는, setWebApplicationType(WebApplicationType.NONE) 을 호출하는 것을 권장합니다.


7. application arguments 접근

만약 _SpringApplication.run(...)` 으로 전달된 application arguments 에 접근해야한다면, org.springframework.boot.ApplicationArguments 빈을 통해 해당 값을 주입할 수 있습니다.

@RequiredArgsConstructor
@RequestMapping("/spring-application")
@RestController
class SpringApplicationController {

    private final ApplicationArguments applicationArguments;

    @GetMapping
    public Data applicationArguments() {
        Map<String, Object> options = new HashMap<>();
        applicationArguments.getOptionNames().forEach(optionName -> options.put(optionName, applicationArguments.getOptionValues(optionName)));

        return new Data(List.of(applicationArguments.getSourceArgs()), options, applicationArguments.getNonOptionArgs());
    }

    record Data(List<String> sourceArgs, Map<String, Object> options, List<String> nonOptionArgs) {
    }
}

java -jar spring-boot/build/libs/spring-boot-0.0.1.jar --debug --tag=test local



{
  "sourceArgs": [
    "--debug",
    "--name=jini",
    "local"
  ],
  "options": {
    "debug": [],
    "name": [
      "jini"
    ]
  },
  "nonOptionArgs": [
    "local"
  ]
}

8. Application 또는 CommandLineRunner 사용

SpringApplication 이 시작된 이후 특정 코드를 실행하고 싶다면, ApplicationRunner 또는 CommandLineRunner 인터페이스를 구현해야합니다.

두 인터페이스 모두 동일한 방식으로 동작되며, 하나의 run 메서더드를 제공하고 있으며, 이는 SpringApplication.run() 이 완료되기 직전에 호출됩니다.
이러한 작업은 보통 애플리케이션이 시작된 후 실질적인 트래픽을 받기 전에 실행되어야하는 작업에 적합합니다.

@Slf4j
@SpringBootApplication
public class SpringBootApp {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootApp.class, args);
	}

	@Bean
	CommandLineRunner commandLineRunner() {
		return args -> log.info("commandLineRunner - args : {}", Arrays.toString(args));
	}

}

웹서버가 실행 된후, 위의 commandLineRunner() 빈 실행에 의해 로그에 commandLineRunner - args : [--debug, --tag=test, local] 가 찍혔습니다.

2024-10-07T13:44:09.486+09:00  INFO 82127 --- [spring-boot] [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2024-10-07T13:44:09.488+09:00 DEBUG 82127 --- [spring-boot] [  restartedMain] inMXBeanRegistrar$SpringApplicationAdmin : Application Admin MBean registered with name 'org.springframework.boot:type=Admin,name=SpringApplication'
2024-10-07T13:44:09.489+09:00 DEBUG 82127 --- [spring-boot] [  restartedMain] o.s.b.d.livereload.LiveReloadServer      : Starting live reload server on port 35729
2024-10-07T13:44:09.490+09:00  INFO 82127 --- [spring-boot] [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2024-10-07T13:44:09.551+09:00 DEBUG 82127 --- [spring-boot] [  restartedMain] o.s.b.a.ApplicationAvailabilityBean      : Application availability state LivenessState changed to CORRECT
2024-10-07T13:44:09.552+09:00  INFO 82127 --- [spring-boot] [  restartedMain] me.jiniworld.springboot.SpringBootApp    : commandLineRunner - args : [--debug, --tag=test, local]
2024-10-07T13:44:09.553+09:00 DEBUG 82127 --- [spring-boot] [  restartedMain] o.s.boot.devtools.restart.Restarter      : Creating new Restarter for thread Thread[#1,main,5,main]
2024-10-07T13:44:09.553+09:00 DEBUG 82127 --- [spring-boot] [  restartedMain] o.s.boot.devtools.restart.Restarter      : Immediately restarting application
2024-10-07T13:44:09.553+09:00 DEBUG 82127 --- [spring-boot] [  restartedMain] o.s.boot.devtools.restart.Restarter      : Starting application me.jiniworld.springboot.SpringBootApp with URLs []
2024-10-07T13:44:09.553+09:00 DEBUG 82127 --- [spring-boot] [  restartedMain] o.s.b.a.ApplicationAvailabilityBean      : Application availability state ReadinessState changed to ACCEPTING_TRAFFIC

8.1. CommandLineRunner 빈의 실행 순서 설정하기

ApplicationRunner, CommandLinerRunner 빈을 여러개 정의하고, Ordered 인터페이스의 구현이나 @Order 애너테이션을 이용하여 실행 순서를 설정할 수 있습니다.

@Slf4j
@SpringBootApplication
public class SpringBootApp {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootApp.class, args);
	}

	@Order(1)
	@Bean
	CommandLineRunner commandLineRunner() {
		return args -> log.info("commandLineRunner bean : {}", Arrays.toString(args));
	}

}
@Slf4j
@Component
public class MyCommandLineRunner implements CommandLineRunner, Ordered {

    @Override
    public void run(String... args) {
        log.info("MyCommandLineRunner: {}", Arrays.toString(args));
    }

    @Override
    public int getOrder() {
        return 2;
    }
}

2024-10-07T14:47:32.154+09:00  INFO 90766 --- [spring-boot] [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2024-10-07T14:47:32.156+09:00 DEBUG 90766 --- [spring-boot] [  restartedMain] inMXBeanRegistrar$SpringApplicationAdmin : Application Admin MBean registered with name 'org.springframework.boot:type=Admin,name=SpringApplication'
2024-10-07T14:47:32.158+09:00 DEBUG 90766 --- [spring-boot] [  restartedMain] o.s.b.d.livereload.LiveReloadServer      : Starting live reload server on port 35729
2024-10-07T14:47:32.158+09:00  INFO 90766 --- [spring-boot] [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2024-10-07T14:47:32.215+09:00 DEBUG 90766 --- [spring-boot] [  restartedMain] o.s.b.a.ApplicationAvailabilityBean      : Application availability state LivenessState changed to CORRECT
2024-10-07T14:47:32.217+09:00  INFO 90766 --- [spring-boot] [  restartedMain] me.jiniworld.springboot.SpringBootApp    : commandLineRunner bean : [--debug, --tag=test, local]
2024-10-07T14:47:32.217+09:00  INFO 90766 --- [spring-boot] [  restartedMain] m.j.springboot.MyCommandLineRunner       : MyCommandLineRunner: [--debug, --tag=test, local]
2024-10-07T14:47:32.217+09:00 DEBUG 90766 --- [spring-boot] [  restartedMain] o.s.boot.devtools.restart.Restarter      : Creating new Restarter for thread Thread[#1,main,5,main]
2024-10-07T14:47:32.217+09:00 DEBUG 90766 --- [spring-boot] [  restartedMain] o.s.boot.devtools.restart.Restarter      : Immediately restarting application
2024-10-07T14:47:32.217+09:00 DEBUG 90766 --- [spring-boot] [  restartedMain] o.s.boot.devtools.restart.Restarter      : Starting application me.jiniworld.springboot.SpringBootApp with URLs []
2024-10-07T14:47:32.217+09:00 DEBUG 90766 --- [spring-boot] [  restartedMain] o.s.b.a.ApplicationAvailabilityBean      : Application availability state ReadinessState changed to ACCEPTING_TRAFFIC

9. 애플리케이션 종료

SpringApplication 은 종료 시 ApplicationContext 가 종료시 graceful 하게 꺼지도록 JVM에 shutdown hook를 등록합니다.
DisposableBean 인터페이스나 @PreDestory 애너테이션과 같은 표준 Spring lifecycle 콜백을 사용할 수 있습니다.

만약 SpringApplication.exit()가 호출될 때 특별한 exit code를 반환하기를 원한다면 org.springframework.boot.ExitCodeGenerator 인터페이스를 구현한 빈을 활용하면 됩니다.

ExitCodeGenerator 가 2개 이상인 경우, 0이 아닌 exit code가 사용되며,

@SpringBootApplication
public class SpringBootApp {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootApp.class, args);
	}

	@Bean
	public ExitCodeGenerator exitCodeGenerator() {
		return () -> 42;
	}

}

ExitCodeEvent 에 대한 EventListener를 만듭니다.

@Slf4j
@Component
public class ExitCodeEventListener {

    @EventListener
    public void handleExitCodeEvent(ExitCodeEvent event) {
        log.info("ExitCodeEvent >>> Exit code: {}", event.getExitCode());
    }

}

data 값이 q 일 경우, 종료시키는 api를 추가하였습니다.

public record SimpleData<T> (T data) { }
@RequiredArgsConstructor
@RequestMapping("/exit-code")
@RestController
class ExitCodeController {

    private final ApplicationContext applicationContext;
    private final ExitCodeGenerator exitCodeGenerator;

    @PostMapping
    public String exitCode(@RequestBody SimpleData<String> req) {
        if ("q".equals(req.data())) {
            SpringApplication.exit(applicationContext, exitCodeGenerator);
        }
        return req.data();
    }

}

HTTPie로 api 테스트를 한 결과입니다.

http POST :8080/exit-code data=q
http: error: ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')) while doing a POST request to URL: http://localhost:8080/exit-code

10. 관리자 기능

spring.application.admin.enabled 프로퍼티 속성을 활성화하여 관리자 기능을 활성화 시킬 수 있습니다.

spring:
  application:
    admin:
      enabled: true

javac 명령어가 설치된 디렉토리에 들어가보면 jconsole 명령어가 있다. 이 명령어를 실행하면 Java Monitoring & Management Console 을 실행할 수 있습니다.

which javac
/opt/homebrew/opt/openjdk/bin/javac
ls /opt/homebrew/opt/openjdk/bin/
jar         java        javadoc     jcmd        jdb         jdeps       jhsdb       jinfo       jmap        jpackage    jrunscript  jstack      jstatd      keytool     serialver
jarsigner   javac       javap       jconsole    jdeprscan   jfr         jimage      jlink       jmod        jps         jshell      jstat       jwebserver  rmiregistry
./jconsole




힙 메모리 사용량, CPU 사용량, 로딩된 클래스 수 등 실행중인 애플리케이션에 대한 다양한 정보를 확인할 수 있습니다.




11. 애플리케이션 시작 추적

애플리케이션을 시작하는 동안 SpringApplicationApplicationContext 는 애플리케이션 생애주기동안 bean lifecycle 이나 애플리케이션 이벤트 처리 와 관련된 다양한 작업을 수행합니다.
_ApplicationStartup_을 사용하면 스프링프레임워크를 사용하여 StartupStep 객체로 애플리케이션 시작 시퀀스를 추적할 수 있습니다.


일반적으로, 기본으로 설정되어있는 DefaultApplicationStartup 를 사용하지만, (별도의 옵션이 설정되어있지 않음)

@SpringBootApplication
public class SpringBootApp {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootApp.class, args);
	}

}

SpringApplication 인스턴스를 설정할 때, setApplicationStartup 로 ApplicationStartup 구현을 설정할 수 있습니다.

ApplicationStartup 구현체로는 BufferingApplicationStartupFlightRecorderApplicationStartup 가 있고, 이 구현체들을 사용할 경우, Spring 애플리케이션이 시작되는 동안, 각 단계별로 걸린 시간들을 상세히 파악할 수 있습니다.

11.1. 시작 시간 분석

@SpringBootApplication
public class SpringBootApp {

	public static void main(String[] args) {
		SpringApplication application = new SpringApplication(SpringBootApp.class);
		application.setApplicationStartup(new BufferingApplicationStartup(2048));
		application.run(args);
	}

}

api 를 실행해봅니다. (아래는 curl로 조회한 결과입니다.)

curl http://localhost:8080/actuator/startup | json_pp
{
  "springBootVersion": "3.3.3",
  "timeline": {
    "startTime": "2024-10-22T05:00:48.922274Z",
    "events": [
      {
        "endTime": "2024-10-22T05:00:48.933747Z",
        "duration": "PT0.011367S",
        "startTime": "2024-10-22T05:00:48.922380Z",
        "startupStep": {
          "name": "spring.boot.application.starting",
          "id": 0,
          "tags": [
            {
              "key": "mainApplicationClass",
              "value": "me.jiniworld.springboot.SpringBootApp"
            }
          ]
        }
      },
      ...
      {
        "endTime": "2024-10-22T05:00:50.013900Z",
        "duration": "PT0.003504S",
        "startTime": "2024-10-22T05:00:50.010396Z",
        "startupStep": {
          "name": "spring.boot.application.started",
          "id": 260,
          "tags": []
        }
      },
      {
        "endTime": "2024-10-22T05:00:50.014133Z",
        "duration": "PT0.000103S",
        "startTime": "2024-10-22T05:00:50.014030Z",
        "startupStep": {
          "name": "spring.beans.instantiate",
          "id": 265,
          "tags": [
            {
              "key": "beanName",
              "value": "myCommandLineRunner"
            },
            {
              "key": "beanType",
              "value": "interface org.springframework.boot.Runner"
            }
          ]
        }
      },
      {
        "endTime": "2024-10-22T05:00:50.014428Z",
        "duration": "PT0.000279S",
        "startTime": "2024-10-22T05:00:50.014149Z",
        "startupStep": {
          "name": "spring.beans.instantiate",
          "id": 267,
          "tags": [
            {
              "key": "beanName",
              "value": "springBootApp"
            }
          ],
          "parentId": 266
        }
      },
      ...
      {
        "endTime": "2024-10-22T05:01:07.532088Z",
        "duration": "PT0.001044S",
        "startTime": "2024-10-22T05:01:07.531044Z",
        "startupStep": {
          "name": "spring.beans.instantiate",
          "id": 293,
          "tags": [
            {
              "key": "beanName",
              "value": "flashMapManager"
            },
            {
              "key": "beanType",
              "value": "interface org.springframework.web.servlet.FlashMapManager"
            }
          ]
        }
      }
    ]
  }
}

11.2. 특정 startup event만 필터링하여 분석

startupStemp의 특정 name만 필터링해서 보여주도록 하고 싶다면 ApplicationStartup 구현체에 startupStep에 아래와 같은 match를 설정해주면 됩니다.

@SpringBootApplication
public class SpringBootApp {

	public static void main(String[] args) {
		SpringApplication application = new SpringApplication(SpringBootApp.class);
		var applicationStartup = new BufferingApplicationStartup(2048);
		applicationStartup.addFilter(startupStep -> startupStep.getName().matches("spring.beans.instantiate"));
		application.setApplicationStartup(applicationStartup);
		application.run(args);
	}

}

api 를 실행해봅니다. (아래는 HTTPie로 조회한 결과입니다.)

http :8080/actuator/startup
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Tue, 22 Oct 2024 05:15:39 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
  "springBootVersion": "3.3.3",
  "timeline": {
    "startTime": "2024-10-22T05:12:04.400145Z",
    "events": [
      {
        "endTime": "2024-10-22T05:12:04.578045Z",
        "duration": "PT0.000673S",
        "startTime": "2024-10-22T05:12:04.577372Z",
        "startupStep": {
          "name": "spring.beans.instantiate",
          "id": 7,
          "tags": [
            {
              "key": "beanName",
              "value": "org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory"
            }
          ],
          "parentId": 6
        }
      },
      ...
      {
        "endTime": "2024-10-22T05:12:05.718332Z",
        "duration": "PT0.020823S",
        "startTime": "2024-10-22T05:12:05.697509Z",
        "startupStep": {
          "name": "spring.beans.instantiate",
          "id": 284,
          "tags": [
            {
              "key": "beanName",
              "value": "viewResolver"
            }
          ]
        }
      },
      {
        "endTime": "2024-10-22T05:12:05.719134Z",
        "duration": "PT0.000787S",
        "startTime": "2024-10-22T05:12:05.718347Z",
        "startupStep": {
          "name": "spring.beans.instantiate",
          "id": 293,
          "tags": [
            {
              "key": "beanName",
              "value": "flashMapManager"
            },
            {
              "key": "beanType",
              "value": "interface org.springframework.web.servlet.FlashMapManager"
            }
          ]
        }
      }
    ]
  }
}

11.3. jq json processor 를 이용한 빈 초기화 기간 조회

사전에 jq를 다운받습니다.
mac OS의 경우, brew로 설치가 가능합니다.

brew install jq

다른 OS에서 설치해야한다면. Download jq를 참고해주세요.


jq를 이용하여 startupStep.namespring.beans.instantiate 인 항목들에 대해서 startupStep - tags[0] - value 와 duration 값만 duration 내림차순으로 조회합니다.

아래의 결과에는 가장 오래걸린 빈 초기화가 0.05초이지만, 만약 빈의 초기화가 오래걸리는 항목이 조회된다면, 애플리케이션의 개선점을 찾는데에 도움이 될 수 있습니다.

curl 'http://localhost:8080/actuator/startup' -X POST \
| jq '[.timeline.events
 | sort_by(.duration) | reverse[]
 | select(.startupStep.name | match("spring.beans.instantiate"))
 | {beanName: .startupStep.tags[0].value, duration: .duration}]'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 77332    0 77332    0     0   600k      0 --:--:-- --:--:-- --:--:--  604k
[
  {
    "beanName": "tomcatServletWebServerFactory",
    "duration": "PT0.051436S"
  },
  {
    "beanName": "jmxMBeanExporter",
    "duration": "PT0.045843S"
  },
  {
    "beanName": "webMvcObservationFilter",
    "duration": "PT0.028073S"
  },
  {
    "beanName": "requestMappingHandlerMapping",
    "duration": "PT0.025103S"
  },
  ...
  {
    "beanName": "org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration$ServletWebConfiguration",
    "duration": "PT0.000052S"
  },
  {
    "beanName": "org.springframework.boot.actuate.autoconfigure.startup.StartupEndpointAutoConfiguration",
    "duration": "PT0.000051S"
  }
]

ApplicationStartup을 활용하여 Spring Boot 애플리케이션의 startup메트릭에 대해 확인할 수 있었습니다.

다만, ApplicationStartup은 애플리케이션의 startup 하는 동안에만 사용할 수 있는 부분으로, 애플리케이션 계측을 위한 Java 프로파일러 및 메트릭 수집 프레임워크를 대체하지는 않습니다.


12. Virtual Thread

Java 21 이상 버전을 사용하는 경우, spring.threads.virtual.enabled 속성을 이용하여 가상 스레드를 활성화할 수 있습니다.

이 옵션을 사용하기 전에, official Java virtual threads Docs를 읽어보는것을 권장합니다.

어떤 경우, "고정된 가상 스레드"로 인하여 애플리케이션의 처리량이 낮아질 수 있습니다.
위의 공식문서에서는 JDK Flight Recorder 또는 jcmd CLI로 이러한 경우를 감지하는 방법도 설명합니다.

note

virtual thread는 전용 스레드 풀이 아닌 JVM 전체 플랫폼 스레드 풀에서 예약됩니다.
따라서, virtual thread 가 활성화되면 thread pool을 구성하는 속성은 더이상 효과가 없어 집니다.


Warning

가상 스레드는 데몬 스레드입니다.
모든 스레드가 데몬 스레드일 경우, JVM이 종료됩니다.
이러한 성질을 유의하지 않으면 애플리케이션 운영 중 문제점을 만날 수 있습니다.

예를 들어, 애플리케이션을 유지하기 위해 @Scheduled Bean에 의존하는 경우 이러한 동작이 문제가 될 수 있습니다.
가상 스레드가 활성화된 상태에서는, 스케줄러 스레드가 데몬 스레드가 되기 때문에 JVM을 활성 상태로 유지하지 않습니다.
이는 스케줄링에 영향을 미칠 뿐만 아니라 다른 기술에도 영향을 미칠 수 있습니다.

모든 경우에 JVM을 계속 실행하기 위해서는 spring.main.keep-alive 속성을 true로 설정하면 됩니다. 이렇게 설정하면 모든 스레드가 가상 스레드인 경우에도 JVM이 활성상태로 유지됩니다.


++

  • Spring Boot Docs 3.3.4
  • Core Features - SpringApplication
728x90
반응형