Java Web Development Workbook Chapter. 08
IoC 란 개발자가 작성한 코드에 따라 제어가 이루어지는 것이 아니라, 외부에 의해 코드의 흐름이 바뀌는 것을 의미
class ProjectListController {
public void execute() {
ProjectDao dao = new ProjectDao();
List<Project> projects = dao.list();
}
}
class ProjectListController {
ProjectDao dao;
public void setProjectDao(ProjectDao dao) {
this.dao = dao;
}
public void execute() {
List<Project> projects = dao.list();
}
}
초창기에는 애플리케이션의 크기가 작아 객체가 필요할 때마다 직접 생성해서 사용해도 문제가 되지 않았다. 하지만 규모가 커지면서 성능이나 유지 보수에 문제가 생기게 된다. 이러한 문제를 해결하기 위해 등장한 것이 의존성 주입이다.
Spring은 IoC 컨테이너가 갖추어야 할 기능들을 ApplicationContext interface에 정의해 두었다. Spring에서 제공하는 IoC 컨테이너들은 모두 이 ApplicationContext interface로부터 상속받는다.
<bean id="score" class="exam.test01.Score">
</bean>
<bean> 태그를 통해 자바 빈을 선언할 때 id 속성이나 name 속성에 빈 이름을 지정할 수 있다.
항목 | id 속성 | name 속성 |
---|---|---|
용도 | 빈 식별자를 지정한다. 중복되어서는 안된다. | 인스턴스의 별명을 추가할 때 사용, id와 마찬가지로 중복되어서는 안된다. |
여러 개의 이름 지정 | 불가능 | 콤마, 세미콜론 또는 공백을 사용하여 여러 개의 이름 지정 가능. (첫번째 이름은 컨테이너에서 빈을 보관할 때 사용, 나머지 이름은 빈의 별명) |
빈 이름 작성 규칙 | 제약 없음 | 제약 없음 |
[ch08] 8.3 Use ClassPathXmlApplicationContext
<!-- exam.test01.Score#0 -->
<!-- exam.test01.Score 는 첫 번째 빈인 exam.test01.Score#0 에 대한 별명이 된다. -->
<bean class="exam.test01.Score">
생성되는 인스턴스에 대해 호출될 생성자를 지정할 수 있다.
<bean id="score" class="exam.test01.Score">
<constructor-arg>
<!-- value 를 통해 매개변수 값을 지정한다-->
<!-- type 속성은 매개변수의 타입, 타입은 생략가능하며, 생략하면 자동 형변환을 수행 -->
<value type="java.lang.String">example</value>
</constructor-arg>
<constructor-arg>
<value type="float">91</value>
</constructor-arg>
</bean>
위와 같이 정의했을 때, Score(String name, float value) 생성자가 호출될 것이다.
[ch08] 8.3 contructor-args usage
다음과 같이 ‘c’ 네임스페이스를 사용하면, c: 로 시작하는 속성을 통해 생성자의 파라미터를 지정할 수 있다.
<beans xmlns="http://www.springframework.org/schema/beans" ...
xmlns:p="http://www.springframework.org/schema/c" ... >
<bean id="score" class"exam.test01.Score" c:name="example" c:value="91" />
</beans>
생성자를 통해 의존 객체를 주입할 때, 새로 빈을 생성하여 넘겨주고 싶다면 자식 태그로 <bean> 을 다음과 같이 선언하면 된다.
<bean id="score" class="exam.test01.Score">
<constructor-arg>
<bean class="exam.test01.final">
...
</bean>
</constructor-arg>
</bean>
인스턴스를 선언할 때 프로퍼티 값을 설정할 수 있다.
<bean id="score" class="exam.test01.Score">
<property name="name">
<value>example</value>
</property>
<property name="value">
<value>10</value>
</property>
</bean>
위와 같이 정의하면 Score 클래스의 setName(String name) / setValue(int value) 가 호출된다.
다음과 같이 ‘p’ 네임스페이스를 사용하면, p: 로 시작하는 속성을 통해 프로퍼티를 설정할 수 있다.
<beans xmlns="http://www.springframework.org/schema/beans" ...
xmlns:p="http://www.springframework.org/schema/p" ... >
<bean id="score" class"exam.test01.Score" p:name="example" p:value="10" />
</beans>
다음과 같이 Car 클래스가 의존하는 객체인 Engine 타입의 빈을 주입할려면 다음과 같이 설정한다.
class Car {
private String model;
private Engine engine;
...
}
class Engine {
...
}
<bean id="car" class="exam.test.Car">
<property name="model">
<value>pride</value>
</property>
<property name="engine">
<ref bean="engine1" />
</property>
</bean>
<bean id="engine1" class="exam.test.Engine">
</bean>
[ch08] 8.3 Use dependency Injection.
[ch08] 8.3 Use dependency Injection with new bean.
<bean id="engine1" class="exam.test.Engine">
</bean>
<bean id="car" class="exam.test.Car">
<property name="model">
<value>pride</value>
</property>
<property name="engines">
<list>
<bean class="exam.test.Engine" />
</bean>
<ref bean ="engine1" />
</list>
</property>
<property name="values">
<list>
<value>10</value>
<value>20</value>
</list>
</property>
</bean>
[ch08] 8.3 Use dependency Injection with list.
<!-- 다음과 같이 java.util.Properties 타입의 값을 설정 -->
<bean id="spareTire" class="exam.test.Tire">
<property name="spec">
<props>
<prop key="width">205</prop>
<prop key="ratio">65</prop>
</props>
</property>
</bean>
<!-- java.util.Map 타입의 값을 설정 -->
<bean id="car" class="exam.test.Car">
<property name="options">
<map>
<entry key="airbag" value="dual" />
<entry>
<key> <value> sunroof </value> </key>
<value> yes </value>
</entry>
<entry key="sparetire">
<ref bean="spareTire" />
</entry>
</map>
</property>
</bean>
[ch08] 8.3 Use dependency Injection with map, props.
팩토리 메소드 패턴을 이용하여 빈 생성을 하고자 할 때 다음과 같이 설정한다.
public class TireFactory {
public static Tire createTire(String make) {
...
}
}
<bean id="hankookTire" class="exam.test.TireFactory" factory-method="createTire">
<constructor-arg value="Hankook" />
</bean>
<bean id="kumhoTire" class="exam.test.TireFactory" factory-method="createTire">
<constructor-arg value="Kumho" />
</bean>
[ch08] 8.3 Use factory method.
다음과 같이 팩토리 클래스의 객체를 만들고, 이 팩토리 객체로부터 인스턴스를 생성할 수도 있다.
public class TireFactory {
public Tire createTire(String make) {
...
}
}
<bean id="tireFactory" class="exam.test.TireFactory" />
<bean id="hankookTire" factory-bean="tireFactory" factory-method="createTire">
<constructor-arg value="Hankook" />
</bean>
<bean id="kumhoTire" factory-bean="tireFactory" factory-method="createTire">
<constructor-arg value="Kumho" />
</bean>
Spring 에서는 팩토리 빈이라면 갖추어야 할 규칙을 org.springframework.beans.factory.FactoryBean 인터페이스에 정의하였다. 팩토리 클래스를 만들 때 이 인터페이스에 따라 메소드를 구현하면 된다. What’s a FactoryBean?
public interface FactoryBean<T> {
T getObject() throws Exception;
Class<T> getObjectType();
boolean isSingleton();
}
그런데 FactoryBean 인터페이스를 직접 구현하기보다는 Spring에서 제공하는 추상 클래스를 상속하는 것이 일반적이다. org.springframework.beans.factory.config.AbstractFactoryBean 은 FactoryBean 인터페이스를 미리 구현하였다.
public abstract class AbstractFactoryBean<T>
implements FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
...
@Override
public abstract Class<?> getObjectType();
protected abstract T createInstance() throws Exception;
...
}
이 클래스에는 createInstance() 라는 추상 메소드가 있다. 빈을 생성할 때 팩토리 메소드로써 getObject() 가 호출되는데, 이 메소드는 내부적으로 createInstance() 메소드를 호출한다.
public class TireFactory extends AbstractFactoryBean<Tire> {
String maker;
public void setMaker(String maker) {
this.maker = maker;
}
@Override
public Class<?> getObjectType() {
return exam.test13.Tire.class;
}
protected Tire createInstance() {
if (maker.equals("Hankook")) {
return createHankookTire();
} else {
return createKumhoTire();
}
}
...
<bean id="hankookTire" class="exam.test13.TireFactory">
<property name="maker" value="Hankook" />
</bean>
[ch08] 8.3 Use AbstractFactoryBean.
Spring IoC 컨테이너는 빈 생성시, 기본으로 하나만 생성한다. 따라서 getBean() 메소드를 호출하면 계속 동일한 객체를 반환한다. 하지만 설정을 통해 이런 빈의 생성 방식을 조정할 수 있다.
범위 | 설명 |
---|---|
singleton | 오직 하나의 빈만 생성 (기본 설정) |
prototype | getBean() 을 호출할 때마다 생성 |
request | HTTP 요청이 발생할 때마다 생성, 웹 애플리케이션에서만 이 범위를 설정 가능 |
session | HTTP 세션이 생성될 때마다 생성, 웹 애플리케이션에서만 이 범위를 설정 가능 |
globalsession | 전역 세션이 준비될 때 빈을 생성. 웹 애플리케이션에서만 이 범위를 설정 가능, 포틀릿 컨텍스트에서 사용 |
다음과 같이 scope 속성을 통해 빈 생성 방식을 prototype 형식으로 변경할 수 있다. kiaEngine 이름의 빈을 getBean 메소드가 호출될 때마다 새로 생성할 것이다.
<bean id="hyundaiEngine" class="exam.test14.Engine">
<property name="maker" value="Hyundai" />
<property name="cc" value="1997" />
</bean>
<bean id="kiaEngine" class="exam.test14.Engine" scope="prototype">
<property name="maker" value="Kia" />
<property name="cc" value="3000" />
</bean>
[ch08] 8.8 Setting bean scope.
빈 설정 파일은 XML 이므로, 결국 프로퍼티의 값은 문자열로 표현하게 된다. 문자열은 숫자로 변환하기 쉽기 때문에, 숫자 타입의 프로퍼티일 경우 추가 작업없이 자동 변환해주는데에 비해 다른 타입에 대해서는 변환해주지 않는다.
여기서는 java.util.Date 타입의 프로퍼티 값을 설정한다. SimpleDateFormat 클래스와 팩토리 메소드 방식을 통해 날짜 값을 설정한다.
<bean id="dateFormat" class="java.text.SimpleDateFormat" >
<constructor-arg value="yyyy-MM-dd" />
</bean>
<bean id="hankookTire" class="exam.test15.Tire" >
<property name="maker" value="Hankook" />
<property name="createdDate">
<bean factory-bean="dateFormat" factory-method="parse">
<constructor-arg value="2014-5-5" />
</bean>
</property>
</bean>
<bean id="kumhoTire" class="exam.test15.Tire" >
<property name="maker" value="Kumho" />
<property name="createdDate">
<bean factory-bean="dateFormat" factory-method="parse">
<constructor-arg value="2014-1-14" />
</bean>
</property>
</bean>
</beans>
[ch08] 8.9 Injection bean of java.util.Date.
java.util.Date 값을 설정한 위의 방식은 날짜 프로퍼티 값을 설정할 때마다 팩토리 메소드 빈을 선언해야 한다는 것이다. 이런 불편한 점을 해소하기 위해, Spring IoC 컨테이너는 프로퍼티 에디터 를 도입하였다.
Spring 에서는 java.util.Date 처럼 자주 사용하는 타입에 대해 몇 가지 프로퍼티 에디터를 제공한다.
다음과 같이 PropertyEditorRegistrar 를 구현한 CustomPropertyEditorRegistrar 클래스를 정의해야 한다. 이 클래스에는 IoC 컨테이너가 프로퍼티 에디터를 설치할 때 registerCustomEditors 메소드를 호출하는데, 에디터 등록기는 이 메서드를 구현해야 한다.
public class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
private CustomDateEditor customDateEditor;
public void setCustomDateEditor(CustomDateEditor customDateEditor) {
this.customDateEditor = customDateEditor;
}
@Override
public void registerCustomEditors(PropertyEditorRegistry registry) {
registry.registerCustomEditor(java.util.Date.class, customDateEditor);
}
}
위의 registerCustomEditors는 java.util.Date 타입에 대해 프로퍼티 에디터를 등록하는데, 이 java.util.Date 타입의 프로퍼티 값을 처리할 때 customDateEditor 가 사용된다.
다음과 같이 앞서 정의한 CustomPropertyEditorRegistrar 타입의 빈에 CustomDateEditor 빈을 주입하고, 이 빈을 CustomEditorConfigurer 에 리스트로 프로퍼티를 설정한다.
<bean id="dateFormat" class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-MM-dd"/>
</bean>
<bean id="dateEditor" class="org.springframework.beans.propertyeditors.CustomDateEditor">
<constructor-arg ref="dateFormat"/>
<constructor-arg value="true"/>
</bean>
<bean id="customPropertyEditorRegistrar" class="exam.test16.CustomPropertyEditorRegistrar">
<property name="customDateEditor" ref="dateEditor"/>
</bean>
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<list>
<ref bean="customPropertyEditorRegistrar"/>
</list>
</property>
</bean>
<bean id="hankookTirme" class="exam.test15.Tire">
<property name="aker" value="Hankook"/>
<property name="createdDate" value="2014-5-5"/>
</bean>
<bean id="kumhoTire" class="exam.test15.Tire">
<property name="maker" value="Kumho"/>
<property name="createdDate" value="2014-1-14"/>
</bean>
[ch08] 8.9 Use custom property editor.
@Autowired annotation을 통해 간단히 의존 객체를 주입 가능.
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcess" />
이 AutowiredAnnotationBeanPostProcess 클래스는 빈의 후 처리기 (post processor)로서 빈을 생성 후 @Autowired 로 선언된 setter를 찾아 호출하는 역할을 수행한다. 파라미터 타입과 일치하는 빈을 찾아 주입해준다. 타입 일치하는 빈이 없거나 2개 이상인 경우, 예외를 발생시킨다.
public class Car {
...
@Autowired
public void setEngine(Engine engine) {
this.engine = engine;
}
...
}
...
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor" />
...
<bean id="hyundaiEngine" class="exam.test17.Engine">
<constructor-arg value="Hyundai"/>
</bean>
<bean id="car1" class="exam.test17.Car">
<property name="model" value="Sonata"/>
</bean>
[ch08] 8.10 Apply @Autowired annotation.
...
<context:annotation-config/>
...
다음과 같이 required 속성을 false로 지정하면 setter에 주입할 빈을 못찾아도 예외가 발생하지 않는다.
public class Car {
...
@Autowired(required = false)
public void setEngine(Engine engine) {
this.engine = engine;
}
...
}
@Autowired annotation 사용시, 주입할 수 있는 빈 객체가 여러 개일 경우 예외를 발생시킨다. 이 상황에서 적절한 빈을 주입시킬 수 있게 해주기 위해, @Qualifier annotation을 사용한다.
annotation | post processor |
---|---|
@Autowired, @Value, @Inject (JSR-330) | AutowiredAnnotationBeanPostProcessor |
JSR-250 annotation (javax.annotation.*) | CommonAnnotationBeanPostProcessor |
@PersistenceUnit, @PsersistenceContext | PersistenceAnnotationBeanPostProcessor |
@Required | RequiredAnnotationBeanPostProcessor |
@Qualifier annotation을 다음과 같이 사용한다. Annotation 에 들어가는 문자열은 주입할 빈의 id 이다. 빈 컨테이너는 이 id 값과 일치하는 이름을 가진 빈을 찾아 주입한다.
public class Car {
...
@Autowired(required = false)
@Qualifier("kiaEngine")
public void setEngine(Engine engine) {
this.engine = engine;
}
...
}
간단히 말해, 이 annotation은 @Autowired + @Qualifier 이다. 이름으로 의존 객체를 지정할 경우, 이 annotation을 사용할 것을 권장한다. 단, required 속성이 없으므로 반드시 주입가능한 빈이 있어야 한다.
public class Car {
...
@Resource(name="kiaEngine")
public void setEngine(Engine engine) {
this.engine = engine;
}
...
}
스프링에서는 @Component annotation을 사용하여, 이 annotation이 붙은 클래스로부터 객체를 자동 생성하여 빈으로 등록한다. 이 annotation 외에도 클래스의 역할에 따라 붙일 수 있는 annotation을 추가로 제공한다.
annotation | description |
---|---|
@Component | 빈 생성 대상이 되는 모든 클래스에 대해 선언 가능 |
@Repository | DAO와 같은 persistence 역할을 수행하는 클래스에 선언 |
@Service | 서비스 역할, 비즈니스 로직이 들어가는 루틴이 있는 클래스에 선언 |
@Controller | MVC 구조에서 Controller 역할을 수행하는 클래스에 선언 |
위의 annotation을 사용하기 위해, 다음과 같은 태그를 빈 설정 파일에 추가해야 한다.
...
<context:component-scan base-package="kr.co.myproject" />
...
@Component("car")
public class {
...
}
위 annotation에서 들어가는 문자열은 빈의 이름이다.