Spring IoC Container

jiho·2021년 5월 31일
0

Spring

목록 보기
2/13

Spring Framework의 핵심을 알기위해서 [공식문서]를 토대로 정리 중 입니다.(https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans)

Spring Framework는 엔터프라이즈 어플리케이션을 개발하기 위해 필요한 기능들을 제공합니다. 그 기능 중 이번에는 객체들(Bean)의 생성과 객체간의 의존성을 관리해주는 Inversion of Control(IoC Container에 대해 알아보고 정리해보겠습니다.

IoC는 DI(Dependency Injection)이라고도 불립니다. 의존성을 직접 객체내에서 생성하지않고 외부에서 의존성을 주입해주는 형태를 IoC or DI라고합니다.

ApplicationContext extends BeanFactory

Spring IoC Containter의 구체적인 인터페이스를 살펴보겠습니다.

public interface ApplicationContext extends BeanFactory, ... { 
	...
}

ApplicationContext interface는 스프링 IoC Container를 나타내며 Bean들을 생성하고 설정하고 조립하는 역할을 합니다. Bean과 Bean 의존성에 대한 내용을 정의한 설정 메타 데이터를 읽어서 어떻게 생성하고 설정하고 조립할지를 결정합니다.

위에서 말하는 설정 메타데이터는 전통적인 XML, Annotation, Java code 방식으로 나타낼 수 있습니다. 이러한 메타데이터 어플리케이션이 어떤 Bean으로 이루어져있고 Bean 사이의 풍부한 상호의존을 나타낼 수 있습니다. 뒤에서 Annotation 기반으로 어떻게 빈과 의존성을 설정하는지 예제로 알아보겠습니다.

추상적인 관점에서 IoC Container의 동작방식을 다이어그램으로 나타낸 것 입니다.

  • Annotation 기반의 설정: 스프링 2.5 에 소개된 방식
  • Java 기반의 설정: 스프링 3.0에 시작됐습니다. @Configuration, @Bean

BeanFactory 직접 사용

주로 직접 사용할 일은 없지만 ApplicationContext를 주입받아서 특정 빈을 직접 받을 수도 있습니다.

// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);

// use configured instance
List<String> userList = service.getUsernameList();

BeanFactory interface가 제공하는getBean(...)같은 메소드들을 사용해서 Bean을 얻어낼 수 있습니다. 하지만 우리어플리케이션에서 이러한 직접 접근하는 메소드는 사용할 일이 거의 없습니다.

Bean

Bean은 Spring IoC Container가 관리하는 객체들을 말합니다.

Bean을 등록하는 방법은 간단합니다. @Component를 활용해서 Component Scanning을 사용해서 등록할 수도 있으면 @Bean을 통해서 등록하는 방법도 있습니다.

  • 가장 흔히 사용하는 @Component 어노테이션을 이용해서 등록
@Component
public class TestBean {
	...
}

정말 간단히 위와 같이 등록할 수 있습니다.

  • 메소드에 @Bean 어노테이션을 활용한 방법
@Configuration
public class TestConfiguration {
	@Bean
    TestBean testBean() {
    	return new TestBean();
    }
}

위와 같이 메소드를 통해서 Bean을 생성할 수도 있습니다.

간단히 Bean간의 의존성을 어떻게 처리하는지 정리해보겠습니다.

Dependency

IoC Container는 Bean 생성과 초기화 이외에도 의존성을 주입하는 역할이 있다고 했었습니다. 이번에 그 내용을 정리해보겠습니다.

Dependency Injection

Spring IoC Container는 injection을 하기 위한 두 가지 방식이 있습니다. 생성자 기반의 의존성 주입Setter 기반의 의존성 주입

생성자 기반의 의존성 주입

생성자 기반의 의존성 주입은 컨테이너가 생성자에 argument 타입에 알맞는 빈들을 주입해서 호출하면서 진행됩니다.

@Component // 의존성 주입을 받으려는 객체도 Bean으로 등록되야합니다.
public class TestService {
	private FooService fooService;
	@Autowired
    public TestService(FooService fooService) {
    	this.fooService = fooService;
    }
}

@Autowired 어노테이션을 적용해준 생성자는 컨테이너에 의해 의존성 주입을 받게 됩니다. 컨테이너는 생성자의 인자로 FooService 타입에 해당하는 Bean이 있는지 탐색하게되고 알맞은 타입의 Bean을 주입하게 됩니다.

Setter 기반의 의존성 주입

Setter 기반의 의존성 주입은 argument가 없는 생성자를 통해 Bean을 생성한 Setter를 호출해줍니다. 마찬가지로 Setter를 호출할 때, 알맞은 타입의 Bean을 주입해주게 됩니다.

@Component // 의존성 주입을 받으려는 객체도 Bean으로 등록되야합니다.
public class TestService {
	private FooService fooService;
    
    @Autowired
    public void setFooService(FooService fooService) {
    	this.fooService = fooService;
    }
}

생성자 기반 혹은 Setter기반의 DI ?

스프링 Team은 점점 생성자를 통한 의존성 주입을 추천합니다. 이러한 방식은 해당 컴포넌트를 immutable한 객체들로 구현하게 해주고 의존성있는 객체가 null이 되지않음을 보장할 수 있기때문입니다. 더해서 생성자 주입 컴포넌트들은 항상 초기화 상태에서 클라이언트 코드에 리턴됩니다.

순환 의존관계

Class A 가 Class B의 인스턴스를 생성자를 통해 주입받고 Class B가 생성자를 통해Class A를 주입받는 예를 생각해보겠습니다. 이럴 경우, Spring IoC Container는 런타임 도중 순환 참조를 감지하고 BeanCurrentlyCreationException을 발생시킵니다.

대안적으로 한가지 해결책은 생성자보다는 Setter에 의해 의존성 주입이 되도록 code를 수정하는 것입니다.

사실 이러한 해결책보다는 순환 참조 오류가 발생한 설계는 좋지않은 설계라는 것을 인지해야합니다. 즉, 순환 참조의 고리를 끊는게 제일 좋은 방법입니다.

이외에도 Lazy-initialization 이라는 방법도 있습니다.

Bean Scope

Bean을 등록할 때 Bean의 Life cycle을 정할 수도 있습니다. 기본적으로 아무 설정을 하지않으면 Singleton Scope로 Bean을 등록하게 됩니다.

스프링은 위와 같이 다양한 Scope를 제공합니다.

prototype scope는 주입받을 때마다 새로운 인스턴스를 생성하는 Scope입니다.

@Scope([scope name])를 사용해서 Bean의 scope를 등록할 수 있습니다.

@Component @Scope("prototype")
public class Proto {
	...
}

Singleton Scope의 Bean에서의 Prototype Scope Bean을 사용시 주의할 점.

singleton Bean 내에서 prototype Bean을 의존할 경우, prototype bean도 Singleton 참조에 의해 계속 하나의 인스턴스가 사용되게 됩니다.

이문제를 해결하기 위해서는 두 가지 방법이 있습니다.

  • ProxyMode 를 사용해서 Prototype Bean 등록
  • ObjectProvider<TargetObject>로 Singleton Bean의 의존성 등록

아래와 같이 ProxyMode를 설정해주면 Proxy 객체가 Prototype Bean을 주입해주게 되어 싱글톤 Bean 내에서도 매번 새로운 인스턴스를 반환하게 됩니다.

@Component @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class PrototypeBean {
...
}

두번째 방법으로 ObjectProvider 를 활용하는 방법입니다. Singleton Bean의 코드를 수정해야한다는 단점이 있습니다.

@Component @Scope("prototype")
public class Proto { ... }

@Component
public class SingleBean {

    @Autowired
    ObjectProvider<Proto> proto;

    public Proto getProto() {
        return proto.getIfAvailable();
    }
}
profile
Scratch, Under the hood, Initial version analysis

0개의 댓글