본문 바로가기
Spring

[Spring] Bean Lazy Initialization 사용법

by 노력남자 2023. 6. 18.
반응형

이번 포스팅에선 Spring의 Lazy initialization에 대해 알아보겠다.

 

먼저 Spring이 lazy initialization 설명한 글을 번역해보겠다.

 

Lazy Initialization in Spring Boot 2.2

 

Lazy란 무엇을 의미하나요?


Spring Framework는 11년 전 소스 코드가 Git으로 이동하기 전부터 lazy bean 초기화를 지원해왔습니다. 기본적으로 응용 프로그램 컨텍스트가 새로 고침될 때마다 컨텍스트의 모든 bean이 생성되고 해당 bean의 종속성이 주입됩니다. 그에 반해, lazy bean 정의가 구성된 경우 해당 bean은 필요할 때까지 생성되지 않고 종속성이 주입되지 않습니다.

 

Lazy Initialization를 활성화하는 방법


Spring Boot의 모든 버전에서 lazy Initialization를 활성화할 수 있습니다. BeanFactoryPostProcessor를 작성하고 직접 처리하는 것을 원한다면 가능합니다. Spring Boot 2.2에서는 새로운 속성인 spring.main.lazy-initialization을 도입하여 이를 더 쉽게할 수 있게 했습니다 (SpringApplication 및 SpringApplicationBuilder에도 동등한 메서드가 있습니다). 이 속성을 true로 설정하면 애플리케이션의 bean 정의가 lazy Initialization를 사용하도록 구성됩니다.

 

Lazy Initialization의 장점


Lazy Initialization를 사용하면 애플리케이션 시작 시 로드되는 클래스와 생성되는 bean의 수가 줄어들어 시작 시간이 크게 감소할 수 있습니다. 예를 들어, 일반적으로 2500ms에 시작되는 Actuator와 Spring Security를 사용하는 작은 웹 애플리케이션은 lazy Initialization를 활성화하면 2000ms에 시작됩니다. 정확한 개선 정도는 애플리케이션마다 달라지며 bean의 종속성 그래프 구조에 따라 다를 수 있습니다.

 

DevTools에 대해서는 어떻게 생각하시나요?


Spring Boot의 DevTools는 이미 개발자 생산성을 크게 향상시킵니다. 변경 사항을 시도할 때마다 JVM과 애플리케이션을 다시 시작해야하는 대신 DevTools를 사용하면 동일한 JVM에서 애플리케이션을 핫 리스타트할 수 있습니다. 핫 리스타트의 중요한 이점은 애플리케이션 시작에 관련된 코드를 JIT(Just-In-Time) 컴파일러가 더 많이 최적화할 수 있는 기회를 제공한다는 것입니다. 몇 번의 재시작 후에 원래의 2500ms 시간은 거의 80% 가까이 줄어들어 500ms 정도로 줄어듭니다. lazy Initialization과 함께 사용하면 더 좋은 결과를 얻을 수 있습니다. spring.main.lazy-initialization을 설정하면 애플리케이션은 IDE에서 직접 400ms로 재시작됩니다.

 

Lazy Initialization의 단점


위에서 보았듯이, lazy Initialization를 활성화하면 시작 시간을 상당히 크게 줄일 수 있습니다. 항상 활성화하도록 선택하려는 유혹을 느낄 수도 있으며, 기본적으로 활성화하지 않은 이유에 대해 궁금할 수도 있습니다. 하지만 lazy Initialization에는 몇 가지 단점이 있어서, 해당 단점이 적절하다고 판단되면 선택적으로 활성화하는 것이 더 나을 것이라고 여겨집니다.

필요한 시점까지 클래스가 로드되지 않고 bean이 생성되지 않기 때문에, lazy Initialization로 인해 이전에 시작 시간에 식별되었을 문제가 감춰질 수 있습니다. 이러한 문제는 클래스를 찾을 수 없는 오류, 메모리 부족 오류 및 잘못된 구성으로 인한 오류 등을 포함할 수 있습니다.

웹 애플리케이션에서 lazy Initialization는 bean 초기화를 트리거하는 HTTP 요청에 대한 대기 시간(latency) 증가로 이어질 수 있습니다. 일반적으로 첫 번째 요청에만 해당되지만, 로드 밸런싱 및 자동 확장에 부정적인 영향을 줄 수 있습니다.

 

언제 Lazy Initialization를 활성화해야 할까요?


위에서 보았듯이, lazy Initialization는 시작 시간에 상당한 개선을 제공할 수 있지만 몇 가지 주목할만한 단점도 있으므로 신중하게 활성화해야 합니다.

lazy Initialization가 혜택을 많이 제공하고 부작용이 거의 없는 한 가지 영역은 개발 중인 경우입니다. 애플리케이션을 반복적으로 개발하는 동안, lazy Initialization와 DevTools의 핫 리스타트를 통해 시작 시간을 크게 줄여 개발 생산성을 대폭 향상시킬 수 있습니다.

또 다른 lazy Initialization의 이점을 얻을 수 있는 영역은 애플리케이션의 통합 테스트입니다. Spring Boot의 테스트 슬라이스를 사용하여 특정 유형의 테스트에서 초기화되는 빈의 수를 제한하여 테스트 실행 시간을 줄이는 경우가 있습니다. lazy Initialization는 비슷한 결과를 얻기 위한 대체 메커니즘을 제공합니다. 애플리케이션을 테스트 슬라이싱에 맞게 구성할 수 없는 경우나 특정 유형의 테스트에 대한 슬라이스가 없는 경우, lazy Initialization를 활성화하면 테스트에서 필요한 빈만 초기화되므로 테스트 실행 시간이 줄어듭니다. 특히 개발 중에 테스트를 독립적으로 실행할 때 이점이 있습니다.

마지막으로, 운영 환경에서도 lazy Initialization를 활성화해 볼 수 있습니다. 그러나 신중하게 진행해야 합니다. 웹 애플리케이션의 경우, 컨테이너 오케스트레이션은 /health 엔드포인트를 더 빠르게 응답할 수 있도록 도움을 받을 수 있지만, 애플리케이션의 자체 엔드포인트로 첫 번째 요청이 이루어질 때 대기 시간이 증가할 수 있다는 점을 인지해야 합니다. 또한, 모든 구성 요소가 사용된 후에도 원하지 않는 메모리 부족 오류를 피하기 위해 lazy Initialization를 비활성화한 상태로 애플리케이션의 JVM 크기를 조정하는 것이 중요합니다.

 

Lazy Initialization 사용법

 

lazy Initialization에 대해 알아봤으니 어떻게 사용하는지 어떻게 동작하는지 알아보겠다.

 

설정 방법

 

spring boot 2.2 버전 이후부터 설정이 가능하다고 한다.

 

// application.yml
spring:
  main:
    lazy-initialization: true

 

spring boot 2.2 이전 버전은 아래와 같이 추가해주면 된다.

 

@Component
class LazyInitBeanFactoryPostProcessor : BeanFactoryPostProcessor {
    
    override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory) {
        beanFactory.beanDefinitionNames.forEach {
            val bean = beanFactory.getBeanDefinition(it)
            
            if (!bean.isLazyInit) {
                bean.isLazyInit = true
            }
        }
    }
}

 

만약 제외시키고 싶은 bean이 있다면 아래와 같이 LazyInitializationExcludeFilter를 선언해주면 된다.

 

@Configuration
class LazyInitExcludeConfiguration {

    @Bean
    fun lazyInitializationExcludeFilter(): LazyInitializationExcludeFilter {
        return LazyInitializationExcludeFilter.forBeanTypes(MyBean.class.java)
    }
}

 

만약 글로벌하게 설정하고 싶지 않다면? @Lazy 어노테이션을 사용하면 된다.

 

@Lazy
@Component
class MyBean {
    // ...
}

 

작동 방법

 

Lazy로 설정해놓으면 실제 호출되기 전까지는 초기화가 되지 않는다.

 

Lazy Bean을 초기화하는 방법은 아래와 같다.

 

- 실제 호출

- Lazy Bean이 의존성 주입을 당하는 순간

 

테스트

 

// TestBean.kt
@Component
class TestBean {
    
    @PostConstruct
    private fun init() {
        println("The TestBean has been initialized.")
    }
}

// TestBean2
@Component
class TestBean2(
    private val testBean: TestBean
) {

    @PostConstruct
    private fun init() {
        println("The TestBean2 has been initialized.")
    }
}

// TestController.kt
@RestController
class TestController(
    private val testBean2: TestBean2
) {

    @GetMapping("/test")
    fun test(): String {
        return "test"
    }
}

 

TestBean2은 TestBean을 의존하고 있다.

 

TestBean, TestBean2 초기화 여부를 판단하기 위해서 @PostConstruct에 초기화 로그를 찍어놨다.

 

초기화가 되면 The TestBean has been initialized., The TestBean2 has been initialized.가 찍힐 거다.

 

// application.yml
spring:
  main:
    lazy-initialization: true

 

lazy-initialization을 true로 설정한 후 서버를 키면

 

 

초기화 로그가 보이지 않는다.

 

TestController에 선언한 "/test"를 호출하면?

 

 

호출하는 순간 TestController가 초기화되면서 TestBean2을 주입받기 때문에 TestBean2와 TestBean2이 의존하고 있는 TestBean이 초기화 되는 걸 볼 수 있다.

 

 

Lazy Bean이 좋아보이지만 단점이 확실하게 있기 때문에 api 서버에 적용하는 건 좀 어려워 보이고 배치 서버에 적용해보는 건 괜찮아 보인다.

 

References

 

https://spring.io/blog/2019/03/14/lazy-initialization-in-spring-boot-2-2

반응형

댓글