[Spring Docs] DevTools

2024. 10. 4. 16:45Spring/Basic

반응형
  1. Developer Tools
  2. Developer Tools 프로퍼티 기본값
  3. log-request-details 프로퍼티를 이용한 헤더 로깅
  4. Hot swapping
    1. 정적 리소스 리로드
    2. 컨테이너 재시작 없이 템플릿 뷰 로드
    3. 빠른 애플리케이션 재시작
    4. 컨테이너 재시작 없이 java 클래스 리로드
  5. 자동 restart
    1. Restart vs Reload
    2. condition evalutaion에서 변경사항 로깅
    3. 리소스 제외
    4. restart 비활성화
    5. 알려져있는 제한사항
  6. 애플리케이션의 프로덕션 배포를 위한 패키징

1. Developer Tools?

devTools 는 개발의 편의성을 위한 여러 기능들을 포함하고 있습니다.

gradle 환경에서는 아래와 같은 의존성을 추가해주면 되고,

dependencies {
	developmentOnly("org.springframework.boot:spring-boot-devtools")
}

Maven 을 사용하고 있다면 아래와 같은 의존성을 추가하면 됩니다.

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-devtools</artifactId>
		<optional>true</optional>
	</dependency>
</dependencies>

위와 같이 의존성을 추가했을 경우, 멀티모듈 프로젝트 상에서 다른 모듈에 DevTools가 전이 적용되는 것을 방지할 수 있습니다.

devTools 는 개발의 편의를 위해 제공되는 기능들로, 보안적인 이유로 production 환경에서는 사용하는 것을 권장하지 않습니다.
참고로 java -jar로 실행된 애플리케이션이나 특별한 클래스 로더를 통해 시작된 로 패키징된 애플리케이션의 경우, production application 으로 간주되어 자동으로 비활성화 됩니다.

만일 강제로 devTools를 활성화시키고 싶다면 -Dspring.devtools.restart.enabled=true 옵션을 설정하여 애플리케이션을 실행하면 되지만, 보안에 취약하기 때문에 권장하지 않습니다.


2. Developer Tools 프로퍼티 기본값

Spring Boot에서 지원하는 다양한 라이브러리들에서는 성능 개선을 위해 캐시를 사용합니다.

  • 예시 1. 예를들면 템플릿 엔진에서 캐시를 사용하여 화면으로 보여줄 템플릿을 캐시합니다. 템플릿 파일을 캐시할 경우, 코드가 수정될 때마다 매번 코드를 분석하지 않습니다.
  • 예시 2. Spring MVC에서 정적 리소스를 제공할 때에 response에 HTTP 캐싱 헤더를 추가

캐싱 기능은 production 환경에서는 성능 개선에 매우 좋지만, 개발 중에는 수정사항을 바로 반영하지 않기 때문에 개발에 역효과를 줍니다.

이러한 이유로 devTools에서는 기본적으로 캐싱 옵션을 비활성화합니다.

아래의 값들은 devTools 를 사용할 경우 적용될 프로퍼티 기본값들입니다.
만일, 아래의 기본값을 사용하고 싶지 않다면 spring.devtools.add-properties 하위에 프로퍼티를 재설정 해주면 됩니다.

  • server.error.include-binding-errors
    • always
  • server.error.include-message
    • always
  • server.error.include-stacktrace
    • always
  • server.servlet.jsp.init-parameters.development
    • TRUE
  • server.servlet.session.persistent
    • TRUE
  • spring.docker.compose.readiness.wait
    • only-if-started
  • spring.freemarker.cache
    • FALSE
  • spring.graphql.graphiql.enabled
    • TRUE
  • spring.groovy.template.cache
    • FALSE
  • spring.h2.console.enabled
    • TRUE
  • spring.mustache.servlet.cache
    • FALSE
  • spring.mvc.log-resolved-exception
    • TRUE
  • spring.reactor.netty.shutdown-quiet-period
    • 0s
  • spring.template.provider.cache
    • FALSE
  • spring.thymeleaf.cache
    • FALSE
  • spring.web.resources.cache.period
    • 0
  • spring.web.resources.chain.cache
    • FALSE

Spring MVC/WebFlux 애플리케이션 개발하는 동안에는 web 그룹에 대한 logging 레벨을 DEBUG로 활성화하는 것을 권장합니다.
web 그룹에 대한 로깅레벨을 DEBUG로 설정할 경우, request, request를 처리하는 핸들러, response 및 세부 정보에 대한 상세 정보를 확인할 수 있습니다.
단, request 세부정보에는 민감 정보가 들어있을 수 있기 때문에,


3. log-request-details 프로퍼티를 이용한 헤더 로깅

org.springframework.web 패키지의 로깅레벨을 trace 로 설정하면 웹 요청과 처리하는 핸들러, 응답 결과 및 세부 정보를 출력할 수 있습니다.

기본적으로 DispatcherServlet 에서 request에 대한 parameters, headers와 response의 headers 정보는 마스킹되어 출력되지 않습니다.
만일, 개발 중에 세부정보를 확인하고 싶다면 spring.mvc.log-request-details 프로퍼티 값을 true로 설정해주면 관련 정보를 로그로 출력할 수 있습니다.
(단, payload 값은 노출되지 않습니다.)

application.yml 을 아래와 같이 설정한 후 로깅 출력값을 확인해봅시다.

spring:
  application:
    name: spring-boot

  mvc:
    log-request-details: true

logging:
  level:
    root: info
    web: debug
    org.springframework.web: trace

3.1. spring.mvc.log-request-details: true

2024-09-26T10:05:24.404+09:00 TRACE 75476 --- [spring-boot] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : POST "/hello?version=2", parameters={version:[2]}, headers={user-agent:[vscode-restclient], content-type:[application/json], accept-encoding:[gzip, deflate], content-length:[64], cookie:[SESSION=MjczMGI1YTUtZWU0YS00MDNjLWFiM2MtYTljMDQ4ODI5ZTQ2], host:[localhost:8080], connection:[keep-alive]} in DispatcherServlet 'dispatcherServlet'
2024-09-26T10:05:24.410+09:00 TRACE 75476 --- [spring-boot] [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to me.jiniworld.springboot.TttController#hello(User)
2024-09-26T10:05:24.447+09:00 TRACE 75476 --- [spring-boot] [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Read "application/json;charset=UTF-8" to [User(2): jini]
2024-09-26T10:05:24.450+09:00 TRACE 75476 --- [spring-boot] [nio-8080-exec-1] o.s.web.method.HandlerMethod             : Arguments: [User(2): jini]
2024-09-26T10:05:24.453+09:00 DEBUG 75476 --- [spring-boot] [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/plain', given [*/*] and supported [text/plain, */*, application/json, application/*+json]
2024-09-26T10:05:24.453+09:00 TRACE 75476 --- [spring-boot] [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Writing ["jini, jiniworld.me"]
2024-09-26T10:05:24.455+09:00 TRACE 75476 --- [spring-boot] [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerAdapter : Applying default cacheSeconds=-1
2024-09-26T10:05:24.455+09:00 TRACE 75476 --- [spring-boot] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : No view rendering, null ModelAndView returned.
2024-09-26T10:05:24.456+09:00 DEBUG 75476 --- [spring-boot] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed 200 OK, headers={Content-Type:[text/plain;charset=UTF-8], Content-Length:[18], Date:[Thu, 26 Sep 2024 01:05:24 GMT], Keep-Alive:[timeout=60], Connection:[keep-alive]}

3.2. spring.mvc.log-request-details: false

2024-09-26T10:13:15.843+09:00 TRACE 75731 --- [spring-boot] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : POST "/hello?version=2", parameters={masked}, headers={masked} in DispatcherServlet 'dispatcherServlet'
2024-09-26T10:13:15.847+09:00 TRACE 75731 --- [spring-boot] [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to me.jiniworld.springboot.TttController#hello(User)
2024-09-26T10:13:15.884+09:00 TRACE 75731 --- [spring-boot] [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Read "application/json;charset=UTF-8" to [User(2): jini]
2024-09-26T10:13:15.887+09:00 TRACE 75731 --- [spring-boot] [nio-8080-exec-1] o.s.web.method.HandlerMethod             : Arguments: [User(2): jini]
2024-09-26T10:13:15.890+09:00 DEBUG 75731 --- [spring-boot] [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/plain', given [*/*] and supported [text/plain, */*, application/json, application/*+json]
2024-09-26T10:13:15.890+09:00 TRACE 75731 --- [spring-boot] [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Writing ["jini, jiniworld.me"]
2024-09-26T10:13:15.892+09:00 TRACE 75731 --- [spring-boot] [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerAdapter : Applying default cacheSeconds=-1
2024-09-26T10:13:15.892+09:00 TRACE 75731 --- [spring-boot] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : No view rendering, null ModelAndView returned.
2024-09-26T10:13:15.893+09:00 DEBUG 75731 --- [spring-boot] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed 200 OK, headers={masked}

4. Hot swapping

Spring Boot는 JVM의 hot swap기능을 지원합니다.
hot swap은 코드 변경사항이 감지되면 애플리케이션을 자동으로 로드하는 기능으로, 클래스파일의 변동사항을 핫스왑하거나 정적 리소스 파일을 리로드 하는 기능을 제공합니다.

IntelliJ 와같은 IDE를 이용하여 개발하는 중에 활용하여 개발 능률 향상 목적으로 사용합니다.

IntelliJ 를 사용하는 경우, 앱 실행에 대한 구성설정에서 관련 설정을 할 수 있습니다.


On 'Update' action, On frame deactivation 옵션 설정을 변경하여 핫스왑 관련 기능을 사용할 수 있는데
설정값에 따라 컨테이너의 재시작 없이 정적 리소스와 클래스의 리로드 하여 개발의 편의성을 올려 줍니다.






4.1. 정적 리소스 리로드

spring boot에서는 hot reload를 위한 몇가지 옵션을 제공합니다.
그 중 가장 추천하는 방식은, spring-boot-devtools 를 활용하여 빠른 애플리케이션 재시작을 이용하여 개발 능률을 향상시키는 것입니다.

devtools 는 classpath에서 변경사항을 모니터링하여 변경사항이 있을 시 작동합니다.

정적 리소스 변경사항의 적용을 위해서는, 프로젝트의 build 과정이 필수이지만, default restart exclusions에 의해 정적 리소스는 변경사항이 생겨도 재시작을 하지 않습니다. (다만 livereload는 작동됩니다.)

spring:
  devtools:
    restart:
      enabled: true
      additional-exclude: ttt/**,**/Ttt*.class

spring.devtools.restart.exclude, spring.devtools.restart.additional-exclude 프로퍼티 설정으로 default restart exclusions를 수정할 수 있습니다.


4.2. 컨테이너 재시작 없이 템플릿 뷰 로드

Spring Boot 에서 지원하고 있는 템플릿의 캐싱 비활성화 옵션을 제공하여, 템플릿 뷰의 수정사항 발생시, 컨테이너 재시작 없이 갱신된 사항을 로드할 수 있게 해줍니다.

예를들어, Thymeleaf 템플릿을 이용하는 경우, spring.thymeleaf.cache 를 false로 설정할 경우, 서버 재시작 없이 템플릿의 변경사항을 로드할 수 있습니다.

spring:
  thymeleaf:
    cache: false

FreeMarker 템플릿을 사용한다면 spring.freemarker.cache, Groovy 템플릿을 사용한다면 spring.groovy.template.cache 에 설정하면 됩니다.


4.3. 빠른 애플리케이션 재시작

devtools에는 자동 애플리케이션을 재시작을 지원합니다.

devtools를 이용한 restart에서는 2개의 클래스 로더를 이용하여 재시작을 하며,
외부 라이브러리의 경우에는 기존에 채워져있는 것을 그대로 사용하고(기본 클래스로더에 로드되어있음) 개발자가 직접 작성한 클래스들만 재시작 클래스로더에 재로드하여 재시작합니다.

JRebel과 같은 기술만큼 빠르지는 않지만, 일반적으로 "cold start" 보다는 빠릅니다.
(cold start는 일반적으로 애플리케이션을 종료하고 새로 시작하는 것을 의미합니다.)


4.4. 컨테이너 재시작 없이 java 클래스 리로드

IntelliJ IDEA, Eclipse 등 최신 IDE에서는 bytecode의 핫 스와핑을 지원하고 있습니다.

class나 method의 시그니처에 영향을 미치지 않는 수준의 변경이 일어난 경우에는 컨테이너의 재시작없이 리로드하여 수정사항을 즉각 반영해줍니다.

class나 method의 시그니처에 영향을 미친 경우에는 재시작없이 리로드 사항이 반영되지 않습니다.

새로운 메서드가 추가 또는 삭제 되는 경우 아래와 같은 팝업 노출과 함께 핫스왑이 실패됩니다.

api url의 변경과 같은 시그니처 변경시에는 별도의 실패 팝업은 뜨지 않지만 리로드는 되어도 api 변경이 반영되지 않습니다.


5. 자동 restart

spring-boot-devtools 를 사용하는 애플리케이션은 classpath 파일이 변경될때마다 재시작합니다. (재시작 할 때, 수정된 파일에 대해 다시 컴파일 한 후 재시작 됩니다.)

이는, IntelliJ와 같은 IDE로 개발을 할 때에, 변경사항을 빠르게 반영해주기 때문에 편리합니다.

단, AspectJ weaving 을 사용할 때엔 자동 재시작을 지원하지 않습니다.

5.1. Restart vs Reload

spring boot에서 제공하는 재시작은 2개의 클래스로더에 의해 작동합니다.
dependency를 통해 이용하고 있는 타사 jar 클래스들은 변경되지 않는 클래스로, 기본 클래스로더(base classloader)에 로드됩니다.
그리고, 개발중인 클래스들은 재시작 클래스로더(restart classloader)에 로드됩니다.

애플리케이션이 재시작될 때, 재시작 클래스로더는 사라지고 새로운 클래스 로더가 생성됩니다.
기본 클래스로더는 기존에 채워져있는 것을 그대로 사용하기 때문에, 애플리케이션 restart는 일반적으로 "cold starts" 보다 훨씬 빠릅니다.

애플리케이션의 재시작이 너무 느리거나, 클래스로딩에 문제가 있다면 JRebel 같은 재로딩 기술을 고려해보는 것도 좋습니다.


5.2. condition evalutaion에서 변경사항 로깅

기본적으로 애플리케이션이 재시작할 때, 조건 평가 델타를 보여주는 보고서가 기록됩니다.
이 보고서에는 빈의 추가나 제거, 구성설정의 변경 등에 대한 변경 사항이 들어있습니다.

컨테이너 restart시 보고서 로깅을 비활성화하고 싶다면 spring.devtools.restart.log-condition-evaluation-delta 값을 false로 설정하면 됩니다.

아래는 condition evalutaion delta의 예시입니다.

2024-09-30T13:42:56.365+09:00 DEBUG 35798 --- [spring-boot] [  restartedMain] .s.b.a.l.ConditionEvaluationReportLogger :


============================
CONDITIONS EVALUATION REPORT
============================


Positive matches:
-----------------

   AopAutoConfiguration matched:
      - @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition)

   AopAutoConfiguration.ClassProxyingConfiguration matched:
      - @ConditionalOnMissingClass did not find unwanted class 'org.aspectj.weaver.Advice' (OnClassCondition)
      - @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition)

   ApplicationAvailabilityAutoConfiguration#applicationAvailability matched:
      - @ConditionalOnMissingBean (types: org.springframework.boot.availability.ApplicationAvailability; SearchStrategy: all) did not find any beans (OnBeanCondition)

      ...

Negative matches:
-----------------

   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'jakarta.jms.ConnectionFactory' (OnClassCondition)

...         

Exclusions:
-----------

   None


Unconditional classes:
----------------------

   org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration

   org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration

   org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration

   org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration

   org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration

   org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration

5.3. 리소스 제외

특정 리소스의 경우, 수정사항이 발생되었다 하더라도 반드시 재시작이 될 필요가 없습니다. (ex. thymeleaf 템플릿)

기본적으로, /META-INF/maven, /META-INF/resources, /resources, /static, /public, /templates 디렉토리 내의 리소스들은 변경사항이 있어도 재시작이 되지 않습니다. (하지만, live reload의 경우, 해당 디렉토리 내의 리소스 변경이 있을 시 재시작됩니다.)

String DEFAULT_RESTART_EXCLUDES = "META-INF/maven/**,META-INF/resources/**,resources/**,static/**,public/**,templates/**,**/*Test.class,**/*Tests.class,git.properties,META-INF/build-info.properties";

만일, 이 리소스 디렉토리들을 변경하고 싶다면, spring.devtools.restart.exclude 에 설정해주면 되고, 기본값을 유지하면서 리소스 제외값을 추가하고 싶다면 spring.devtools.restart.additional-exclude 프로퍼티 값을 추가해주면 됩니다.

spring:
  application:
    name: spring-boot

  devtools:
    restart:
      log-condition-evaluation-delta: false
      additional-exclude: /ttt/**,**/*Test.class

5.4. restart 비활성화

재시작 기능을 비활성화 하고 싶다면, spring.devtools.restart.enabled 값을 false 로 설정하면 됩니다.

프로퍼티값을 통해 restart 비활성화 시킬 경우, 재시작 클래스로더가 초기화된 후, 파일 변경 사항을 감시하지 않아 컨테이너 재시작이 되지 않습니다.

만일, 완전한 restart 비활성화를 설정하고 싶다면, 코드상에 시스템 설정값을 통해 spring.devtools.restart.enabled 값을 false로 설정하면 됩니다.

@SpringBootApplication
public class MyApplication {

	public static void main(String[] args) {
		System.setProperty("spring.devtools.restart.enabled", "false");
		SpringApplication.run(MyApplication.class, args);
	}

}

5.5. 알려져있는 제한사항

restart 기능은 표준 ObjectInputStream 을 이용한 역직렬화된 객체에서 잘 작동되지 않습니다.

만약 데이터 역직렬화가 필요한 경우라면, Spring 의 ConfigurableObjectInputStreamThread.currentThread().getContextClassLoader() 와 함께 사용해야할 수 있습니다.

몇몇 3rd party 라이브러리들에서 context classloader를 고려하지 않고 역직렬화하는 경우가 있어, 이 부분을 유의해야합니다.


6. 애플리케이션의 프로덕션 배포를 위한 패키징

애플리케이션을 프로덕션 환경에 배포하기 위해서는 패키징을 해야합니다.
스프링 부트에서는 애플리케이션 패키징을 최적화하기 위한 다양한 옵션을 제공합니다.

프로덕션 환경에 health check, REST 메트릭, JMX end-point, spring actuator 등을 추가하는 것을 고려해보세요.


++

  • Hot Swap failed. SpringBootApp: add method not implemented
  • Hot Swap failed. SpringBootApp: delete method not implemented
728x90
반응형