Spring IoC Container (3): IoC Container
이 글은 스프링 공식 문서를 기반으로 정리한다. 저번 글에 이어 이번 글에서는 IoC Container를 조금 더 자세히 알아본다.
Container 개요
org.springframework.context.ApplicationContext 인터페이스는 Spring IoC 컨테이너를 대표하며, 빈을 인스턴스화하고 설정하고 조립하는 역할을 담당한다.
컨테이너는 configuration metadata(설정 메타데이터)를 읽음으로써 인스턴스화, 설정 및 조립할 컴포넌트들에 대한 지침을 얻는다.
설정 메타데이터는 어노테이션이 달린 컴포넌트 클래스, 팩토리 메서드가 있는 설정 클래스, 또는 외부 XML 파일이나 Groovy 스크립트로 표현할 수 있다. 어떤 형식을 사용하든, 애플리케이션과 해당 컴포넌트들 간의 상호 의존성을 세팅 할 수 있다.
ApplicationContext 인터페이스의 여러 구현체는 Spring 코어의 일부로써 핵심이다.
독립형(stand-alone) 애플리케이션에서는 AnnotationConfigApplicationContext 또는 ClassPathXmlApplicationContext 인스턴스를 생성하는 것이 일반적.
대부분 애플리케이션 시나리오에서, 하나 이상의 Spring IoC 컨테이너 인스턴스를 생성하기 위해 명시적인 사용자 코드가 필요하지는 않다.
예를 들어, 일반적인 웹 애플리케이션 시나리오에서는 애플리케이션의 web.xml 파일에 있는 간단한 보일러 플레이트 web descriptor XML만으로도 충분하다(참조).
Spring Boot 시나리오에서는 일반적인(공통적인, common) 설정 관례에 따라 애플리케이션 컨텍스트가 암시적으로 부트스트랩된다.
스프링 부트에서는
SpringApplication.run()코드로서 컨테이너를 생성하고@ComponentScan과 자동 설정(Auto Configuration)으로 세팅이되는 반면 레거시에서는web.xml,applicationContext.xml에 컨테이너와 빈설정을 했다.
다음 다이어그램은 Spring의 동작 방식에 대한 high-level view를 보여준다.

애플리케이션 클래스는 설정 메타데이터와 결합되고, 이후에 ApplicationContext가 생성되고 초기화되면, 우리는 완전히 설정(configured)되어있고 실행 가능한 시스템 또는 애플리케이션을 소유하게 되는 것이다.
Configuration Metadata
위 다이어그램에서 보여주듯이, Spring IoC 컨테이너는 설정 메타데이터를 사용한다. 이 설정 메타데이터는 애플리케이션 개발자가 Spring 컨테이너에 애플리케이션의 컴포넌트를 인스턴스화하고, 설정하고, 조립하는 방법을 지시하는 방식을 나타내준다.
Spring IoC 컨테이너 자체는 이러한 설정 메타데이터가 실제로 작성되는 형식과 완전히 분리되어 있다.
요즘 많은 개발자가 Spring 애플리케이션을 위해 Java 기반 설정을 택한다.
- Annotation-based configuration: 애플리케이션의 컴포넌트 클래스들에 어노테이션 기반 설정 메타데이터를 사용하여 빈을 정의함
- Java-based configuration: Java 기반 설정 클래스를 사용하여 애플리케이션 클래스들의 외부에서 빈을 정의함
- 이러한 기능을 사용하려면
@Configuration,@Bean,@Import,@DependsOn어노테이션을 참조(스프링 부트가 이를 활용함)
- 이러한 기능을 사용하려면
Spring 설정은 컨테이너가 관리해야 하는 최소 하나 이상의 빈 정의로 구성된다.
Java configuration은 일반적으로 @Configuration 클래스 내에서 @Bean 어노테이션이 달린 메서드를 사용하며, 각 메서드는 하나의 빈 정의에 해당한다.
이런 빈 정의는 애플리케이션을 구성하는 실제 객체들이 된다.
일반적으로 서비스 계층 객체, 리포지토리나 데이터 액세스 객체(DAO)와 같은 영속성 계층 객체, 웹 컨트롤러와 같은 프레젠테이션 객체, JPA EntityManagerFactory나 JMS 큐와 같은 인프라 객체 등을 정의한다.
하지만 컨테이너에서 세밀한 도메인 객체까지 설정하지는 않는다. 왜냐하면 도메인 객체를 생성하고 로드하는 것이 보통 리포지토리와 비즈니스 로직의 책임이기 때문이다.
여기서 DDD 관련 개념이 등장. 도메인 객체는 컨테이너에서 관리 안하는게 스프링 프레임워크에서도 권장하는 듯하다.
외부 설정 DSL로서의 XML
DSL이란? Domain-Specific Language의 약자. 우리말로는 '도메인 특화 언어'로 번역. 모든 문제를 해결하기 위해 만들어진 범용 언어와 달리, 특정한 용도나 특정 분야(도메인)를 해결하기 위해 설계된 언어를 뜻한다.
XML 기반 설정 메타데이터는 이러한 빈들을 최상위 <beans/> 요소 내의 <bean/> 요소로 세팅한다. 다음 예제는 XML 기반 설정 메타데이터의 기본 구조다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="..." class="...">
</bean>
<bean id="..." class="...">
</bean>
</beans>id 속성은 개별 빈 정의를 식별하는 문자열. class 속성은 빈의 타입을 정의하며 정규화된 클래스 이름(fully qualified class name)을 사용한다.
id 속성의 값은 협력하는 객체들을 참조하는 데 사용될 수 있습니다. 협력 객체를 참조하기 위한 XML은 이 예제에 없다. 자세한 내용은 Dependencies를 참조해 볼 것.
컨테이너를 인스턴스화하려면 XML 리소스 파일에 대한 위치 경로를 ClassPathXmlApplicationContext 생성자에 제공해야 한다. 이를 통해 컨테이너는 로컬 파일 시스템, Java CLASSPATH 등 다양한 외부 리소스로부터 설정 메타데이터를 로드할 수 있다.
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");Spring의 IoC 컨테이너에 대해 배우고 나면, URI 구문으로 정의된 위치에서 InputStream을 읽기 위한 편리한 메커니즘을 제공하는 Spring의 Resource 추상화에 대해 더 알고 싶으면 여기를 참고하자. Application Contexts and Resource Paths에 설명된 대로 Resource 경로는 애플리케이션 컨텍스트를 구성하는 데 사용된다.
다음 예제는 서비스 계층 객체(services.xml) 설정 파일 예시.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
<property name="accountDao" ref="accountDao"/>
<property name="itemDao" ref="itemDao"/>
</bean>
</beans>다음 예제는 데이터 액세스 객체(DAO)인 daos.xml 파일.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="accountDao"
class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
</bean>
<bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
</bean>
</beans>앞선 예제에서 서비스 계층은 PetStoreServiceImpl 클래스와 JpaAccountDao, JpaItemDao(JPA 객체-관계 매핑 표준 기반) 타입의 두 데이터 액세스 객체로 구성된다.
property name 요소는 JavaBean 프로퍼티의 이름을 참조하고, ref 요소는 다른 빈 정의의 이름을 참조한다.
id와 ref 요소 간의 이러한 연결은 협력하는 객체들 사이의 의존성을 표현한다.
객체의 의존성을 설정하는 자세한 방법은 Dependencies를 참조하면된다.
XML 기반 설정 메타데이터 구성하기
빈 정의를 여러 XML 파일에 나누어 작성하는 것이 유용할 수 있다. 종종 각 개별 XML 설정 파일은 아키텍처의 논리적 계층이나 모듈을 낸다.
또한 우리는 ClassPathXmlApplicationContext 생성자를 사용하여 XML 조각들로부터 빈 정의를 로드할 수 도 있다.
이전 섹션에서 보여준 것처럼, 이 생성자는 여러 Resource 위치를 인자로 받는다.
다른 대안으로는, 하나 이상의 <import/> 요소를 사용하여 다른 파일로부터 빈 정의를 로드할 수 있다.
<beans>
<import resource="services.xml"/>
<import resource="resources/messageSource.xml"/>
<bean id="bean1" class="..."/>
<bean id="bean2" class="..."/>
</beans>위 예제에서, 외부 빈 정의는 services.xml과 messageSource.xml 파일로부터 로드된다.
모든 위치 경로는 임포트를 수행하는 정의 파일을 기준으로 한 상대 경로이므로, services.xml은 임포트하는 파일과 동일한 디렉토리나 클래스패스 위치에 있어야 하며, messageSource.xml은 임포트하는 파일 위치 하위의 resources 위치에 있어야 한다.
참고로 맨 앞의 슬래시는 무시해도된다.
임포트되는 파일의 내용은 최상위 <beans/> 요소를 포함하여 Spring 스키마에 따른 유효한 XML 빈 정의여야 한다.
마지막으로, 네임스페이스 자체가 임포트 지시어 기능(import directive feature)을 제공한다.
단순한 빈 정의를 넘어서는 추가 설정 기능은 Spring에서 제공하는 선택적인 XML 네임스페이스(예: context 및 util 네임스페이스)에서 사용할 수 있다.
(+)경로 설정에대해 더 주의할 점 및 조언이 있는데, 상대 경로인 "../"를 사용하여 상위 디렉토리의 파일을 참조하는 것은 가능하지만 권장않는다. 그렇게 하면 현재 애플리케이션 외부에 있는 파일에 대한 의존성이 생기기 때문이다.
특히 이 참조 방식은 classpath: URL(예: classpath:../services.xml)에 권장되지 않는데, 런타임 결정(resolution) 프로세스가 "가장 가까운" 클래스패스 루트를 선택한 다음 그 상위 디렉토리를 탐색하기 때문.
클래스패스 설정이 변경되면 잘못된 디렉토리를 선택하게 될 수도 있으니 주의하자.
상대 경로 대신 file:C:/config/services.xml이나 classpath:/config/services.xml과 같이 정규화된(fully qualified) 리소스 위치를 항상 사용할 수도 있다.
하지만 이 방식으 경우는 애플리케이션의 설정이 특정 절대 위치에 결합된다는 점에 유의해야한다.
일반적으로 이러한 절대 위치에 대해서는 간접 참조를 유지하는 것이 바람직하다.
예를 들어, 런타임에 JVM 시스템 프로퍼티에 따라 결정되는 "${...}" 플레이스홀더를 사용하는 방식이 있다.
Container 사용하기
ApplicationContext는 다양한 빈(bean)들과 그 의존 관계의 레지스트리를 유지 관리할 수 있는 고급(advanced) 팩토리 기능을 위한 인터페이스다.
T getBean(String name, Class<T> requiredType) 메서드를 사용하면 빈 인스턴스를 검색하여 가져올 수 있다.
// 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();Groovy 설정을 사용하는 경우에도 부트스트랩 과정은 이와 유사하다. Groovy를 인식할 수 있는(또한 XML 빈 정의도 이해할 수 있는) 다른 컨텍스트 구현 클래스를 사용하면 된다.
ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");가장 유연한 방법은 GenericApplicationContext를 리더 델리게이트(reader delegates)와 결합하여 사용하는 것이다.
예를 들어, XML 파일의 경우 XmlBeanDefinitionReader와 결합하며, 다음 예제와 같이 작성하면된다.
GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();다음과 같이 Groovy 파일에 대해 GroovyBeanDefinitionReader를 사용할 수도 있음.
GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();동일한 ApplicationContext에서 이러한 리더 델리게이트들을 혼합하여 사용할 수 있으며, 이를 통해 다양한 설정 소스로부터 빈 정의를 읽어올 수 있다.
설정 방법들이 매우 유연한걸 알 수 있다.
이렇게 빈 정의와 초기화가 다 되었으면 getBean을 사용하여 빈 인스턴스를 가져올 수 있는 것.
ApplicationContext 인터페이스에는 빈을 가져오기 위한 몇 가지 다른 메서드들이 있지만, 이상적으로는 애플리케이션 코드에서 이를 절대 사용하지 말아야한다.
즉, 애플리케이션 코드에는 getBean() 메서드 호출이 전혀 없어야 하며, 따라서 Spring API에 대한 의존성도 전혀 없어야 한다.
예를 들어, Spring과 웹 프레임워크의 통합 기능은 컨트롤러나 JSF-managed 빈과 같은 다양한 웹 프레임워크 컴포넌트에 대한 의존성 주입을 제공하므로, 메타데이터(예: autowiring 어노테이션)를 통해 특정 빈에 대한 의존성을 선언하여 사용할 수 있다.
JSF-managed 빈: 웹 화면과 백엔드 로직 사이를 연결하여 데이터와 이벤트를 전달하는 자바 객체. 사용자가 화면에 입력한 값을 자바 코드에 연결해주며, 프레임워크가 객체의 생성부터 소멸까지 직접 관리해준다.