본문 바로가기
스프링(부트)/스프링 내용 정리

[스프링] 토비의 스프링 1.1 IoC 컨테이너 : 빈 팩토리와 애플리케이션 컨텍스트

by doflamingo 2020. 3. 1.

 

IoC 컨테이너(Inverse of Control):

 스프링 애플리케이션은 오브젝트의 생성과 관계설정, 사용, 제거 등의 작업을 컨테이너가 담당한다. 이 컨테이너를 IoC 컨테이너라고 부른다. 

 

이런 IoC컨테이너를 빈 팩토리 또는 애플리케이션 컨텍스트 라고 부르기도 한다.

 

빈 팩토리: 오브젝트 생성과 오브젝트 사이의 런타임 관계를 설정하는 DI(Dependency Injection) 관점의 컨테이너

 

애플리케이션 컨텍스트: 빈 팩토리 + α(추가 기능)

 

public interface ApplicationContext extends ListableBeanFactory, HierarchicalBeanFactory,
MessageSource, ApplicationEventPublisher, ResourcePatternResolver {

};

위 코드와 같이 ApplicationContext는 BeanFactory를 상속하고 있다. 

 

결국, 스프링 컨테이너는 ApplicationContext 인터페이스를 구현한 클래스의 오브젝트이다. 

 

 

IoC 컨테이너를 이용해 애플리케이션 만들기

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

 

StaticApplicationContext ac = new StaticApplicationContext();

이렇게 IoC 컨테이너 하나가 준비됐지만 아직은 아무런 하는 일이 없는 빈 컨테이너일 뿐이다. 

이런 컨테이너가 본격적인 동작을 하기 위해서는 두 가지가 필요하다. 

 

POJO 클래스설정 메타 정보 이다. 

 

POJO (Plain Old Java Object) 클래스

 

POJO는 특정 기술과 스펙에서 독립적일뿐더러 의존관계에 있는 다른 POJO와 느슨한 결합을 갖도록 만들어야한다. 

아래의 코드로 살펴보자. 

 

Hello 클래스는 Printer라는 인터페이스에만 의존한다. 실제로 런타임시 어떤 구체적인 클래스의 오브젝트를 사용하게 될 지는 상관이 없다. 

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

 

public class Hello {

    String name;
    Printer printer; 
    
    public String sayHello() {
    	return "Hello "+ name; 		// 프로퍼티로 DI 받은 이름을 이용해 간단한 인사문구 만들기 
    }
	
    public void print() {
    	this.printer.print(sayHello());		// DI에 의해 의존 오브젝트로 제공받은 Printer타입의 오브젝트에게 
        									// 출력 작업을 위임
    }
    
    public void setName(String name) {
    	this.name = name;
    }
    
    public void setPrinter(Printer printer) {
    	this.printer = printer;
    }
}

 

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


public class StringPrinter implements Printer {
    private StringBuffer buffer = new StringBuffer();
    
    public void print(String message) {
    	this.buffer.append(message);	// 내장 버퍼에 메시지 추가
    }
    
    public String toString() {
    	return this.buffer.toString();	// 내장 버퍼에 추가해둔 메시지를 스트링으로 가져온다. 
    }
}

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

 

 

설정 메타정보

 

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

 

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

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

 

스프링의 설정 메타정보는 BeanDefinition 인터페이스로 표현되는 순수한 추상 정보다.  IoC 컨테이너는 이 BeanDefinition으로 만들어진 메타정보를 담은 오브젝트를 사용해 IoC와 DI 작업을 수행한다. 

 

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

 

  • 빈 아이디, 이름, 별칭: 빈 오브젝트를 구분할 수 있는 식별자
  • 클래스 또는 클래스 이름: 빈으로 만들 POJO 클래스 또는 서비스 클래스 정보
  • 스코프: 싱글톤, 프로토타입과 같은 빈의 생성 방식과 존재 범위 
  • 프로퍼티 값 또는 참조: DI에 사용할 프로퍼티 이름과 값 또는 참조하는 빈의 이름
  • 생성자 파라미터 값 또는 참조: DI에 사용할 생성자 파라미터 이름과 값 또는 참조할 빈의 이름
  • 지연된 로딩 여부, 우선 빈 여부, 자동와이어링 여부, 부모 빈 정보, 빈팩토리 이름 등

 

컨테이너를 통해 애플리케이션이 만들어지는 방식

 

 

결국 스프링 애플리케이션이란 POJO 클래스와 설정 메타정보를 이용해 IoC 컨테이너가 만들어주는 오브젝트의 조합이다. 

 

아래 코드를 통해 두 개의 빈을 등록하고 DI한 후에 두 개의 빈이 서로 연동해서 동작하는지 확인 할 수 있다. 

 

@Test
public void registerBeanWithDependency(){

    StaticApplicationContext ac = new StaticApplicationContext();

    /* StringPrinter 클래스 타입이며 printer라는 이름을 가진 빈을 등록한다. */
    ac.registerBeanDefinition("printer", new RootBeanDefinition(StringPrinter.class));
    
    BeanDefinition helloDef = new RootBeanDefinition(Hello.class);
    helloDef.getPropertyValue().addPropertyValue("name","Spring");  // 단순 값을 갖는 프로퍼티
    helloDef.getPropertyValue().addPropertyValue("printer", 	// 아이디가 printer인 빈에 대한
        new RuntimeBeanReference("printer"));                   // 레퍼런스를 프로퍼티로 등록
    
    ac.registerBeanDefinition("hello",helloDef);
    
    Hello hello = ac.getBean("hello", Hello.class);
    hello.print();
    
    
    /* Hello 클래스의 print() 메소드는 DI 된 Printer 타입의 오브젝트에게 요청해서 인사말을 출력한다.
       이 결과를 스트링으로 저장해두는 printer 빈을 통해 확인한다. */
    assertThat(ac.getBean("printer").toString(), is("Hello Spring"));

}

 

IoC 컨테이너 종류와 사용 방법

 

 

 1. StaticApplicationContext

 

코드를 통해 빈 메타정보를 등록하기 위해 사용한다. (스프링 기능에 대한 학습테스트를 만들때가 아니면 실제로 사용되지 않는다.)

 

 2. GenericApplicationContext

 

가장 일반적인 애플리케이션 컨텍스트의 구현 클래스이다. 실전에서 사용될 수 있는 모든 기능을 갖추고 있는 애플리케이션 컨텍스트다. 

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

 

GenericApplicationContext ac = new GenericApplicationContext();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(ac);
reader.loadBeanDefinitions("~~~~.xml");
ac.refresh();

 

3. GenericXmlApplicationContext

 

GenericApplicationContext와 XmlBeanDefinitionRead가 결합된 형태로 생성자에 xml파일을 넣어주면 된다. 

 

GenericApplicationContext ac = new GenericXmlApplicationContext("~~~~~~.xml");

4. WebApplicationContext

 

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

 

스프링 IoC 컨테이너는 빈 설정 메타정보를 이용해 빈 오브젝트를 만들고 DI 작업을 수행한다. 하지만 이것만으로 애플리케이션이 동작하지 않는다. 

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

이런 기동 역할을 맡은 빈을 사용하려면 IoC 컨테이너에서 요청해서 빈 오브젝트를 가져와야한다. 그래서 getBean()이라는 메소드로 가져올 수 있다. 

이 후에는 빈 오브젝트끼리 DI로 서로 연결되어 있어서 의존관계를 타고 필요한 오브젝트가 호출되면서 애플리케이션이 동작한다. 

 

그러나 웹 애플리케이션은 동작하는 방식이 근본적으로 다르다. 독립 자바 프로그램은 자바 VM에게 main() 메소드를 가진 클래스를 시작시켜 달라고 요청할 수 있다. 하지만 웹에서는 main() 메소드를 호출할 방법이 없다. 대신 서블릿 컨테이너가 브라우저로 오는 HTTP요청을 받아서 해당 요청에 매핑되어 있는 서블릿을 실행해주는 방식으로 동작한다. 서블릿이 일종의 main() 메소드와 같은 역할을 한다. 

 

웹 애플리케이션에서 스프링 애플리케이션을 기동시키는 방법은 일단 main() 메소드 역할을 하는 서블릿을 만들어두고, 미리 애플리케이션 컨텍스트 생성해둔 다음 요청이 서블릿으로 들어올 때마다 getBean()으로 필요한 빈을 가져와 정해진 메소드를 실행해주면 된다. 

 

 

 

웹 환경에서 스프링 애플리케이션이 기동하는 방식

main() 에서 했던 것을 단순히 서블릿이 대신해준다고 생각하면 쉽다. 

 

스프링에서는 DispatchServlet이 애플리케이션 컨텍스트를 생성하고 설정 메타정보로 초기화해주고, 클라이언트로 들어오는 요청마다 적절한 빈을 찾아서 실행해준다. 

 

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

 

 

 

 

 

출처: 토비의 스프링

댓글