잊지 않겠습니다.

8. ApplicationContext

Java 2013. 9. 9. 10:57

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



Spring에서 ApplicationContext는 IoC, AOP를 설정하는 Container 또는 Configuration이라고 할 수 있습니다.
ApplicationContext는 

# bean의 집합
# bean에 대한 Map
# bean에 대한 정의
# AOP에 의한 bean의 확장에 대한 정의

를 포함하고 있습니다. ApplicationContext에 대해서 좀 더 깊게 들어가보도록 하겠습니다.


ApplicationContext

Application Context는 org.springframework.context.ApplicationContext interface를 상속받은 객체입니다. interface의 정의는 다음과 같습니다. 

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
        MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
    String getId();
    String getApplicationName();
    String getDisplayName();
    long getStartupDate();
    ApplicationContext getParent();
    AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}

ApplicationContext는 bean에 대한 집합이라고 했습니다. bean은 일반적으로 POJO class로 구성이 되게 됩니다. bean과 POJO의 정의는 다음과 같습니다. 


Java Bean
원 목적은 Servlet에서 Java 객체를 가지고 와서 사용하기 위해서 작성된 객체입니다. 매우 간단한 객체이고, 사용이 편리해야지 된다. 라는 것을 원칙으로 가지고 있습니다. 특징으로는

# property를 갖는다. (private 변수와 get/set method를 갖는다.)
# serialization이 가능하다.

라는 특징을 갖습니다. 그렇지만 두번째 특징인 serialization이 가능한 특징은 지금은 거의 사용되고 있지 않습니다. property를 갖는 java 객체라는 의미로 생각해주시면 됩니다. 줄여서 bean이라는 표현을 많이 사용합니다. 

POJO
Plan Old Java Object의 약자입니다. POJO 객체는 특정 기술과 Spec에 독립적인 객체로 만들어지는 것을 원칙으로 삼습니다. 자신이 속한 package에 속한 POJO 객체 이외에는 Java의 기본 객체만을 이용해서 작성하는 것이 원칙입니다. 또한, 확장성을 위해 자신이 속한 package의 POJO 객체가 아닌 POJO 객체의 interface를 속성으로 갖는 bean 객체로서 만들어지는 것이 일반적입니다. 지금까지 작성된 Book, User, UserHistory 객체의 경우에 POJO 객체라고 할 수 있습니다. 

bean에 대한 집합인 ApplicationContext는 다음과 같은 특징을 갖습니다.

1. bean id, name, alias로 구분이 가능한 bean들의 집합입니다.
2. life cycle을 갖습니다. (singleton, prototype)
3. property는 value 또는 다른 bean을 참조합니다.

ApplicationContext는 bean들의 집합적인 특징 뿐 아니라, bean들의 loading 도 역시 담당하고 있습니다. ApplicationContext의 정보는 일반적으로 xml을 이용하고 있지만, 지금까지 저희가 사용한 내용을 보셨듯이 annotation을 이용한 bean의 등록 역시 가능합니다. 이번에 사용한 내용 그대로, xml과 annotation을 혼용하는 것이 일반적입니다. 그리고 아직까지 한번도 사용안해본 xml을 전혀 사용하지 않는 ApplicationContext역시 가능합니다. 

먼저 ApplicationContext를 수동으로 만들어보는 것을 알아보겠습니다.

프로젝트를 만듭니다. 지금까지 구성하던것 처럼, maven-archetype-quickstart로 maven project를 하나 생성합니다. 생성된 project에 spring context 선언을 pom.xml에 추가합니다.

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>


Hello 객체와 Printer interface를 선언하고 ConsolePrinter 객체를 만들어보도록 합시다.

public class Hello {
    private String name;
    private Printer printer;
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Printer getPrinter() {
        return printer;
    }
    public void setPrinter(Printer printer) {
        this.printer = printer;
    }
    public String sayHello() {
        return "Hello " + name;
    }
    public void print() {
        this.printer.print(sayHello());
    }
}

public interface Printer {
    void print(String message);
}

public class ConsolePrinter implements Printer {
    public void print(String message) {
        System.out.println(message);
    }
}


먼저, Spring에서 제공하는 StaticApplicationContext를 사용해서 ApplicationContext에 직접 bean을 등록하고 얻어보는 것을 해보도록 하겠습니다.

테스트 코드를 간단히 작성해보도록 하겠습니다.

    @Test
    public void registerApplicationContext() {
        StaticApplicationContext ac = new StaticApplicationContext();
        ac.registerSingleton("hello1", Hello.class);
        Hello hello = ac.getBean("hello1", Hello.class);
        assertNotNull(hello);
    }

 
이렇게 만들어진 ApplicationContext에서 hello1이라는 이름의 객체를 계속해서 얻어오는 것이 가능합니다. 하나 재미있는 것이 다음 코드입니다. 

    @Test
    public void registerApplicationContext() {
        StaticApplicationContext ac = new StaticApplicationContext();
        ac.registerSingleton("hello1", Hello.class);
        Hello hello = ac.getBean("hello1", Hello.class);
        assertNotNull(hello);
        Hello hello2 = ac.getBean("hello1", Hello.class);
        assertThat(hello, is(hello2));
    }

    @Test
    public void registerApplicationContextWithPrototype() {
        StaticApplicationContext ac = new StaticApplicationContext();
        ac.registerPrototype("hello1", Hello.class);
        Hello hello = ac.getBean("hello1", Hello.class);
        assertNotNull(hello);
        Hello hello2 = ac.getBean("hello1", Hello.class);
        assertThat(hello, is(not(hello2)));
    }

registerApplicationContext와 registerApplicationContextWithProtytype은 Hello 객체를 Singleton 또는 Prototype으로 등록하게 됩니다. Singleton은 ApplicationContext에 등록된 모든 객체들을 재사용하게 되는데, registerPrototype으로 등록된 객체들은 ApplicationContext에서 얻어낼 때마다 객체를 다시 생성해서 얻어내게 됩니다. 이는 xml의 설정에서 scope와 동일합니다. 위 코드는 다음 xml로 표현이 가능합니다. 

<bean id="hello1" class="com.xyzlast.ac.Hello" scope="singleton"/>
<bean id="hello2" class="com.xyzlast.ac.Hello" scope="prototype"/>


이제 bean에 대한 property를 설정하는 코드에 대해서 알아보도록 하겠습니다. Property를 설정하기 위해서는 BeanDefinition 객체를 사용해야지 됩니다. 테스트 코드를 보면서 간단히 확인해보도록 하겠습니다. 

   @Test
    public void registerBeanDef() {
        BeanDefinition helloDef = new RootBeanDefinition(Hello.class);
        helloDef.getPropertyValues().add("name", "ykyoon");
        helloDef.getPropertyValues().add("printer", new ConsolePrinter());
        StaticApplicationContext ac = new StaticApplicationContext();
        ac.registerBeanDefinition("hello1", helloDef);
        
        Hello hello = ac.getBean("hello1", Hello.class);
        assertNotNull(hello);
        assertThat(hello.sayHello(), is("Hello ykyoon"));
    }


ApplicationContext의 종류

spring에서는 십여개의 applicationContext를 제공하고 있습니다. 다음은 Spring에서 제공하는 applicationContext의 종류입니다. 





StaticApplicationContext
Test code에서 사용한 ApplicationContext입니다. 실 프로젝트에서는 절대로 사용되지 않는 ApplicationContext입니다. xml로딩 기능이나 그런것들도 없고, 테스트 코드에서 사용해보신 것과 같이 객체에 대한 IoC 기능의 테스트에서만 사용되는 객체입니다.

GenericApplicationContext
xml을 이용하는 ApplicationContext입니다. 우리가 만든 xml 파일과 동일한 xml을 이용한 ApplicationContext 구성이 가능합니다. 지금까지 사용한 Test code에서 사용된 ApplicationContext가 모두 GenericApplicationContext입니다.

GenericXmlApplicationContext
GenericApplicationContext의 확장판입니다. xml 파일의 경로가 생성자에 들어가 있어서, 좀더 편하게 xml을 로딩할 수 있는 장점 이외의 차이점은 없습니다.

WebApplicationContext
web project에서 사용되는 ApplicationContext입니다. web.xml의 org.springframework.web.context.ContextLoaderListener를 통해서 로드 및 생성이 됩니다.

이 4개의 ApplicationContext는 매우 자주 사용되는 형태입니다. 각각의 간단한 특징들만 알아두는것이 좋습니다.


ApplicationContext의 계층 구조


spring forum에서 가장 많이 나오는 질문들이 있습니다.
"bean 을 등록했는데, 사용할 수가 없어요."
Bean이 xml에 등록이 되어있으나, 사용하지 못하는 경우가 간간히 나옵니다. 그 이유는 Spring에 있는것이 아니라 Bean의 계층구조를 이해하지 못하고 Bean을 등록해서 사용하고 있기 때문입니다. 

ApplicationContext는 계층 구조를 가질 수 있습니다. 다음과 같은 구조화가 가능합니다.

여기서 주의할 점은 형제 node끼리는 bean을 검색할 수 없습니다. upper node에 있는 객체와 자신의 객체만을 사용할 수 있고, 형제 node에 있는 bean들은 검색할 수 없습니다. 그리고, upper node에 있는 bean 이름과 동일한 bean 이름을 갖는 객체를 선언하면, 자식의 node에 있는 객체로 덧씌워져 버립니다. 이런 계층구조 사이의 혼란한 bean 구조는 매우 힘든 버그를 발생시킬 수 있습니다. bean을 정의할 때, 이런 부분을 주의해야지 될 필요성이 있습니다. 

그럼, 이런 ApplicationContext간의 계층구조는 왜 만들게 되는지가 궁금해질 수 가 있습니다.
이 부분은 webApplicationContext를 만들때 이런 구조가 만들어집니다. Spring Web MVC를 사용하는 경우, Root ApplicationContext는 web.xml에 정의되고 로드됩니다. 그리고 Servlet을 정의하고 DispatcherServlet을 사용하면, DispatcherServlet에서 사용되는 child ApplicationContext가 로드가 되게 됩니다. Spring Web은 기본적으로 Front Controller Pattern을 사용하기 때문에 child ApplicationContext가 하나만 로드가 되는 것이 일반적이지만, 간혹 경우에 따라 child ApplicationContext를 여러개를 로드시켜야지 되는 때가 있습니다. 각 url에 따라 다른 Servlet을 사용하고 싶은 경우도 생길수 있으니까요. 그때는 여러개의 형제 ApplicationContext가 만들어지게 되고 이 ApplicationContext는 서로간에 bean을 사용할 수 없게 됩니다. 그리고, 각 ApplicationContext에서 따로 bean을 등록하는 경우에는 각각 다른 bean 정보를 갖게 됩니다. 

다음은 Web application에서 ApplicationContext의 기본 구조입니다.





ApplicationContext.xml의 등록 방법

applicationContext 의 등록방법은 spring에서 계속해서 발전되어가는 분야중 하나입니다. 총 3가지의 방법으로 나눌 수 있으며, 이 방법들은 같이 사용되는 것도 가능합니다. 

1. applicationContext.xml 을 이용하는 방법
bookDao를 이용할 때, 처음 사용한 방법입니다. bean을 선언하고, id값을 이용해서 사용하는 방법으로 이 방법은 가장 오랫동안 사용해왔기 때문에 많은 reference들이 존재합니다. 그렇지만, 객체가 많아질수록 파일의 길이가 너무나 길어지고 관리가 힘들어지는 단점 때문에 요즘은 잘 사용되지 않습니다. 

2. @annotation과 aplicationContext.xml을 이용하는 방법
@Component, @Repository, @Service, @Controller, @Value 등을 사용하고, component-scan 을 이용해서 applicationContext.xml에서 등록하는 방법입니다. 이 방법은 지금 가장 많이 사용되고 있는 방법입니다. applicationContext.xml의 길이가 적당히 길고, 구성하기 편하다는 장점을 가지고 있습니다. 
이 방법은 반드시 알아둬야지 됩니다. 지금 정부표준 프레임워크 및 대부분의 환경에서 이 방법을 사용하고 있습니다.

3. @Configuration을 이용한 applicationContext 객체를 이용하는 방법
Spring에서 근간에 밀고 있는 방법입니다. 이 방법은 다음과 같은 장점을 가지고 있습니다. 

1) 이해하기 쉽습니다. - xml에 비해서 사용하기 쉽습니다.
2) 복잡한 Bean 설정이나 초기화 작업을 손쉽게 적용할 수 있습니다. - 프로그래밍 적으로 만들기 때문에, 개발자가 다룰수 있는 영역이 늘어납니다.
3) 작성 속도가 빠릅니다. - eclipse에서 java coding 하는것과 동일하게 작성하기 때문에 작성이 용의합니다. 

그리고, 개인적으로는 2, 3번 방법을 모두 알아둬야지 된다고 생각합니다. 이유는 2번 방법의 경우, 가장 많이 사용되고 있다는 점이 가장 큰 장점입니다. 또한 아직 Spring의 하부 Project인, Spring Security를 비롯하여 Work Flow등은 아직 3번 방법을 지원하지 않습니다. (다음 버젼에서 지원 예정입니다.) 그래도 3번 방법을 알아야지 됩니다. 이유는 다음과 같습니다. 지금 Spring에서 밀고 있습니다. 그리고, 최근에 나온 외국 서적들이 모두 이 방법을 기준으로 책을 기술하고 있습니다. 마지막으로, 나중에 나올 web application의 가장 핵심인 web.xml이 없는 개발 방법이 servlet 3.0에서 지원되기 시작했습니다. Java 언어에서 xml을 이용한 설정 부분을 배재하는 분위기로 흘러가고 있다는 것이 제 개인적인 판단입니다. 

그럼, 이 3가지 방법을 모두 이용해서 기존의 BookStore를 등록하는 applicationContext를 한번 살펴보도록 하겠습니다.

applicationContext.xml만을 이용하는 방법
초기 Spring 2.5 이하 버젼에서 지원하던 방법입니다. 일명 xml 지옥이라고 불리우는 어마어마한 xml을 자랑했습니다. 최종적으로 만들어져 있는 applicationContext.xml의 구성은 다음과 같습니다. Transaction annotation이 적용되지 않기 때문에, Transaction에 대한 AOP code 까지 추가되는 엄청나게 긴 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" xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">
  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource" />
  </bean>
  <bean id="dataSource" class="com.jolbox.bonecp.BoneCPDataSource"
    destroy-method="close">
    <property name="driverClass" value="${connect.driver}" />
    <property name="jdbcUrl" value="${connect.url}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
    <property name="idleConnectionTestPeriodInMinutes" value="60" />
    <property name="idleMaxAgeInMinutes" value="240" />
    <property name="maxConnectionsPerPartition" value="30" />
    <property name="minConnectionsPerPartition" value="10" />
    <property name="partitionCount" value="3" />
    <property name="acquireIncrement" value="5" />
    <property name="statementsCacheSize" value="100" />
    <property name="releaseHelperThreads" value="3" />
  </bean>
  <context:property-placeholder location="classpath:spring.properties" />
  <bean id="bookDao" class="com.xyzlast.bookstore02.dao.BookDaoImpl">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
  </bean>
  <bean id="userDao" class="com.xyzlast.bookstore02.dao.UserDaoImpl">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
  </bean>
  <bean id="historyDao" class="com.xyzlast.bookstore02.dao.HistoryDaoImpl">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
  </bean>
  <bean id="userService" class="com.xyzlast.bookstore02.services.UserServiceImpl">
    <property name="bookDao" ref="bookDao"/>
    <property name="userDao" ref="userDao"/>
    <property name="historyDao" ref="historyDao"/>
  </bean>
  <bean id="bookService" class="com.xyzlast.bookstore02.services.HistoryServiceImpl">
    <property name="bookDao" ref="bookDao"/>
    <property name="userDao" ref="userDao"/>
    <property name="historyDao" ref="historyDao"/>
  </bean>
  
  <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
  </bean>
  <bean id="transactionManagerAdvisor" class="com.xyzlast.bookstore02.utils.TransactionAdvisor">
    <property name="transactionManager" ref="transactionManager"/>
  </bean>
  
  <bean
    class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames">
      <list>
        <value>userService</value>
        <value>bookService</value>
      </list>
    </property>
    <property name="interceptorNames">
      <list>
        <value>transactionManagerAdvisor</value>
      </list>
    </property>
  </bean>
</beans>

@annotation + applicationContext.xml을 이용한 방법
@Repository, @Component, @Service를 이용해서 편한 방법을 제공합니다. 특히 component-scan과 @Autowired를 이용하면 위 applicationContext.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" xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">
  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource" />
  </bean>
  <bean id="dataSource" class="com.jolbox.bonecp.BoneCPDataSource"
    destroy-method="close">
    <property name="driverClass" value="${connect.driver}" />
    <property name="jdbcUrl" value="${connect.url}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
    <property name="idleConnectionTestPeriodInMinutes" value="60" />
    <property name="idleMaxAgeInMinutes" value="240" />
    <property name="maxConnectionsPerPartition" value="30" />
    <property name="minConnectionsPerPartition" value="10" />
    <property name="partitionCount" value="3" />
    <property name="acquireIncrement" value="5" />
    <property name="statementsCacheSize" value="100" />
    <property name="releaseHelperThreads" value="3" />
  </bean>
  <context:property-placeholder location="classpath:spring.properties" />
  <context:component-scan base-package="com.xyzlast.bookstore02.dao" />
  <context:component-scan base-package="com.xyzlast.bookstore02.services" />
  <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
  </bean>
  <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

@annotation + @Configuration을 이용한 방법
이 방법은 xml을 아애 없애버릴 수 있습니다. xml이 제거된 ApplicationContext의 내용은 다음과 같습니다.
@Configuration
@PropertySource("classpath:spring.properties")
@ComponentScan(basePackages = {"com.xyzlast.bookstore02.dao", "com.xyzlast.bookstore02.services"})
@EnableTransactionManagement
public class BookStoreConfiguration {
    @Autowired
    private Environment env;

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        PropertySourcesPlaceholderConfigurer configHolder = new PropertySourcesPlaceholderConfigurer();
        return configHolder;
    }

    @Bean
    public DataSource dataSource() {
        BoneCPDataSource dataSource = new BoneCPDataSource();
        dataSource.setUsername(env.getProperty("connect.username"));
        dataSource.setPassword(env.getProperty("connect.password"));
        dataSource.setDriverClass(env.getProperty("connect.driver"));
        dataSource.setJdbcUrl(env.getProperty("connect.url"));
        dataSource.setMaxConnectionsPerPartition(20);
        dataSource.setMinConnectionsPerPartition(3);
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        JdbcTemplate template = new JdbcTemplate();
        template.setDataSource(dataSource());
        return template;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource());
        return transactionManager;
    }
}

지금까지 작성하던 xml과는 완전히 다른 모습의 ApplicationContext입니다. ApplicationContext Config 객체는 다음과 같은 특성을 갖습니다. 

1. @Configuration annotation을 갖는다.
2. bean으로 등록되는 객체는 method로 관리되고, @Bean annotation을 갖습니다.
3. method의 이름이 <bean id="">값과 매칭됩니다.
4. component-scan, property-place-holder의 경우, class의 annotation으로 갖습니다.
5. @EnableTransactionManagement와 같이 @Enable** 로 시작되는 annotation을 이용해서 전역 annotation을 구성합니다. 
6. Properties 파일을 사용하기 위해서는 반드시 static method로 PropertySourcesPlaceholderConfigurer를 return 시켜줘야지 됩니다.

앞으로 적용되는 모든 Project는 @Configuration을 이용한 3번 방법으로 구성하도록 하겠습니다. 그리고 xml 역시 같이 소개하도록 하겠습니다.

Summay

지금까지 사용하던 ApplicationContext에 대한 기본 개념을 정리해봤습니다. ApplicationContext는 Spring의 핵심 기능입니다. DI를 통한 IoC를 가능하게 하고, AOP에 대한 설정 등 모든 Spring에서 하는 일이 설정되어 있는 것이 ApplicaionContext라고 할 수 있습니다. 이에 대한 설정 및 구성을 명확하게 알아놓을 필요가 있습니다. 그리고, 내부에서 어떤 일을 해서 Spring에서 하는 이런 일들이 가능하게 되는지에 대한 이해가 필요합니다. 마지막으로 ApplicationContext를 구성하는 방법에 대해서 알아봤습니다. 제공되는 3가지 방법에 대해 자유자재로 사용할 수 있는 능력이 필요합니다.

감사합니다. 




Posted by Y2K
,