스프링 부트 환경에서 Bean 지연 로딩
2019년 7월 기준으로 스프링부트 공식 버전은 2.1.6.RELEASE 이다. 2.2 버전은 M.4 버전까지 진행되었는데, 올해 안에 공식 릴리스가 될 것으로 추측된다. 스프링부트 2.2.0 에서 신규로 추가된 기능 중에서 스프링 Lazy initialization 기능이 포함되었다. 사실, 특별한 건 없다. 스프링 Bean 을 쉽게 Lazy 로딩할 수 있도록 프로퍼티 설정을 제공할 뿐이다. Lazy 로딩은 예전부터 지원하는 기능인데, 부트 2.2.0 에서는 좀 더 심플하게 적용할 수 있게 되었다.
이번 글에서는, 스프링에서 Bean 을 Lazy 로딩하는 방법에 대해서 이야기 해보자. 참고로, 필자가 주말에 글 쓰는게 너무 귀찮아서 내용을 많이 생략하면서 대충대충 작성하였다. 필자의 개인적인 지식을 정리했기 때문에 잘못된 내용이 포함 되었을 가능성도 있다. 이 글을 보는 개발자분들은 댓글로 피드백을 꼭 해주길 바란다.
이번 글의 주제는 Bean Lazy 이기 때문에, 기본적인 스프링 IOC, Bean LifeCycle 에 대해서는 간략하게만 정리하겠다. 자세한 내용은 관련 서적을 찾아보길 바란다.
Spring IOC, Bean Life Cycle
기본적으로 스프링 IOC 는 애플리케이션 시작할 때 모든 Bean 을 초기화한다. 일반적으로 이 과정에서 Bean 초기화에 실패할수도 있는데, 애플리케이션 실행 시점에서 오류를 즉시 발견할 수 있다. 이런 특징으로 인해서, 애플리케이션은 런타임시 발생할 수 있는 장애를 미리 실행시점에서 사전에 발견할 수 있다. 반면에, 스프링의 이런 특징으로 인해서, 애플리케이션 설정이 많고 무겁다면, 애플리케이션 초기화할 때 많은 시간이 걸린다. 특정 애플리케이션이 JDBC 연결도 해야하고, ElasticSearch 연동도 해야한다고 가정해보자. JDBC 커넥션을 맺어야하고, Elasticsearch 커넥션을 맺어야 하는 등 의존성이 많을수록 애플리케이션 실행 시간은 많이 느려질 것이다. 물론, 개발환경에서는 Spring Boot DevTools 를 사용해서 이런 단점을 개선할 수는 있지만, 완벽한 해결책은 아니다.
필자는 그동안 Bean Lazy 로딩해본적은 거의 없다. 실서비스 환경은 대부분 무중단 배포가 가능한 환경이었기 때문에, 배포 시간이 조금 느려도 실서비스에는 전혀 이슈가 되지 않았다. 물론, 느려봤자 30초 내로 배포가 되기는 했다. 또한, 개발 환경에서도 큰 불편은 없었다. 단위 테스트는 당연히 그 어떤 디펜던시도 없기 때문에 문제가 없었고, 통합 테스트의 경우에는 테스트 컨피그레이션을 설정하거나, 로컬 프로퍼티 설정하는 방식 등 대안을 찾아서 해결하였다.
@PostConstruct
Bean 객체가 생성된 직후 초기화 작업이 필요한 경우 사용한다. 메서드 상단에 선언하면 된다.
InitializingBean
@PostConstruct 와 유사하게, Bean 객체가 생성 된 직후 초기화 하기 위해서는 InitializingBean 인터페이스를 구현하는 방법이 있다. InitializingBean 인터페이스의 afterPropertiesSet() 메서드를 구현하면 된다.
DisposableBean
Bean 객체가 소멸될 때 메서드를 수행하기 위한 인터페이스이다. 해당 인터페이스를 구현하는 클래스는 destroy() 를 구현해야 한다.
@Bean(initMethod="")
생략
마무리
자세한 내용은 생략한다.
아주 간단하게 Bean 을 만들어서 @Lazy 어노테이션을 사용해보겠다.
Repository 클래스 하나 만든다. 생성자에 10초 지연시간 추가했다. 초기화 될 때 많이 느릴 것이다.
Bean 을 만드는 방법으로 @Bean 어노테이션을 사용하겠다. @Bean 어노테이션을 사용하는 방법이기 때문에 별도로 @Component 또는 @Repository 어노테이션을 선언하지 않았다. Configuration 클래스 하나 만들고 @Bean 어노테이션을 사용해서 CoffeeRepository 빈을 정의한다. 그리고 @Lazy 어노테이션을 같이 선언한다.
요렇게 선언하면 coffeeRepository 빈은 Lazy 초기화 될 것이다. 이걸로 끝일까?
여기서 작은 함정이 있다.
coffeeRepository 를 다른 컴포넌트에서 주입받아서 사용하게 되면 어떻게 될까? CoffeeService 에서 CoffeeRepository 를 주입받아서 사용한다고 가정하자. CoffeeService 는 @Service 어노테이션에 의해서 선언이 되었고 스프링이 실행 될 때 "coffeeService" 빈이 초기화 될 것이다. 이때 주입 받는 CoffeeRepository 역시 초기화가 된다.
그래서, 이 경우에는 CoffeeRespository를 지연 로딩하지 않는다. CoffeeRepository Bean 을 Lazy 로딩하도록 선언했음에도 불구하고, CoffeeService 에 주입해야하기 때문에 어쩔수 없이 CoffeeRepository 를 바로 초기화할 수 밖에 없다.
그렇다면 이런 경우에,
즉, 디펜던시 인젝션을 해야하는 경우에는 어떻게 설정해야 CoffeeRepository 를 Lazy 로딩할 수 있을까?
디펜던시 인젝션에서 @Lazy 로딩을 하는 방법에 대해서 알아보자. 사실, 아주 간단하다. 생성자 주입 시 @Lazy 어노테이션 선언해주면 된다. 아래와 같이 @Lazy 어노테이션을 생성자 상단에 선언하고 애플리케이션을 실행해보자. CoffeeService 에서 주입하는 CoffeeRepository 는 애플리케이션 실행 시점에서 초기화 되지 않는다. 즉, CoffeeService 를 사용하기 전까지는 CoffeeRepository 에 적용한 10초 시간은 수행하지 않는다는 의미다.
스프링 애플리케이션이 초기화 한 이후에, CoffeeRespository 를 실제로 사용하는 시점에서 CoffeeRepository 는 초기화 될 것이다. 참고로 필드 인젝션 주입도 가능허다.
https://github.com/sieunkr/spring-boot-lazy-Initialization/tree/master/spring-boot-lazy-annotation
만약 모든 컴포넌트를 Lazy 로딩하고 싶다면 어떻게 할까? 즉, 스프링이 실행 될 때 모든 Bean 을 초기화하지 않게 할 수 있을까? 가장 간단한 방법은 위에서 설명한 방법으로, @Lazy 어노테이션을 모든 컴포넌트에 선언해주면 될 것이다. 하지만, 모든 컴포넌트에 @Lazy 를 선언해주는 방법은 깔끔하지 않을 수 있다. @Lazy 어노테이션을 사용하지 않고, 지연 로딩을 구현 해보자. BeanFactoryPostProcessor Bean 를 선언해주는 커스텀 Configuration 클래스를 선언해주자. 그리고 아래 샘플 코드와 같이 모든 Bean 에 setLazyInit(true) 를 정의해주면 된다.
필자의 설명에 오해가 없기를 바란다. 모든 Bean 을 Lazy 로딩하기 위해서 BeanFactoryPostProcessor 를 사용했을 뿐, BeanFactoryPostProcessor가 Lazy 로딩을 위해서만 사용하지는 않는다. @Lazy 어노테이션은 매우 정적이고 제한적이다. 특정 조건에 따라서 Dynamic 하게 빈 설정을 변경하기 위해서 BeanFactoryPostProcessor 를 사용하는 것이다. BeanFactoryPostProcessor 는 스프링 컨텍스트가 초기화 된 후에 Bean 정의를 수정할 수 있도록 제공하며, 어떤 Bean 을 Lazy 하게 구성할지 설정할 수 있다. 필자의 샘플은 모든 Bean 에 Lazy 를 적용했을 뿐이다. 필요에 따라서는 특정 Bean 만 lazy 로딩을 초기화 할수있도록 구성할 수도 있다.
https://github.com/sieunkr/spring-boot-lazy-Initialization/tree/master/spring-boot-lazy-beanfactory
이 글의 초반에 Spring Boot 2.2.0 에서는 Lazy 로딩을 위한 프로퍼티를 제공한다고 소개하였다.
이렇게 설정을 하면, 모든 Bean 이 Lazy 하게 초기화 될 것이다. 필자의 이번 글에서 설명한 BeanFactoryPostProcessor 를 이해했다면, 사실 해당 기능은 바로 이해가 될 것이다. 내부적으로 어떻게 동작하는지 코드를 찾아보자. LazyInitializationBeanFactoryPostProcessor 클래스를 확인하면 된다.
자. 그리고, 아래와 같은 구문에서 프로퍼티 설정 값을 Bean 을 Lazy 하게 설정한다는 것을 확인할 수 있다.
주의!! 스프링 부트 2.2.0 은 아직 공식 Release 가 되지 않았다. 2019년7월20일 기준으로 현재 2.2.0.M4 버전이 진행중이다. 올해 안으로 Release 가 될것으로 추측된다. 마일스톤을 실무에서 사용하는 것은 주의가 필요하다. 오픈소스 생태계에 기여하고 싶다면 마일스톤 버전을 사용해보는 것을 추천한다. 하지만, 안정적인 애플리케이션을 구축하고 싶다면 추후에 나오는 RELEASE 버전을 사용하는 것을 추천한다.
필자가 최근에 스프링 통합 테스트 관련 글을 작성하였다.
https://brunch.co.kr/@springboot/207
스프링에서의 통합테스트는 일반적으로 @SpringBootTest 어노테이션을 사용하는데, 해당 기능은 스프링을 실행할때 모든 Bean 을 초기화하게 된다. 물론, Classes 라는 속성을 지정해서 특정 클래스만 실행할 수 있도록 정의할수는 있지만, 해당 방법으로는 해결하기 어려운 경우도 많다. 모든 Bean 을 Lazy하게 초기화하는 것은 통합테스트 시간을 단축시킬 수 있기 때문에 나름 의미가 있다. 테스트 코드 관련해서는 필자가 추가로 글을 작성하겠다.
필자가 글을 잘 못써서 재미없는 글이 되었지만, 스프링을 공부하는 개발자에게는 아주 유익한 내용일 것이다. 스프링 Bean 을 Lazy 하게 로딩하는것이 무조건 좋은 것은 아니다. 애플리케이션 실행 시간을 줄일 수 있어서 생산성을 높일 수 있지만, 예상하지 못한 애플리케이션 이슈가 발생할 수도 있다. 애플리케이션의 특성에 맞게 Lazy 를 사용할지 말지 검토해야 한다. 스프링에서 기능을 제공한다고 해서, 그 기능이 항상 정답이 아닌 것이다.
어쨋든, 너무 중요한 내용이니깐 각자 고민을 잘 해보길 바란다.
https://stackoverflow.com/questions/56695983/lazy-vs-beanfactorypostprocessor-spring-boot
https://www.baeldung.com/spring-boot-lazy-initialization
https://spring.io/blog/2019/03/14/lazy-initialization-in-spring-boot-2-2