[spring] Bean 생성 및 의존관계 형성(Singleton, 생성자DI와 Immutable Bean) **

2021. 9. 8. 13:09[ 백엔드 개발 ]/[ Spring ]

Bean Scope

Spring에는 6가지 bean scope이 있지만 기본적으로 Bean을 Singleton으로 관리한다.

 

 

IoC 컨테이너 생성 시점에 Bean을 생성하는 이유(Bean Scope이 싱글톤일때)

You can generally trust Spring to do the right thing. It detects configuration problems, such as references to non-existent beans and circular dependencies, at container load-time. Spring sets properties and resolves dependencies as late as possible, when the bean is actually created. This means that a Spring container that has loaded correctly can later generate an exception when you request an object if there is a problem creating that object or one of its dependencies — for example, the bean throws an exception as a result of a missing or invalid property. This potentially delayed visibility of some configuration issues is why ApplicationContext implementations by default pre-instantiate singleton beans. At the cost of some upfront time and memory to create these beans before they are actually needed, you discover configuration issues when the ApplicationContext is created, not later. You can still override this default behavior so that singleton beans initialize lazily, rather than being eagerly pre-instantiated.

 

위 스프링 공식문서를 정리하면 다음과 같다.

 

  • 우리는 IoC 컨테이너가 알아서 올바르게 bean들 간 의존관계를 형성시켜줄 것이라 믿지만, IoC 컨테이너는 bean이 실제로 생성된 후에야 실질적인 의존관계를 형성하며, 이 과정 중에 bean 설정에 대한 예외가 발생했는지 파악한다.
  • 만약 bean이 필요시에만 만들어진다면, 해당 bean 생성 요청을 하기 전 까지는 bean 설정에 대한 예외가 발생했는지 파악할 수 없을 것이다.
  • 이러한 이슈는 ApplicationContext(IoC container)에서 bean을 구현할 때 pre-instantiate singleton을 기본으로 선택한 이유이다. Spring IoC container는 Bean을 생성한 뒤에 실질적인 의존성 관계를 형성한다.(dependency resolution)

 

cf. Pre-instantiate Singleton vs lazy initialization

- pre initialization singleton : 프로세스 생성시점에 바로 싱글톤을 생성(스프링이 사용하는 방식)

- lazy initialization singleton : 싱글톤이 필요한 '가장 최초시점'에 싱글톤 생성

 

* 주의: 싱글톤의 의미는 heap 메모리에 해당 객체가 하나만 존재하는 것을 의미한다. 즉, pre initialization singleton이든 lazy initialization singleton이든 해당 객체는 1개만 존재하는 것은 맞지만 싱글톤을 어느 시점에 생성시킬 것인가에 대한 차이뿐이다.

 

 

 

 

DI 시점과 생성자 기반의 DI

스프링은 2가지 주요 DI 방식을 설명한다. 생성자 주입, 세터 주입 방식이 있다. 스프링에서는 생성자 주입 방식을 권장하는데, 생성자 주입 방식을 사용하면 실질적인 DI가 애플리케이션 부팅 시점에 이루어지기 때문이다.(pre initialization singleton이 생성시점) 즉, 생성자 기반의 DI를 사용하면 Spring IoC container가 생성되면서 Bean들을 생성하고, Bean이 생성된 후에 해당 Bean과 관련된 의존성을 주입한다.

 

위 스프링 공식 문서를 보면 생성자 기반의 DI를 권장한다고 한다. 생성자 기반의 DI를 사용하면 객체를 사용할 때 해당 객체가 갖는 의존성이 모두 정해져 있다고 보장할 수 있다. 이를 통해 NPE과 같은 런타임 예외 발생 여지를 줄인다.

 

또한 생성자 기반으로 의존성을 주입하면 final 키워드를 사용하여 의존관계를 갖는 객체를 immutable하게 관리할 수 있다. 즉, 해당 객체의 상태를 불변으로 관리하여 멀티쓰레드 환경에서 공유하는 heap 영역의 bean을 thread-safty하게 관리하고 잠재적 버그의 여지를 줄인다.

 

cf. 생성자 인자가 많은 객체는 많은 의존성을 가졌다는 것을 의미한다. 즉, 관심사의 분리가 필요한 객체이다.

cf. 생성자기반 주입방식을 사용하면 Circular dependencies 발생 가능성이 있다. 한 가지 해결책은 Setter기반 주입방식으로 수정하는 것이다.

 

 

cf. DI를 사용하는 이유

  • 유연성 확보 : 의존 관계 설정이 컴파일 시가 아닌 런타임 시에 이루어지도록 하여 모듈 간 결합도를 낮춘다. 특정 객체를 필요로 하는 클래스 내에서 직접 객체를 생성한다면 클래스를 특정 객체에 확정 짖는 것이고, 이후에 해당 객체가 다른 객체로 대체된다면 클래스의 수정이 요구된다. 즉, 다른 객체를 필요로 하는 경우 클래스를 재사용할 수 없게 된다. 하지만 의존성을 외부로부터 주입받도록 하여 코드의 재사용성 및 모듈 간 결합도를 낮출 수 있다. 또한 주입 받을 의존성을 인터페이스 타입으로 두어, 코드를 수정하지 않고도 해당 인터페이스에서 제공하는 모든 것들을 사용할 수 있도록 한다.
  • 테스트 코드 작성 용이 : 특정 객체를 해당 클래스에서 직접 생성한다면 실제 객체를 모의 객체로 대체할 수 없기 때문에 클래스를 테스트하기 힘들게 한다. 특정 모듈(class)에 대해 테스트 시, 해당 모듈이 의존하는 다른 모듈과 더욱 독립적이며 이런 모듈을 가장하는 stub 또는 mock객체를 사용해 편리한 단위 테스트가 가능하다. 특히, 생성자 기반 DI를 사용하면 테스트 시 생성자 주입을 통해서 목 객체를 쉽게 전달할 수 있다. 즉, 목 객체를 이용해 생성자로 주입해주면 테스트할 모듈(클래스)만을 테스트할 수 있도록 한다.

 

 

Spring에서 Singleton을 기본으로 사용하는 이유

대규모 트래픽을 처리하기 위함이다. 스프링은 엔터프라이즈급 애플리케이션을 목표로 만들어졌다. 엔터프라이즈급 애플리케이션에서 수 많은 요청이 계속적으로 올 때마다 bean 객체를 만드는 것은 매우 비효율적일 것이고, 아무리 GC성능이 좋아졌다 하더라도 부하를 감당하기 힘들 것이다. 따라서 (bean scope이 모두 default일 경우) 스프링은 초기 로딩 시 시간이 걸리더라도 pre-instantiate singleton으로 bean을 만든다. 즉, Spring IoC container 생성 시에 IoC container는 모든 bean들을 만들고, bean설정에 대한 예외처리를 초기에 검증한다.

 

cf. reflection 오버헤드 영향도 있음

 

 

Bean이 Singleton이라면 Thread-Safe한가?

클라이언트로부터 HTTP요청이 오면, 서블릿 컨테이너(WAS)가 쓰레드 풀에서 요청 당 쓰레드를 할당한다.(Spring MVC 한정) 기본적으로 객체는 heap메모리에 존재하므로 별다른 설정이 없으면 Thread-Safe 하지 않다. 따라서 Bean에는 상태가 바뀔 수 있는 필드를 작성하지 않는 것이 좋다. 즉, Bean자체를 불변(Immutable)으로 관리하면 된다. Bean을 불변으로 관리하기 위한 방법 중 하나로 필드에 final 키워드를 사용하는 것이고 이것이 Spring이 생성자 주입 방식을 지향하는 이유이다.

 

 

정리

- 싱글톤과 의존성 필드에 대한 final 키워드를 통해 Immutable, stateless하게 bean을 관리한다는 것은 쓰레드 간 동기화를 고려할 필요가 없다는 것을 의미한다.

- 스프링은 Bean을 생성할 때 reflection을 기반으로 하는데, Pre initialization singleton 방식으로 Bean을 생성함으로써 스프링 애플리케이션 부팅 시점에 Bean을 모두 생성하여 reflection 오버헤드를 줄인다.

 

 

 

 

 

Reference

- https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependency-resolution

- https://stackoverflow.com/questions/15745140/are-spring-objects-thread-safe

- https://docs.spring.io/spring-framework/docs/2.5.5/reference/beans.html