Spring IoC와 DI에 대해 정리.

토비의 스프링 3.1과 스프링 철저입문 도서, 타 블로그의 정리 내용을 보고 정리했습니다.

책 위주로 정리한 내용이고 풀어서 정리되어있기 때문에 간단한 정리는 타 블로그 참고를 추천드립니다.

 

Reference

  • 토비의 스프링 3.1
  • 스프링 철저 입문

 

IoC와 DI에 대한 정리는 아래와 같은 순서로 진행.

  1. IoC Container란
  2. IoC Container의 종류와 사용방법
  3. Web Application의 IoC Container 구성
  4. Bean 설정 방식, DI 방식
  5. Autowiring, ComponentScan
  6. Bean Scope
  7. Bean 설정 분할과 profile별 설정
  8. Bean 생명주기, IoC Container 종료

 

 

이번 포스팅은 두개로 나눠서 작성한다.

1. IoC Container의 사용방법

2. IoC 컨테이너의 종류

 

1. IoC Container의 사용방법

IoC 컨테이너의 종류를 확인하기 전에 IoC 컨테이너를 사용하는 애플리케이션을 만드는 방법과 동작원리에 대해 정리한다.

가장 간단하게 IoC 컨테이너를 만드는 방법은 ApplicationContext 구현 클래스의 인스턴스를 만드는 것이다.

그리고 이 컨테이너를 본격적인 IoC 컨테이너로 동작시키기 위해 POJO 클래스와 설정 메타정보가 필요하다.

 

토비의 스프링 3.1에 나온 예제로 클래스를 생성한다.

지정된 사람에게 인사를 하는 Hello 클래스와 

메세지를 받아 이를 출력하는 Printer 인터페이스,

이를 구현한 StringPrinter 클래스와 다른 방법으로 구현한 ConsolePrinter 클래스를 만들어준다.

 

출처 : 토비의 스프링 3.1

 

//Hello Class
public class Hello{
	
    String name;
    Printer printer;
    
    public String sayHello(){
    	return "Hello " + name;
        //property로 DI 받은 이름을 이용해 만드는 간단한 인사문구
    }
    
    public void print(){
    	this.printer.print(sayHello());
        /*
        	DI에 의해 의존 오브젝트로 제공받은 Printer 타입의 Object에게 출력작업을 위임.
            구체적으로 어떤 방식으로 출력하는지는 상관하지 않는다.
            또한 어떤 방식으로 출력하도록 변경해도 Hello 클래스의 코드는 수정할 필요가 없다.
        */
    }
    
    public void setName(String name){
    	this.name = name;
    }
    
    public void setPrinter(Printer printer){
    	this.printer = printer;
    }
}


//Printer Interface
public interface Printer{
	
    void printer(String message);

}


//StringPrinter class
public class StringPrinter implements Printer{
	
    private StringBuffer buffer = new StringBuffer();
    
    @Override
    public void print(String message){
    	this.buffer.append(message);
        //Printer 인터페이스의 메소드 구현.
        //buffer에 메세지를 추가해준다.
    }
    
    public String toString(){
    	return this.buffer.toString();
        //buffer에 추가해둔 메세지를 String으로 가져온다.
    }
}


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

 

Hello 클래스는 Printer 인터페이스에만 의존하고있으며 구현클래스인 StringPrinter, ConolePrinter와 Printer인터페이스를 사이에 두고 느슨하게 연결되어있다.

서로 구체적으로 알 필요도 없고 관계를 맺을 수 있도록 필요한 최소한의 인터페이스 정보만 공유하면 된다.

실제로 Hello클래스는 runtime시에 어떤 구체적인 클래스의 오브젝트를 사용하게 될지 알지도 못하고 관심도 없다.

그래서 Printer 인터페이스의 구현 클래스를 변경하더라도 Hello클래스의 코드 변경은 필요 없고 단지 런타임시에 오브젝트를 연결해주는 IoC 컨테이너의 도움만 있으면 된다.

 

이렇게 각자 기능에 충실하게 독립적으로 설계된 POJO 클래스를 만들고 결합도가 낮은 유연한 관계를 가질 수 있도록 인터페이스를 이용해 연결해주는 것 까지가 IoC 컨테이너가 사용할 POJO를 준비하는 단계다.

 

다음은 설정 메타정보가 필요하다.

POJO클래스들 중에 애플리케이션에서 사용할 것을 선정하고 이를 IoC 컨테이너가 제어할 수 있도록 적절한 메타정보를 만들어 제공하는 작업이다.

 

IoC 컨테이너의 가장 기초적인 역할은 오브젝트를 생성하고 이를 관리하는 것인데 컨테이너가 관리하는 오브젝트를 Bean이라고 부른다.

IoC 컨테이너가 필요로 하는 설정 메타정보는 이 Bean을 어떻게 만들고 어떻게 동작하게 할 것인가에 대한 정보다.

 

스프링의 메타정보는 XML 파일만을 지칭하는 것이 아니다.

주로 XML에 담긴 내용을 읽어서 활용하는것은 사실이지만, 스프링의 설정 메타정보는 BeanDefinition 인터페이스로 표현되는 순수한 추상 정보다.

스프링 IoC 컨테이너, 즉 ApplicationContext는 이 BeanDefinition으로 만들어진 메타정보를 담은 오브젝트를 사용해 IoC와 DI 작업을 수행한다.

어떤 파일이던 상관없이 BeanDefinition으로 정의되는 스프링의 설정 메타정보 내용을 표현한 것이 있다면 무엇이든 사용가능하고 BeanDefinition 오브젝트로 변환해주는 BeanDefinitionReader가 있으면 된다.

BeanDefinitionReader는 인터페이스이고 이를 구현한 리더를 만들어주면 어떤 형식으로든 설정 메타정보를 작성할 수 있다.

 

BeanDefinition 인터페이스로 정의되는 IoC 컨테이너가 사용하는 Bean 메타정보는 대략 다음과 같다.

  • Bean 아이디, 이름, 별칭 : Bean 오브젝트로 구분할 수 잇는 식별자
  • 클래스 또는 클래스 이름 : Bean으로 만들 POJO 클래스 또는 서비스 클래스 정보
  • Scope : singleton, prototype과 같은 빈의 생성 방식과 존재 범위
  • property 값 또는 reference : DI에 사용할 property 이름과 value 또는 참조하는 Bean의 이름
  • constructor 파라미터(constuctor-arg) value 또는 참조 : DI에 사용할 constructor 파라미터 이름과 value또는 참조할 Bean의 이름
  • lazy-loading 여부, 우선 Bean 여부, Autowiring 여부, 부모 Bean 정도, BeanFactory 이름 등

스프링 IoC 컨테이너는 각 Bean에 대한 정보를 담은 설정 메타정보를 읽어들인 후에 이를 참고해 Bean 오브젝트를 생성하고 property나 constructor를 통해 의존 오브젝트를 주입해주는 DI 작업을 수행한다.

이 작업을 통해 만들어지고 DI로 연결되는 오브젝트들이 모여 하나의 애플리케이션을 구성하고 동작하게 된다.

 

일반적으로 설정 메타정보는 XML 파일이나 Annotation과 같은 외부 리소스를 전용 리더가 읽어서 BeanDefinition타입의 오브젝트로 만들어 사용한다.

원한다면 직접 코드에서 BeanDefinition 메타정보를 생성할 수도 있다.

 

 

아래 예제는 Hello 클래스를 IoC 컨테이너에 Bean으로 등록하는 예제다.

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@RunWith(SpringJUnit4Runner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class testClass{
	
    @Test
    public void IoCTest(){
    	//IoC 컨테이너 생성. 생성과 동시에 컨테이너로 동작.
        StaticApplicationContext context = new StaticApplicationContext();
        
        //Hello 클래스를 hello1이라는 이름의 singleton Bean으로 컨테이너에 등록.
        context.registerSingleton("hello1", Hello.class);
        
        //IoC 컨테이너가 등록한 Bean을 생성했는지 확인하기 위해 Bean을 요청하고 Null이 아닌지 확인.
        Hello hello1 = context.getBean("hello1", Hello.class);
        assertNotNull(hello1);
    }
}

 

Bean 메타정보의 항목들은 대부분 default값이 있다.

singleton으로 관리되는 Bean 오브젝트를 등록할 때 반드시 제공해줘야 하는 정보는 Bean의 이름과 POJO클래스 뿐이다.

 

IoC컨테이너가 관리하는 Bean은 오브젝트 단위지 클래스 단위가 아니다.

보통은 클래스당 하나의 오브젝트를 만들기는 하지만 경우에 따라 하나의 클래스를 여러개의 Bean으로 등록하기도 한다.

예를들어 데이터베이스를 여러개 사용해야 한다면 dataSource Bean을 여러개 등록하고 각각 다른 데이터베이스 설정을 지정해서 사용하는 경우가 있다.

 

위 예제처럼 default 메타정보를 사용해 Bean을 등록해주는 방법과는 조금 다른 방법으로 직접 BeanDefinition 오브젝트를 만들어 Bean에 대한 설정정보를 넣어주고 IoC 컨테이너에 등록하는 방법도 있다.

 

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.jupiter.api.Assertions.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/applicationContext.xml"})
public class testClass{

    @Test
    public void IoCTest(){
        //hello1 Bean 생성 후 컨테이너에 등록.
        StaticApplicationContext context = new StaticApplicationContext();
        context.registerSingleton("hello1", Hello.class);

        Hello hello1 = context.getBean("hello1", Hello.class);

        //hello2 Bean 생성 후 컨테이너에 등록
        //Bean 메타정보를 담은 오브젝트 생성. Bean Class는 Hello로 지정.
        //<bean class="com.example.Hello" /> 에 해당하는 메타정보
        BeanDefinition helloDef = new RootBeanDefinition(Hello.class);
        
        //Bean의 name property에 들어갈 값을 지정.
        //<property name="name" value="Spring" />에 해당.
        helloDef.getPropertyValues().addPropertyValue("name", "Spring");

        //앞에서 생성한 Bean 메타 정보를 hello2라는 이름을 가진 Bean으로 등록.
        //<bean id="hello2" ..... /> 에 해당
        context.registerBeanDefinition("hello2", helloDef);

        Hello hello2 = context.getBean("hello2", Hello.class);


        //hello2.sayHello()의 출력 결과 확인
        assertEquals(hello2.sayHello(), "Hello Spring");
        //hello1과 hello2가 서로다른 오브젝트인지 확인.
        assertNotEquals(hello1, hello2);

        //IoC 컨테이너에 등록된 Bean 설정 메타정보.
        //Bean의 이름, 등록된 Bean의 개수 등등의 정보를 확인가능.
        for(String a : context.getBeanFactory().getBeanDefinitionNames())
            System.out.println(a);

        assertEquals(context.getBeanFactory().getBeanDefinitionCount(), 2);
    }
}

 

 

IoC 컨테이너는 Bean 설정 메타정보를 담은 BeanDefinition을 이용해 오브젝트를 생성하고 DI 작업을 진행한 뒤에 Bean으로 사용할 수 있도록 등록해준다.

이때 BeanDefinition의 class, property, Bean 아이디 등의 정보가 활용된다.

 

위 예제를 통해 Bean은 오브젝트 단위로 등록되고 만들어지기 때문에 같은 클래스 타입이더라도 서로 다른 Bean 오브젝트를 생성할 수 있다는 것을 확인할 수 있다.

그리고 예제 하단처럼 Bean 설정 메타정보를 확인가능하다.

 

예제 주석에 XML 파일 설정시에 어디에 해당하는 코드인지 적어뒀는데 이 설정을 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-4.3.xsd">
    
    <context:component-scan base-package="com.example" />

    <bean id="hello1" class="com.example.Hello">
    </bean>

    <bean id="hello2" class="com.example.Hello">
        <property name="name" value="Spring">
    </bean>
</beans>

 

그리고 이게 제대로 설정이 된것인지 확인하는 방법은 아래처럼 확인한다.

 

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.jupiter.api.Assertions.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/applicationContext.xml"})
public class testClass{
    @Autowired
    private ApplicationContext context;

    @Test
    public void IoCTest(){
        String[] beans = context.getBeanDefinitionNames();

        for(String res : beans)
            System.out.println(res);

        Hello hello2 = context.getBean("hello2", Hello.class);

        assertEquals(hello2.sayHello(), "Hello Spring");
    }
}

 

Hello 클래스와 StringPrinter 클래스는 Printer 인터페이스를 사이에 두고 느슨하고 간접적인 관계를 맺고 있다.

이 두 클래스의 오브젝트간의 관계는 설정 메타정보를 참고해서 runtime시에 IoC 컨테이너가 주입해준다.

 

그럼 위 예제에서 Hello 클래스의 sayHello() 메소드를 호출해 "Hello " + name을 리턴 받았다면 이번에는 Hello 타입과 StringPrinter 타입의 빈을 hello와 printer라는 Bean 이름으로 생성한 뒤 printer Bean이 hello Bean에게 DI 되도록 한다.

 

import com.example.Hello;
import com.example.StringPrinter;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.jupiter.api.Assertions.*;


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/applicationContext.xml"})
public class testClass {

    @Test
    public void IoCTest(){
        StaticApplicationContext context = new StaticApplicationContext();

        //StringPrinter 타입의 printer라는 이름을 가진 bean 등록.
        context.registerBeanDefinition("printer", new RootBeanDefinition(StringPrinter.class));

        //Hello 클래스로 지정된 Bean 메타정보를 담은 오브젝트 생성.
        BeanDefinition helloDef = new RootBeanDefinition(Hello.class);
        //단순 값을 갖는 프로퍼티 등록.
        helloDef.getPropertyValues().addPropertyValue("name", "Spring");
        //아이디가 printer인 Bean에 대한 Reference를 property로 등록
        helloDef.getPropertyValues().addPropertyValue("printer", new RuntimeBeanReference("printer"));
        //앞서 생성한 Bean 메타정보를 hello라는 이름의 Bean으로 등록.
        context.registerBeanDefinition("hello", helloDef);

        Hello hello = context.getBean("hello", Hello.class);
        hello.print();

        assertEquals(context.getBean("printer").toString(), "Hello Spring");
    }
}

Hello 클래스의 print() 메소드는 DI 된 Printer 타입의 오브젝트(StringPrinter)에게 요청해서 사용해야 하기 때문에 결과를 스트림으로 저장해두는 printer Bean을 통해 확인하게 된다.

 

이것 역시 applicationContext.xml에 작성하고 테스트하면 아래처럼 하면 된다.

//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-4.3.xsd">
    
    <context:component-scan base-package="com.example" />

    <bean id="hello1" class="com.example.Hello">
    </bean>

    <bean id="hello2" class="com.example.Hello">
        <property name="name" value="Spring">
        <property name="printer" ref="printer"/>
    </bean>

    <bean id="printer" class="com.example.StringPrinter">
    </bean>
</beans>


//test class
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/applicationContext.xml"})
public class testClass{

    @Autowired
    private ApplicationContext context;

    @Test
    public void IoCTest(){
        Hello hell2 = (Hello) context.getBean("hello2");
        hello2.print();

        assertEquals(context.getBean("printer").toString(), "Hello Spring");
    }
}

이렇게 애플리케이션을 구성하는 Bean 오브젝트를 생성하는 것이 IoC 컨테이너의 핵심 기능이다.

IoC 컨테이너는 일단 Bean 오브젝트가 생성되고 관계가 만들어지면 그 뒤로는 거의 관여하지 않는다.

기본적으로 singleton Bean은 ApplicationContext 초기화 작업중에 모두 만들어진다.

 

 

마지막으로 IoC 컨테이너에서 Bean을 가져오는 방법은 아래처럼 몇가지가 존재한다.

Hello hello = context.getBean(Hello.class);
Hello hello = context.getBean("hello1", Hello.class);
Hello hello = (Hello) context.getBean("hello1");

첫번째 방법은 가져오려는 Bean의 타입을 지정하는 방법이다.

지정한 타입에 해당하는 Bean이 IoC 컨테이너에 오직 하나만 존재할 때 사용하며 스프링 3.0.0부터 사용할 수 있다.

 

두번째 방법은 가져오려는 Bean의 이름(hello1)과 타입(Hello.class)을 지정하는 방법이다.

지정한 타입에 해당하는 Bean이 IoC 컨테이너에 여러개 있을 때 이름으로 구분하기 위해 사용한다.

스프링 2.5.6까지는 반환값이 Object 타입이라서 원하는 Bean의 타입으로 형변환 해야 하지만 3.0.0부터는 형변환을 하지 않아도 된다.

 

마지막 방법은 가져오려는 Bean의 이름(hello1)을 지정하는 방법이다.

반환값이 Object 타입이라서 원하는 빈의 타입으로 형변환을 해야 하며 스프링 1.0.0부터 사용할 수 있다.

 

2. IoC Container의 종류

 

ApplicationContext 인터페이스를 바르게 구현했다면 어떤 클래스든 스프링의 IoC 컨테이너로 사용할 수 있다.

스프링에는 다양한 용도로 쓸 수 있는 ApplicationContext 구현 클래스가 존재하기 때문에 개발자가 직접 구현할 일은 거의 없다.

근데 스프링 애플리케이션에서 직접 코드를 통해 ApplicationContext 오브젝트를 생성하는 경우는 거의 없다.

대부분 간단한 설정을 통해 ApplicationContext가 자동으로 만들어지는 방법을 사용하기 때문이다.

 

책을 두개를 보면서 같이 정리하다보니 이 구현클래스 설명에 대한 차이가 있었다.

일단 스프링 철저 입문에서는 아래 네가지를 설명했다.

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
/*
    자바 기반 설정 방식으로 AnnotationConfigApplicationContext의 생성자에 @Configuration 어노테이션이 붙은 클래스를 인수로 전달한다.
*/
ApplicationContext context = new AnnotationConfigApplicationContext("com.example.app");
/*
    어노테이션 기반의 설정 방식으로 AnnotationConfigApplicationContext의 생성자에 패키지 명을 인수로 전달한다.
    그럼 지정된 패키지 이하의 경로에서 컴포넌트를 스캔한다.
*/
ApplicationContext context = new ClassPathXmlApplicationContext("META-INF/spring/applicationContext.xml");
/*
    XML 기반의 설정 방식으로 ClassPathXmlApplicationContext의 생성자에 XML 파일을 인수로 전달한다.
    경로에 접두어(prefix)가 생략된 경우에는 classpath안에서 상대 경로로 설정 파일을 탐색한다.
*/
ApplicationContext context = new FileSystemXmlApplicationContext("./spring/applicationContext.xml");
/*
    XML 기반의 설정방식으로 FileSystemXmlApplicationContext의 생성자에 XML 파일을 인수로 전달한다.
    경로에 접두어가 생략된 경우에는 JVM의 작업 디렉토리 안에서 상대경로로 설정 파일을 탐색한다.
*/

 

책으로 정리하기 전에 처음 IoC/DI에 대해 접하고 공부할 때 많이 보던 구현클래스들이었다.

 

토비의 스프링 3.1에서는 위 예제들에서 사용했던 StaticApplicationContext, GenericApplicationContext, GenericXmlApplicationContext, WebApplicationContext 이렇게 네가지를 설명했다.

 

IDE를 통해 ApplicationContext.class를 들어가 확인해보면 상속받고 있거나 구현하고 있는 클래스가 22개가 나온다.

당연히 이 두 책에 나온 모든 ApplicationContext를 볼 수 있었고 그 외에도 엄청 많았다.

이게 각 클래스들이 다 ApplicationContext가 최상위에 있는건 맞는데 상속에 상속에 상속 이런식으로 얽혀있는게 많아서 다 보려면 아마 따로 하나하나 다 찾아보면서 봐야할 것 같기 때문에......................

일단은 토비의 스프링에 나와있는 네가지만 설명을 정리한다.

 

1. StaticApplicationContext

StaticApplicationContext는 코드를 통해 Bean 메타정보를 등록하기 위해 사용한다.

스프링의 기능에 대한 예제를 만들 때를 제외하면 실제로는 사용되지 않는다고 한다.

그럼에도 예제에서 사용한 이유는 '스프링 IoC 컨테이너는 파일 포맷이나 리소스 종류에 독립적이며 오브젝트로 표현되는 순수한 메타정보를 사용한다' 라는 것을 보여주기 위함이었다고 한다.

그리고 또 한가지 이유는 스프링 웹에서 예제 테스트로 검증해보고 싶을 때 유용하게 쓸 수 있기 때문이다.

웹 관련 기능 테스트시에는 StaticWebApplicationContext를 사용한다.

StaticApplicationContext는 실전에 사용하지는 않고 테스트 목적으로 Bean을 등록하고 컨테이너가 어떻게 동작하는지 확인하고 싶을 때 사용하면 된다고 한다.

 

2. GenericApplicationContext

GenericApplicationContext는 가장 일반적인 애플리케이션 컨텍스트의 구현 클래스다.

실전에서 사용될 수 있는 모든 기능을 갖추고 있고 컨테이너의 주요 기능을 DI를 통해 확장할 수 있도록 설계되어 있다.

GenericApplicationContext는 StaticApplicationContext와는 달리 XML 파일과 같은 외부의 리소스에 있는 Bean 설정 메타정보를 Reader를 통해 읽어들여 메타정보로 전환해 사용한다.

특정 포맷의 Bean 설정 메타정보를 읽어서 이를 ApplicationContext가 사용할 수 있는 BeanDefinition 정보로 변환하는 기능을 가진 오브젝트는 BeanDefinitnionReader 인터페이스를 구현해서 만들고, Bean 설정 정보 리더 라고 불린다.

XML로 작성된 Bean 설정 정보를 읽어서 컨테이너에게 전달하는 대표적인 Bean 설정정보 리더는 XmlBeanDefinitionReader다.

 

예제 하나를 통해 이 Reader를 GenericApplicationContext가 이용해도록 해 hello Bean과 printer Bean을 등록하고 사용하도록 하는 것을 확인한다.

 

//XML로 만든 Bean 설정 메타정보
<?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
            http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="hello" class="com.example.Hello">
        <property name="name" value="Spring"/>
        <property name="printer" value="printer"/>
    </bean>

    <bean id="printer" class="com.example.StringPrinter"/>
</beans>


//GenericApplicationContext 사용방법 에제
@Test
public void genericApplicationContext(){
    GenericApplicationContext context = new GenericApplicationContext();
    XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context);

    reader.loadBeanDefinitions(
            "applicationContext2.xml"
    );
    //XmlBeanDefinitionReader는 기본적으로 classpath로 정의된 리소스로부터 파일을 읽는다.

    ac.refresh();
    //모든 메타정보가 등록이 완료되었으니 어플리케이션 컨테이너를 초기화 하는 명령.

    Hello hello = context.getBean("hello", Hello.class);
    hello.print();

    assertEquals(context.getBean("printer").toString(), "Hello Spring");
    //검증 내용은 StaticApplicationContext 테스트와 동일하다.
}

 

XmlBeanDefinitionReader는 스프링의 resource loader를 이용해 XML 내용을 읽어온다.

리소스 대신 String을 넘기면 기본적으로 classpath 리소스로 인식한다.

그래서 위 예제에서 applicationContext2.xml의 위치는 src/main/resources/applicationContext2.xml이 된다.

그렇다고 classpath: 를 붙이면 안되는건 아니고 생략했을 때 default로 classpath로 인식한다는 것이다.

그리고 classpath 외에도 file: , http: 같은 접두어를 이용해 구체적인 리소스 타입을 지정할 수 있다.

위에서 얘기했듯이 꼭 xml 파일이 아니더라도 IoC 컨테이너가 사용할 수 있는 BeanDefiniton 오브젝트로 변환할 수만 있따면 설정 메타정보는 어떤 포맷으로 만들어져도 상관없다.

 

GenericApplicationContext는 BeanDefinitionReader를 여러개 사용해서 여러 리소스로부터 설정 메타정보를 읽어들이게도 할 수 있다.

모든 설정 메타정보를 가져온 후에 refresh() 메소드를 한번 호출해서 ApplicationContext가 필요한 초기화 작업을 수행하게 해주면 된다.

스프링을 사용하면서 GenericApplicationContext를 직접 만들어서 사용할 일은 거의 없다.

스프링 컨테이너 자체를 확장해 새로운 프레임워크를 만들거나 스프링을 사용하는 독립형 애플리케이션을 만들지 않는 한 직접 만들어서 이용할 필요는 없다.

하지만 코드로 직접 만들어서 초기화하거나 하는 경우가 없을 분이지 알게모르게 자주 사용된다.

JUnit 테스트는 테스트 내에서 사용할 수 있도록 ApplicationContext를 자동으로 만들어주는데 이때 생성되는 context가 GenericApplicationContext다.

 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class UserServiceTest{
    @Autowired
    ApplicationContext applicationContext;
    
    ....
}

 

테스트가 실행되면서 GenericApplicationContext가 생성되고 @ContextConfiguration에 지정한 XML 파일로 초기화 돼서 테스트내에서 사용할 수 있도록 준비된다.

 

 

3. GenericXmlApplicationContext

코드에서 GenericApplicationContext를 사용하는 경우에 xmlBeanDefinitionReader를 만들어 설정 메타정보를 읽어야 한다고 했다.

GenericXmlApplicationContext는 이 두 클래스를 결합한 것으로 XmlBeanDefinitionReader를 내장하고 있기 때문에 xml파일을 읽어들이고 refresh()를 통해 초기화 하는 것까지 한 줄로 끝낼 수 있다.

XML파일로 설정을 만들고 ApplicationContext에서 XML을 읽어 사용하는 코드를 시험삼아 만들어볼 필요가 있따면 사용하기에 적당하다.

XML 파일정보는 리소스로더가 읽을 수 있는 형식으로 GenericXmlApplicationContext 생성자에 넣어주면 되며 하나 이상의 파일을 지정할 수도 있다.

 

GenericApplicationContext context = new GenericXmlApplicationContext(
		"file:src/main/webapp/WEB-INF/applicationContext2.xml");
        
Hello hello = context.getBean("hello", Hello.class);

 

4. WebApplicationContext

스프링 애플리케이션에서 가장 많이 사용되는 ApplicationContext다.

ApplicationContext를 확장한 인터페이스이므로 정확하게는 WebApplicationContext를 구현한 클래스를 사용하는 셈이다.

이름 그대로 웹 환경에서 사용할 때 필요한 기능이 추가된 ApplicationContext인데 스프링 애플리케이션은 대부분 servlet 기반의 독립 웹 애플리케이션(war)으로 만들어지기 떄문이다.

WebApplicationContext중에서도 가장 많이 사용되는건 XmlWebApplicationContext다.

XML 이외의 설정정보 리소스도 사용할 수 있는데

Annotation을 이요한 설정리소스만 사용한다면 AnnotationConfigWebApplicationContext를 사용하면 되고 

default는 XmlWebApplicationContext다.

 

WebApplicationContext를 보기 전에 IoC 컨테이너를 적용했을 때 애플리케이션을 기동시키는 방법에 대해 살펴볼 필요가 있다.

IoC Container는 Bean 설정 메타정보를 이용해 Bean 오브젝트를 만들고 DI 작업을 수행한다.

Bean 오브젝트의 메소드를 호출함으로써 애플리케이션을 동작시켜야 한다.

계속 봐왔던 예제처럼 getBean() 메소드를 사용해 Bean 오브젝트를 가져와야 하고 한번 가져온 뒤에는 다시 getBean()으로 가져올 필요는 없다.

그럼 Bean 오브젝트끼리 DI로 서로 연결되어있으므로 의존관계를 타고 필요한 오브젝트가 호출되면서 애플리케이션이 동작하게 된다.

IoC 컨테이너의 역할은 이렇게 초기에 Bean 오브젝트를 생성하고 DI 한 후에 최초로 애플리케이션을 기동할 Bean하나를 제공해주는것 까지다.

 

그런데 웹 애플리케이션은 동작하는 방식이 근본적으로 다르다.

독립 자바 프로그램은 VM에게 main 메소드를 가진 클래스를 시작시켜 달라고 요청할 수 있지만

웹에서는 main 메소드를 호출할 방법이 없고 사용자도 여럿이며 동시에 웹 애플리케이션을 사용한다.

그래서 웹 환경에서는 main 메소드 대신 ServletContainer가 브라우저로부터 오는 HTTP 요청을 받아 해당 요청에 매핑되어 있는 servlet을 실행해주는 방식으로 동작한다.

servlet이 main메소드와 같은 역할을 하는 셈이다.

 

웹 애플리케이션에서 스프링 애플리케이션을 기동하기 위해서는 main 메소드 역할을 하는 servlet을 만들어두고 미리 ApplicationContext를 생성해둔 다음, 요청이 servlet으로 들어올때마다 getBean()으로 필요한 Bean을 가져와 정해진 메소드를 실행해주면 된다.

아래 그림이 웹 환경에서 스프링 Bean으로 이뤄진 애플리케이션이 동작하는 구조다.

 

출처: 토비의 스프링 3.1

 

 

서버 환경이기 때문에 필요한 ServletContainer와 client로부터의 원격 호출을 감안하고 본다면 main메소드 안에서 ApplicationContext를 만들고 설정 메타정보를 읽어들여서 초기화 한 후에 애플리케이션의 기동 책임을 맡는 POJO Bean 오브젝트를 요청해 메소드를 실행하는것과 다를바가 없다.

main 메소드나 테스트 메소드에서 했던 작업을 WebApplication과 그에 소속된 Servlet이 대신 해줄 뿐이다.

 

ServletContainer는 브라우저와 같은 client로부터 들어오는 요청을 받아 servlet을 동작시켜주는 역할을 한다고 했다.

servlet은 웹 애플리케이션이 시작될 때 미리 만들어둔 WebApplicationContext에게 Bean 오브젝트로 구성된 애플리케이션의 기동역할을 해줄 Bean을 요청해 받아둔다.

그리고 미리 지정된 메소드를 호출함으로써 스프링 컨테이너가 DI 방식으로 구성해둔 애플리케이션의 기능이 시작되는 것이다.

 

스프링은 이런 웹 환경에서 ApplicationContext를 생성하고 설정 메타정보로 초기화해주고 클라이언트로 들어오는 요청마다 적절한 Bean을 찾아 이를 실행해주는 기능을 갖고 있는 DispatcherServlet 이라는 이름의 serlvet을 제공한다.

이 servlet을 web.xml에 등록하는 것만으로 웹 환경에서 스프링 컨테이너가 만들어지고 애플리케이션을 실행하는데 필요한 대부분의 준비는 끝난다.

 

WebApplicationContext의 특징은 자신이 만들어지고 동작하는 환경인 웹 모듈에 대한 벙보에 접근할 수 있다는 것이다.

이를 이용해 웹 환경으로부터 필요한 정보를 가져오거나 웹 환경에 스프링 컨테이너 자신을 노출할 수 있는데 이렇게 노출되면 같은 웹 모듈에 들어있는 스프링 Bean이 아닌 일반 오브젝트와 연동될 수 있다.

'Spring' 카테고리의 다른 글

IoC와 DI(4. Bean 설정 방식, DI 방식)  (0) 2022.08.24
IoC와 DI(3. WebApplication의 IoC Container 구성)  (0) 2022.08.22
IoC와 DI(1. IoC Container란)  (0) 2022.08.18
JPA 양방향 연관관계(mappedBy)  (0) 2022.03.18
JPA Entity Listener 2  (0) 2022.03.17

+ Recent posts