//test class
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class testClass {
@Test
public void divTest() {
ApplicationContext context = new GenericXmlApplicationContext(
"file:src/main/webapp/WEB-INF/applicationContext.xml");
String[] beanArr = context.getBeanDefinitionNames();
System.out.println("bean Arr");
for(String beanName : beanArr)
System.out.println(beanName);
UserService userService1 = context.getBean(UserService.class);
UserService userService2 = context.getBean(UserService.class);
assertEquals(userService1, userService2);
AdminService adminService1 = context.getBean(AdminService.class);
AdminService adminService2 = context.getBean(AdminService.class);
assertEquals(adminService1, adminService2);
ProductService productService1 = context.getBean(ProductService.class);
ProductService productService2 = context.getBean(ProductService.class);
assertEquals(productService1, productService2);
}
}
/* 테스트 결과
beanArr
userService
productService
adminService
productService1, productService2 검증에 대한 오류
*/
XML 기반 설정에서 주의할점은 import시에 경로다.
지금 예제에서는 applicationContext.xml과 같은 webapp/WEB-INF 위치에 *-config.xml 파일을 생성했기 때문에 저렇게 딱 파일명만 적어도 되지만 resource 폴더에 생성하는 경우 classpath: 를 사용한다거나 해서 경로를 잘 잡아줘야 한다.
분할된 bean 정의 파일은 일반적인 bean 정의 파일과 동일한 형식으로 작성하면 된다.
테스트 결과를 보면 자바 기반 설정 방식과는 조금 다른것을 볼 수 있는데 자바 기반 설정방식에서는 import 해준 클래스까지 모두 출력된 반면에 xml 기반 설정 방식에서는 생성하고자 했던 bean의 이름만 딱 나오는 것을 볼 수 있다.
사용하는데에 대한 차이는 없었지만 컨테이너에 등록되는 bean의 차이가 좀 있다는 점이 차이점이라면 차이점일 수 있겠다.
프로파일(profile)별 설정
스프링에서는 설정 파일을 특정 환경이나 목적에 맞게 선택적으로 사용할 수 있도록 그룹화 할 수 있으며, 이 기능을 프로파일(profile)이라고 한다.
예를들어, 애플리케이션이 실행될 환경마다 서로 다른 프로파일을 만든다면 개발 환경을 위한 development profile, 검증을 위한 staging profile, 실제 운영 환경을 위한 production profile등을 만들 수 있을 것이다.
프로파일 정의는 모든 자바, XML, 어노테이션 기반 설정 방식 모두 가능하다.
1. 자바기반 설정 방식에서의 profile 정의
자바기반 설정 방식에서 프로파일을 정의할 때는 @Profile 어노테이션(org.springframework.context.annotation.Profile)을 사용한다.
@Configuration
@Profile("development")
public class DevelopmentConfig {
....
}
@Configuration
@Profile("staging")
public class satgingConfig {
....
}
@Configuration
@Profile("production")
public class ProductionConfig {
....
}
@Profile 어노테이션에서는 @Profile({"development", "staging"})과 같이 여러개의 프로파일을 지정하거나
@Profile("!production")과 같이 production 프로파일만을 제외한다는 부정형으로 표현할 수도 있다.
프로파일을 개발 환경별로 구분하는 방식은 개발이 어느정도 진행된 다음에 다른 개발 환경으로 쉽게 옮겨갈 수 있게 해주는 유용한 기법이다.
2. XML 기반 설정에서의 profile 정의
XML 기반 설정 방식에서는 <beans> 요소의 profile 속성을 활용한다.
<!-- 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
http://www.springframework.org/schema/beans/spring-beans.xsd"
profile="development">
<!-- 이 안에서 정의한 내용은 지정한 프로파일(development)내에서만 유효하다. -->
</beans>
<!-- 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
http://www.springframework.org/schema/beans/spring-beans.xsd">
<beans profile="development">
<!-- 이 안에서 정의한 내용은 development 프로파일 내에서만 유효하다 -->
<bean id="datasource" class="....">
<!-- 생략 -->
</bean>
</beans>
<beans profile="staging">
<!-- 이 안에서 정의한 내용은 staging 프로파일 내에서만 유효하다 -->
<bean id="datasource" class="....">
<!-- 생략 -->
</bean>
</beans>
<beans profile="production">
<!-- 이 안에서 정의한 내용은 production 프로파일 내에서만 유효하다 -->
<bean id="datasource" class="....">
<!-- 생략 -->
</bean>
</beans>
</beans>
XML 기반 설정 방식에서는 예제중에 위에 있는 경우처럼 각 프로파일별로 XML 파일을 만들어 정의하는 방법과 하나의 XML 파일 안에 <beans> 요소를 통해 여러개의 프로파일을 정의하는 방법으로 사용할 수 있다.
그리고 여러 프로파일을 동시에 지정하고 싶다면 <beans profile="profile1, profile2, ....."> 과 같이 쉼표로 구분해서 나열하면 된다.
3. 어노테이션 기반 설정 방식에서의 profile 정의
어노테이션 기반 설정 방식에서는 컴포넌트에 @Profile 어노테이션을 부여하고 프로파일을 지정하면 된다.
@Component
@Profile("staging")
public class DummyUserRepository implements UserRepository {
....
}
만약 프로파일이 별도로 지정되지 않은 bean 설정이 있다면 이것들은 모든 프로파일에서 사용이 가능하다고 보면 된다.
프로파일 선택
실행시 어떤 프로파일을 선택해야 할지에 대한 정보는 시스템 프로퍼티를 통해 전달 할 수 있는데, 자바 애플리케이션을 실행할 때 명령행 옵션으로 spring.profiles.active라는 프로퍼티 값과 사용할 프로파일 이름을 지정하면 된다.
# 자바 명령행 옵션으로 프로파일을 지정하는 방법
-Dspring.profiles.active=production
만약 프로파일을 여러개 선택하고 싶으면 쉼표로 구분해서 나열할 수 있다.
자바 명령행 옵션으로 전달하는 방법 외에도 환경 변수를 이용할 수도 있는데 환경 변수명 SPRING_PROFILES_ACTIVE에 사용할 프로파일 이름을 지정하면 된다.
# 환경 변수로 프로파일을 지정하는 방법
export SPRING_PROFILES_ACTIVE=production
웹 애플리케이션에서는 웹 애플리케이션 설정 파일(Web Application Deployment Descriptor)인 web.xml에 다음과 같이 작성하면 된다.
//어노테이션 기반으로 scope 설정
//singleton Scope Bean
@Component
public class UserServiceImpl implements UserService{
}
//prototype Scope Bean
@Component
@Scope("prototype")
public class UserServiceImpl implements UserService{
}
//xml 기반 설정 테스트
//xml singleton Scope Bean Test
@RunWith(SpringJUnitClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class TestClass{
@Test
public void singletonScopeXmlTest() {
ApplicationContext context = new GenericXmlApplicationContext(
"file:src/main/webapp/WEB-INF/applicationContext.xml"
);
UserService userService1 = context.getBean(UserService.class);
UserService userService2 = context.getBean(UserService.class);
assertEquals(userService1, userService2);
//테스트 통과
}
}
//xml prototype Scope Bean Test
@RunWith(SpringJUnitClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class TestClass{
@Test
public void prototypeScopeXmlTest() {
ApplicationContext context = new GenericXmlApplicationContext(
"file:src/main/webapp/WEB-INF/applicationContext.xml"
);
UserService userService1 = context.getBean(UserService.class);
UserService userService2 = context.getBean(UserService.class);
assertEquals(userService1, userService2);
//테스트 실패
}
}
//Annotation 기반 설정 테스트
//Annotation singleton Scope Bean Test
@RunWith(SpringJUnitClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class TestClass{
@Test
public void singletonScopeAnnotationTest() {
ApplicationContext context = new AnnotationConfigApplicationContext("com.example");
UserService userService1 = context.getBean(UserService.class);
UserService userService2 = context.getBean(UserService.class);
assertEquals(userService1, userService2);
//테스트 통과
}
}
//Annotation prototype Scope Bean Test
@RunWith(SpringJUnitClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class TestClass{
@Test
public void prototypeScopeAnnotationTest() {
ApplicationContext context = new AnnotationConfigApplicationContext("com.example");
UserService userService1 = context.getBean(UserService.class);
UserService userService2 = context.getBean(UserService.class);
assertEquals(userService1, userService2);
//테스트 실패
}
}
테스트 결과로 어느 설정이건 스코프 설정을 아무것도 하지 않았을때는 default로 singleton 스코프가 되고,
prototype으로 정의했을 때는 제대로 다른 인스턴스가 생성되는 것을 확인할 수 있다.
스프링 MVC와 같이 스프링 프레임워크를 기반으로 한 웹 애플리케이션을 사용할 때는 굳이 reqeust나 session 스코프 등의 웹 애플리케이션 전용 스코프는 설정하지 않아도 된다.
하지만 Servlet Filter와 같이 스프링 프레임워크를 활용하지 않는 영역에서 웹 애플리케이션 전용 스코프의 bean을 사용하고 싶다면 web.xml에 설정을 추가해주면 된다.
JSF 같은 다른 웹 애플리케이션 프레임워크에서도 스프링과 연계할 때 이 방법을 활용한다.
스코프는 bean의 생존 기간을 의미하기 때문에 각 bean의 스코프가 다르다는 것은 각각의 수명이 다르다는 것이다.
예를들어 singleton 스코프가 prototype 스코프보다 더 오래산다.
웹 애플리케이션 환경이라면 request < session < singleton 순서로 오른쪽으로 갈 수록 더 오래 산다.
IoC 컨테이너에서는 bean 간의 의존관계가 형성되는데,
만약 하나의 bean이 또 다른 bean에 의존하고 있다면 IoC 컨테이너에 의해 주입된 bean은 자신의 스코프와 상관없이 주입받는 bean의 스코프를 따르게 된다.
prototype 스코프의 bean을 singleton 스코프의 bean에 주입시켰을 때
prototype 스코프의 bean은 자신을 주입받은 singleton 스코프의 bean이 살아있는 한은 IoC 컨테이너에서 다시 만들 필요가 없기 때문에 결과적으로는 singleton 스코프의 bean과 같은 수명을 갖게 된다.
좀 더 구체적인 예시로 PasswordEncoder는 Multi Thread 환경에서 안전하지 않기 때문에 반드시 요청을 받을 때 마다 새로 생성하는 prototype 스코프로 동작해야 한다.
만약 singleton으로 처리되어 여러 스레드가 동시에 이 bean을 이용하게 된다면 오동작을 일으킬 수 있다.
//passwordEncoder Bean
@Bean
@Scope("prototype")
PasswordEncoder passwordEncoder() {
//Multi Thread 환경에서 안전하지 않으므로 singleton으로 사용하면 안된다.
return new ThreadUnsafePasswordEncoder();
}
//passwordEncoder bean을 주입받는 UserService Bean
@Component
public class UserServiceImpl implements UserService {
@Autowired
PasswordEncoder passwordEncoder;
public void register(User user, String rawPassword) {
String encodedPassword = passwordEncoder.encode(rawPassword);
//생략
}
}
위 예제를 보면 passwordEncoder Bean은 prototype으로 정의되어 있고 UserServiceImpl은 별도의 스코프 정의가 없기 때문에 singleton 스코프를 갖게 된다.
그리고 UserServiceImpl에서는 passwordEncoder를 주입받아 register 메소드에서 사용하고 있다.
그럼 passwordEncoder는 prototype으로 정의되었지만 singleton인 UserServiceImpl에 주입되어 있기 때문에 singleton의 수명을 갖게 된다.
그럼 singleton의 수명을 갖게 되었으니 passwordEncoder에서 prototype으로 정의했더라도 singleton처럼 매번 새로운 인스턴스를 생성하지 않고 이미 만들어둔 인스턴스를 사용하게 된다.
즉, prototype으로 동작하는 것이 아닌 singleton으로 동작하게 된다.
그럼 register 메소드를 두번 실행하면 같은 passwordEncoder 인스턴스가 두번 사용되는 것이므로
prototype으로 정의해둔 의미가 없다.
이런 경우에는 Lookup Method Injection(룩업 메소드 인젝션)으로 해결하거나 Scoped Proxy(스코프트 프록시)로 해결하거나 Custom Scope(커스텀 스코프)로 해결하는 방법이 있다.
● Lookup Method Injection(룩업 메소드 인젝션)으로 해결
이 문제를 해결하는 가장 좋은 방법은 passwordEncoder를 주입받지 않는 것이다.
그 대신 필요할때마다 IoC 컨테이너에서 bean을 찾아오면 된다.
@Component
public class UserServiceImpl implements UserService {
@Autowired
ApplicationContext context; //ApplicationContext 자동 주입
public void register(User user, String rawPassword) {
//passwordEncoder 메소드를 호출한 반환값을 할당
PasswordEncoder passwordEncoder = passwordEncoder();
String encodedPassword = passwordEncoder.encode(rawPassword);
//생략
}
PasswordEncoder passwordEncoder() {
return this.context.getBean(PasswordEncoder.class);
/*
ApplicationContext를 통해 IoC 컨테이너에 등록된 bean을 직접 찾아서 가져온다.
이때 꺼내오는 bean은 원래 의도한 스코프대로 설정되어 나온다.
즉, passwordEncoder는 prototype 스코프로 설정되어 있는 상태고
register에서 호출될때마다 새로 생성되기 때문에 매번 prototype 스코프 상태의 bean을 사용할 수 있다.
*/
}
}
가장 쉬운 방법이고 이 코드가 동작하는데는 큰 문제가 없다.
의존관계에 있는 bean끼리의 낮은 결합도를 유지하기 위해 IoC 컨테이너를 사용했다.
하지만 한가지 흠이 있다면 IoC 컨테이너를 사용하는 과정에서 IoC 컨테이너에 의존적인 클래스와 API가 소스코드 상에 노출된다는 점이다.
bean 간의 의존관계는 해결했지만 IoC 컨테이너와의 의존관계가 소스코드에 남았으니 사실상 바람직하지 못한 방법이고 피해야 하는 방법이다.
IoC 컨테이너와 관련된 코드를 소스코드에 남기지 않고 bean을 찾아오게 만드는 방법으로 룩업 메소드 인젝션을 사용한다.
IoC 컨테이너에는 passwordEncoder 메소드 같은 코드를 bytecode 형태로 만드는 기능이 있다.
IoC 컨테이너가 bean을 룩업(Lookup)하는 메소드를 만든 다음에 그 메소드를 의존할 bean에게 주입하면 되는데 이 기능을 룩업 메소드 인젝션이라고 하는 것이다.
이 기능을 사용하려면 IoC 컨테이너에게 룩업을 대행하게 하고 싶은 메소드에게 @Lookup(org.springframework.beans.factory.annotation.Lookup) 어노테이션을 붙여주면 된다.
그러면 이 bean이 IoC 컨테이너에 등록되는 시점에 IoC 컨테이너에서 bean을 찾는 실제 코드가 @Lookup 어노테이션이 붙은 메소드 자리에 주입된다.
@Component
public class UserServiceImpl implements UserService {
public void register(User user, String rawPassword) {
PasswordEncoder passwordEncoder = passwordEncoder();
String encodedPassword = passwordEncoder.encode(rawPassword);
}
@Lookup
PasswordEncoder passwordEncoder() {
return null;
//return 값은 null이어도 상관이 없다.
}
}
이 동작원리를 살펴보자면 IoC 컨테이너느 UserServiceImpl의 서브 클래스를 동적으로 만든다.
이때 IoC 컨테이너는 기존의 PasswordEncoder 메소드를 IoC 컨테이너가 직접 만든 룩업 메소드로 오버라이드한다.
따라서 @Lookup을 붙인 메소드는 private이나 final을 지정하면 안된다.
매개변수 역시 지정하면 안되는데 IoC 컨테이너가 해당 메소드를 오버라이드 할 때 방해가 되기 때문이다.
@Lookup 어노테이션의 value 속성에는 bean의 이름을 지정할 수 있다.
별도의 value를 지정하지 않았다면 그때는 메소드의 반환값 타입을 보고 룩업 대상 bean을 찾게 된다.
xml 기반 설정 방식에서는 <lookup-method> 요소를 통해 룩업 메소드 인젝션을 사용할 수 있다.
이 방법은 보통 request나 session 같이 수명이 짧은 bean을 singleton 스코프같은 상대적으로 수명이 긴 bean에 주입할 때 많이 사용한다.
스코프트 프록시를 활성화 할때는 @Scope 어노테이션에 proxyMode 속성에 프록시를 만드는 방법을 지정하면 된다.
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
PasswordEncoder passwordEncoder() {
return new ThreadUnSafePasswordEncoder();
}
@Component
public class UserServiceImpl implements UserService {
@Autowired
PasswordEncoder passwordEncoder;
public void register(User user, String rawPassword) {
String encodedPassword = passwordEncoder.encode(rawPassword);
}
}
이렇게 되면 스코프트 프록시가 활성화 된 상태이기 때문에 passwordEncoder 필드에는 PasswordEncoder의 프록시가 주입되고 encode 메소드가 호출될때마다 request 스코프의 PasswordEncoder 인스턴스가 만들어진다.
스코프트 프록시를 사용하려면 proxyMode 속성에 다음 중 하나를 지정해야 한다.
ScopedProxyMode.INTERFACES
JDK의 동적 프록시(java.lang.reflect.Proxy)를 사용해 인터페이스 기반의 프록시를 만든다.
ScopedProxyMode.TARGET_CLASS
스프링 프레임워크에 내장되어 있는 CGLIB을 사용해 서브클래스 기반의 프록시를 만든다.
스코프트 프록시 방식은 proxyMode에 따라 인터페이스나 서브클래스 기반으로 프록시를 만든다.
아래 예제는 생성되는 프록시의 예제로 동적으로 생성되는 만큼 반드시 이렇게 만들어진다는 보장은 없고 단지 차이를 보기위한 예시정도이다.
//인터페이스 기반 프록시
public class PasswordEncoderProxy implements PasswordEncoder {
@Autowired
ApplicationContext context;
@Override
public String encode(String rawPassword) {
PasswordEncoder passwordEncoder = context.getBean("passwordEncoder", PasswordEncoder.class);
return passwordEncoder.encode(rawPassword);
}
}
//서브클래스 기반 프록시
public class PasswordEncoderProxy extends ThreadUnsafePasswordEncoder {
@Autowired
ApplicationContext context;
@Override
public String encode(String rawPassword) {
PasswordEncoder passwordEncoder = context.getBean("passwordEncoder", PasswordEncoder.class);
return passwordEncoder.encode(rawPassword);
}
}
만약 스코프트 프록시를 적용할 대상 bean이 인터페이스를 갖고 있지 않은 경우 서브클래스 기반 프록시를 사용해야 한다.
그리고 서브클래스 기반의 프록시는 메소드를 오버라이드 해야 하기 때문에 메소드나 클래스에 final을 붙일 수 없다.
XML 기반 설정 방식으로 스코프트 프록시를 표현할때는 <aop:scoped-proxy> 요소를 사용한다.
그리고 <beans> 요소는 aop 요소를 사용하기 위한 네임스페이스(xmlns)와 스키마(xsi) 정보가 추가되어 있어야 한다.
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- aop 기능을 사용하기 위해 aop 관련 네임스페이스와 스키마 정보를 추가 -->
<bean id="passwordEncoder"
class="com.example.ThreadUnsafePasswordEncoder"
scope="request">
<aop:scoped-proxy proxy-target-class="false"/>
<!-- 스코프트 프록시를 적용할 bean의 <bean> 요소 아래에 <aop:scoped-proxy> 요소를 정의한다.
proxy-target-class 속성을 false로 지정하면 인터페이스를 기반으로 한 프록시가 만들어지고
true로 지정하면 서브클래스 기반 프록시가 만들어진다. -->
</bean>
<bean id="userService" class="com.example.UserServiceImpl">
<property name="passwordEncoder" ref="passwordEncoder"/>
<!-- 생략 -->
</bean>
</beans>
어노테이션 기반 설정 방식으로 스코프트 프록시를 표현할 때는 스캔 대상 클래스에 붙인 @Scope 어노테이션에 proxyMode 속성을 추가하면 된다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
public class ThreadUnsafePasswordEncoder implements PasswordEncoder {
//생략
}
스프링 공식문서에서는 request, session, globalSession 스코프에서 스코프트 프록시를 사용하고 prototype 스코프에 대해서는 룩업 메소드 인젝션을 사용하도록 안내하고 있다.
프로토타입 스코프에서 스코프트 프록시를 사용하지 못하는 것은 아니지만,
주입된 필드에서 프록시 안에 있는 메소드를 한번 더 호출하기 때문에 매번 새로운 인스턴스가 만들어질때마다
각 프록시의 메소드가 반복해서 호출되므로 효율성 측면에서 바람직하지 않다는 점을 감안해야 한다.
● Custom Scope로 해결
스프링 프레임워크에서는 기존에 미리 만들어져 제공되는 스코프 외에도 사용자가 직접 정의한 Custom Scope(커스텀 스코프)를 만들 수 있다.
커스텀 스코프를 만들려면 Scope 인터피에스(org.springframework.beans.factory.config.Scope)를 구현하고 CustomScopeConfiguration 클래스(org.springframework.beans.factory.config.CustomScopeConfiguration)에 자신이 만든 스코프를 스코프 명과 함께 설정하면 된다.
아래는 Scope 인터페이스를 직접 구현하는 대신 스프링 프레임워크에서 제공하는 샘플 구현체를 사용한 예다.
샘플로 제공되는 SimpleThreadScope 클래스(org.springframework.context.support.SimpleTreadScope)를 커스텀 스코프라고 생각하고 자바 기반 설정 방식으로 어떻게 설정하는지를 보면 된다.
@Bean
static CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("thread", new SimpleThreadScope());
return configurer;
}
여기까지 하고 나면 thread 단위로 스코프를 주고 싶은 bean에 @Scope("thread") 어노테이션만 붙이면 된다.
그럼 IoC 컨테이너에 해당 bean을 요청할 때마다 thread 단위로 인스턴스가 만들어진다.
타입으로 오토와이어링 할때 IoC컨테이너에 같은 타입의 Bean이 여러개 있다면 그 중에서 어떤 것을 사용해야 할 지 알 수가 없다.
그래서 이런 경우에는 NoUniqueBeanDefinitionException이 발생한다.
보이는 의미 그대로 Bean 정의가 유일하지 않은 예외이다.
이처럼 같은 타입의 Bean이 여러개 정의된 경우에는 @Qualifier 어노테이션을 추가하면서 Bean 이름을 지정하면 같은 타입의 Bean 중에서 원하는 Bean만 선택할 수 있다.
//같은 타입의 Bean 여러개를 자바 기반 방식으로 정의
@Configuration
@ComponentScan
public class AppConfig {
@Bean
PasswordEncoder sha256PasswordEncoder() {
return new Sah256PasswordEncoder();
}
@Bean
PasswordEncoder bcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
//PasswordEncoder를 오토와이어링
@Compoent
public class UserServiceImpl implements UserService {
@Autowired
@Qualifier("sha256PasswordEncoder")
PasswordEncoder passwordEncoder;
....
}
이렇게 동일한 PasswordEncoder 타입이고 이름이 다른 두개의 Bean이 존재할 때
그냥 @Autowired 어노테이션 하나만으로 처리하면 NoUniqueBeanDefintionExcpetion이 발생하는 것이다.
둘다 동일하게 PasswordEncoder 타입이기 때문에 어떤것을 가져와야 할지 알 수 없기 때문이다.
그래서 @Qualifier 어노테이션에 원하는 Bean의 이름(sha256PasswordEncoder)을 추가로 명시하면 그에 해당하는 Bean을 선택할 수 있게 된다.
이걸 해결하는 또 다른 방법이 하나 더 있는데 만약 100번 정도의 오토와이어링에서 90번은 bcryptPasswordEncoder를 사용하는데 10번만 sha256PasswordEncoder를 가져와야 한다고 가정했을 때 100번 모두 @Qualifier 어노테이션을 전부 다 부여해서 처리하기에는 작성할 때 오타가 날 수도 있고 작성해야 하는 코드 양도 너무 많아진다.
그래서 우선적으로 선택할 수 있도록 하는 어노테이션이 존재한다.
//같은 타입의 Bean 여러개를 자바 기반 방식으로 정의
@Configuration
@ComponentScan
public class AppConfig {
@Bean
PasswordEncoder sha256PasswordEncoder() {
return new Sah256PasswordEncoder();
}
@Bean
@Primary
PasswordEncoder bcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
//PasswordEncoder타입 중 bcryptPasswordEncoder를 오토와이어링
@Compoent
public class UserServiceImpl implements UserService {
@Autowired
PasswordEncoder passwordEncoder;
....
}
//PasswordEncoder타입 중 sha256PasswordEncoder를 오토와이어링
@Compoent
public class UserServiceImpl implements UserService {
@Autowired
@Qualifier("sha256PasswordEncoder")
PasswordEncoder passwordEncoder;
....
}
이렇게 우선적으로 가져오도록 할 Bean에 @Primary 어노테이션을 부여해주게 되면 @Qualifier가 붙어있지 않은 오토와이어링에서 먼저 선택해 가져오게 된다.
그럼 남은 10개의 오토와이어링에서만 예제 제일 아래처럼 @Qualifier로 sha256PasswordEncoder를 가져오도록 하면 100번의 오토와이어링을 좀 더 편하게 처리할 수 있게 된다.
하지만 이렇게 @Qualifier로 수식하는 Bean의 이름에 구현 클래스의 이름이 포함된다거나 구현과 관련된 정보가 포함되어 있다면 명명 방법이 바람직하다고 볼 수는 없다.
즉, 코드에서 '나는 지금 여기에 sha256PasswordEncoder를 오토와이어링 했어요!' 라고 알리는것은 좋지 않다는 것이다.
결합도를 낮추기 위해 DI 방식을 채택했는데 Bean을 사용할 때 특정 구현체가 사용될 것으로 의식한 이름을 지정해버리면 굳이 DI를 사용하는 의미가 없어지기 때문이다.
굳~이 이렇게 사용해야 한다면 DI 를 사용하지 않는것이 더 나을 수도 있다.
이런 경우네는 Bean의 이름으로 구현체의 이름을 쓰는 대신 역할이나 사용목적, 혹은 용도로 이름을 쓰는 것이 좋다.
sha256은 bcrypt에 비해 '경량'이기 때문에 책에서 예제는 'lightweight'라는 이름으로 지어 작성했다.
@Configuration
@ComponentScan
public class AppConfig {
@Bean(name = "lightweight")
PasswordEncoder sha256PasswordEncoder() {
return new Sha256PasswordEncoder();
}
@Bean
@Primary
PasswordEncoder bcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Autowired
@Qualifier("lightweight")
PasswordEncoder passwordEncoder;
이렇게 @Bean에서 name 속성에 이름을 명시함으로써 직접적인 구현체의 이름을 쓴다거나 클래스에 대한 정보가 포함되는 것을 방지할 수 있다.
이런 Bean의 역할이나 용도는 문자열 형태의 이름이 아닌 타입(어노테이션)으로 표현할 수도 있다.
//LightWeight 어노테이션 생성
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LightWeight {
}
//Bean 에 적용
@Configuration
@ComponentScan
public class AppConfig {
@Bean
@LightWeight
PasswordEncoder sha256PasswordEncoder() {
return new Sha256PasswordEncoder();
}
@Bean
@Primary
PasswordEncoder bcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
//오토와이어링에서 사용
@Autowired
@LightWeight
PasswordEncoder passwordEncoder;
이렇게 어노테이션을 생성해 Bean을 정의하고 필드 인젝션시에도 사용할 수 있다.
이처럼 직접 어노테이션을 정의하는 방식은 문자열로 된 Bean 이름을 지정하는 방식과 달리 오타가 발생하게 되었을 때 IDE에서 잡아내 주기 때문에 편하게 확인할 수 있다.
그리고 여러개의 유사한 Bean을 정의해야 한다면 최적의 방법이다.
물론 이렇게 처리하더라도 해당 구현에 대한 정보를 담고 있는 @Sha256 이러한 명명은 피해야 한다.
이름으로 오토와이어링하기
Bean의 이름이 필드명이나 프로퍼티명과 일치할 경우에 Bean 이름으로 필드 인젝션을 하는 방법도 있다.
이 방법에서는 JSR 250 사양을 지원하는 @Resource 어노테이션(javax.annotation.Resource)을 활용한다.
@Component
public class UserServiceImpl implements UserService {
@Resource(name = "sha256PasswordEncoder")
PasswordEncoder passwordEncoder;
....
}
이 예제는 @Qualifier 대신 @Resource가 들어간 것이다.
그리고 이전에 타입으로 오토와이어링 할 때 봤던 예제 중에서 @Bean의 name 속성이나 @Lightweight 어노테이션을 사용하지 않은 경우이다.
하지만 @Resource 어노테이션의 name 속성에 Bean의 이름을 명시함으로써 sha256으로 선택이 되는것이다.
//필드 이름과 일치하는경우
@Component
public class UserServiceImpl implements UserService {
@Resource
PasswordEncoder sha256PasswordEncoder;
....
}
//프로퍼티 이름과 일치해 세터인젝션을 하는 경우
@Component
public class UserServiceImpl implements UserService {
private PasswordEncoder passwordEncoder;
@Resource
public void setSha256PasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
....
}
@Resource 어노테이션은 이렇게 두가지 경우로 사용할 수 있다.
이 두가지의 경우에 해당하지 않는다면 이름으로 오토와이어링을 하는것이 아닌 타입으로 오토와이어링을 해야 한다.
@Resource 방식은 조금 복잡한 편이라 동작방식을 제대로 이해한 후에 사용하는 것이 좋으며 컨스트럭터 인젝션에서는 @Resource 어노테이션을 사용하지 못한다.
Collection이나 Map 타입으로 오토와이어링
스프링에서는 위에서 봤던 방법들처럼 하나로 정의된 Bean을 가져오는 방법 외에도 같은 인터페이스를 구현한 Bean을 Collection이나 Map에 담아 가져오는 방법도 제공한다.
//IF 인터페이스를 구현한 여러개의 Bean 정의
public interface IF<T> {
}
@Component
public class IntIF1 implements IF<Integer> {
}
@Component
public class IntIF2 implements IF<Integer> {
}
@Component
public class StringIF implements IF<String> {
}
이렇게 하나의 인터페이스를 구현한 Bean이 여러개 존재하는 경우 오토와이어링을 아래와 같은 형태로 할 수 있다.
어느쪽을 사용해도 스캔하는데는 문제가 없으나 이 속성을 생략하는 경우 @ComponentScan이 설정된 클래스가 속한 패키지부터 하위 패키지만 스캔한다는 것에 유의해야 한다.
이 컴포넌트 스캔 대상이 되는 어노테이션 중 가장 많이 활용되는 네가지는 다음과 같다.
1. @Controller
MVC 패턴에서의 C. 즉, 컨트롤러 역할을 하는 컴포넌트에 붙이는 어노테이션.
클라이언트에서 오는 요청을 받고 비즈니스 로직의 처리 결과를 응답으로 돌려보내는 기능을 한다.
이때 실제 비즈니스 로직은 @Service가 붙은 컴포넌트에서 처리하도록 위임한다.
2. Service
비즈니스 로직(service)을 처리하는 컴포넌트에 붙이는 어노테이션.
컨트롤러에서 받은 입력 데이터를 활용해 비즈니스 로직을 실행하는 기능을 한다.
이때 영속적으로 보관해야 하는 데이터가 있다면 @Repository가 붙은 컴포넌트에서 처리하도록 위임한다.
3. @Repository
영속적인 데이터 처리를 수행하는 컴포넌트에 붙이는 어노테이션.
ORM 관련 라이브러리를 활용해 데이터의 CRUD를 처리하는 기능을 한다.
4. @Component
위의 세 경우에 해당하지 않는 컴포넌트(유틸리티 클래스나 기타 지원 클래스 등)에 붙이는 어노테이션.
이 컴포넌트 스캔 대상들 외에도 추가로 다른 컴포넌트를 더 포함하고 싶다면 필터를 적용해 스캔 범위를 커스터마이징 할 수 있다.
스프링에서는 아래와 같은 필터를 제공한다.
어노테이션을 활용한 필터(ANNOTATION)
할당 가능한 타입을 활용한 필터(ASSIGNABLE_TYPE)
정규 표현식 패턴을 활용한 필터(REGEX)
AspecjJ 패턴을 활용한 필터(ASPECTJ)
아래는 필터 예제다.
//할당 가능한 타입으로 필터링(인터페이스)
public interface DomainService {
}
//할당 가능한 타입으로 필터링(자바 기반 설정 방식)
@ComponentScan(basePackages = "com.example.demo"
, includeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE
, classes = {DomainService.class})
})
//정규표현식 패턴으로 필터링(자바 기반 설정 방식)
@ComponentScan(basePackages = "com.example.demo"
, includeFilters = {
@ComponentScan.Filter(type = FilterType.REGEX
, pattern = {".+DomainService$"})
})
<!-- 할당 가능한 타입으로 필터링(XML 기반 설정 방식) -->
<context:component-scan base-package="com.example.demo">
<context:include-filter type="assignable" expression="com.example.demo.domain.DomainService"/>
</context:component-scan>
<!-- 정규 표현식 패턴으로 필터링(XML 기반 설정 방식) -->
<context:component-scan base-package="com.example.demo">
<context:include-filter type="regex" expression=".+DomainService$"/>
</context:component-scan>
여기서 주의해야 할 점은 필터를 적용해서 컴포넌트 스캔을 할 때 앞서 살펴본 어노테이션이 붙은 스캔 대상도 함께 탐색 범위에 포함된다는 것이다.
만약 기본 설정에서 어노테이션이 붙은 스캔 대상을 무시하고, 순수하게 필터를 적용해서 탐색되는 컴포넌트만 사용하고 싶다면 아래와 같이 useDefaultFilters 속성을 false로 설정하면 된다.
그리고 기본 스캔 대상에 필터를 적용해 특정 컴포넌트를 추가하는 것과 반대로 빼는 방법도 있는데 이때는 excludeFilter 속성을 사용하면 된다.
//기본 스캔 대상을 제외하고 필터로만 스캔(자바 기반 설정 방식)
@ComponentScan(basePackages = "com.example.demo"
, useDefaultFilters = false
, includeFilters = {
@ComponentScan.Filter(type = FilterType.REGEX
, pattern = {".+DomainService$"})
})
//기본 스캔 대상과 특정 컴포넌트(@Exclude 어노테이션이 붙은 컴포넌트)를 제외하고 필터로만 스캔
@ComponentScan(basePackages = "com.example.demo"
, useDefaultFilters = false
, includeFilters = {
@ComponentScan.Filter(type = FilterType.REGEX
, pattern = {".+DomainService$"})
})
, excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION
, pattern = {"Exclude.class"})
})
<!-- 기본 스캔 대상을 제외하고 필터로만 스캔(XML 기반 설정 방식) -->
<context:component-scan base-package="com.example.demo" use-default-filters="false">
<context:include-filter type="regex" expression=".+DomainService$"/>
</context:component-scan>
<!-- 기본 스캔 대상과 특정 컴포넌트(@Exclude 어노테이션이 붙은 컴포넌트)를 제외하고 필터로만 스캔 -->
<context:component-scan base-package="com.example.demo" use-default-filters="false">
<context:include-filter type="regex" expression=".+DomainService$"/>
<context:exclude-filter type="annotation" expression="com.example.demo.Exclude"/>
</context:component-scan>
만약 포함(include)하는 필터와 제외(exclude)하는 필터 모두에 해당하는 컴포넌트가 있는 경우 제외하는 필터가 포함하는 필터보다 우선순위가 높기 때문에 해당 컴포넌트는 스캔 대상에서 제외되고 결과적으로 IoC 컨테이너에도 등록되지 않는다.
자바 클래스에 @Configuration Annotation을, 메소드에 @Bean Annotation을 사용해 Bean을 정의하는 방법으로 스프링 프레임워크 3.0.0부터 사용할 수 있다.
최근에는 스프링 기반 애플리케이션 개발에 자주 사용되고 특히 스프링부트에서 이 방식을 많이 사용한다.
● XML 기반 설정 방식(XML-based Configuration)
XML 파일을 사용하는 방법으로 <bean> 요소의 class 속성에 FQCN(Fully-Qualified Class Name)을 기술하면 Bean이 정의된다.
<constructor-arg>나 <property> 요소를 사용해 의존성을 주입한다. 스프링 1.0.0부터 사용할 수 있다.
● 어노테이션 기반 설정 방식(Annotation-based Configuration)
@Component 같은 마커 어노테이션(Maker Annotation)이 부여된 클래스를 탐색해서(Component Scan) IoC 컨테이너에 Bean을 자동으로 등록하는 방법이다. 스프링 2.5부터 사용할 수 있다.
하나의 방법만 사용해서 빈을 정의할 수도 있지만 대부분의 경우 자바 기반 + 어노테이션 기반을 조합하거나
XML 기반 + 어노테이션 기반을 조합해 사용한다.
그럼 이 설정 방식들에 대해 정리한다.
자바 기반 설정 방식(Java-based Configuration)
자바 기반 설정 방식에서는 자바 코드로 Bean을 설정한다.
이때 사용되는 자바 클래스를 Java Configuration Class라고 부른다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
....
//클래스에 @Configuration을 붙여 설정 클래스라고 선언한다. 설정 클래스는 여러개 정의할 수 있다.
@Configuration
public class AppConfig{
/*
메소드에 @Bean을 붙여 Bean을 정의한다.
메소드명이 Bean의 이름이 되고 그 Bean의 인스턴스가 반환값이 된다.
여기서는 userRepository가 Bean의 이름이된다.
만약 다르게 하고 싶다면
@Bean(name="userRepo") 이렇게 어노테이션에서 속성을 재정의하면 된다.
*/
@Bean
UserRepository userRepository(){ //Bean의 이름 userRepository
return new UserRepositoryImpl(); //Bean의 인스턴스 = UserRepositoryImpl
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
UserService userService(){
/*
다른 컴포넌트를 참조해야 할 때는 해당 컴포넌트의 메소드를 호출한다.
의존성 주입이 프로그램적인 방법으로 처리된다.
*/
return new UserServiceImpl(userRepository(), passwordEncoder());
}
/*
자바 기반 설정 방식에서는 메소드에 매개변수를 추가하는 방법으로
다른 컴포넌트의 의존성을 주입할 수 있다.
단, 인수로 전달될 인스턴스에 대한 Bean은 별도로 정의해야 한다.
*/
@Bean
UserService userService(UserRepository userRepository, PasswordEncoder passwordEncoder){
return new UserServiceImpl(userRepository, passwordEncoder);
}
...
}
자바 기반 설정 방식만 사용해서 Bean을 설정할 때는 애플리케이션에서 사용되는 모든 컴포넌트를 Bean으로 정의해야 한다.
어노테이션 방식과 조합하면 설정 내용의 많은 부분들을 줄일 수 있어서 조합해서 사용하면 편해진다.
XML 기반 설정 방식(XML-based Configuration)
<?xml version="1.0" encoding="UTF-8"?>
<!-- <beans> 요소 안에 Bean 정의를 여러개 한다 -->
<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">
<!-- <bean> 요소에 bean 정의를 한다.
id 속성에서 지정한 값은 bean의 이름이 되고
class 속성에서 지정한 클래스가 해당 bean의 구현 클래스다.
이때 class 속성은 FQCN으로 패키지명부터 클래스명까지 정확하게 기재해야 한다. -->
<bean id="userRepository" class="com.example.demo.UserRepositoryImpl"/>
<bean id="passwordEncoder" class="com.example.demo.BCryptPasswordEncoder"/>
<bean id="userService" class="com.example.demo.UserServiceImpl">
<!-- <constructor-arg> 요소에서 생성자를 활용한 의존성 주입을 한다.
ref 속성에 주입할 Bean의 이름을 기재한다. -->
<constructor-arg ref="userRepository"/>
<construcotr-arg ref="passwordEncoder"/>
</bean>
</beans>
XML 기반 설정 방식 역시 이것만 사용하려고 하면 모든 컴포넌트를 Bean으로 정의해야 한다.
그래서 자바기반과 마찬가지로 어노테이션 기반 방식과 조합해 사용하면 좀 더 편하게 사용할 수 있다.
그리고 의존성 주입에서 주입할 대상이 다른 bean이 아니라 특정 값인 경우에는 ref 속성을 사용하지 않고 value 속성을 사용한다.
xml 파일의 value 속성에서 문자열을 지정한다고 해서 실제 자바코드에서 그 값을 받는 타입이 반드시 문자열 타입일 필요는 없다.
xml에서 문자열로 기재되어 있더라도 필요한 경우 DI를 하는 과정에서 형변환 할 수 있기 때문이다.
어노테이션 기반 설정 방식(Annotation-based Configuration)
어노테이션 기반 설정 방식에서는 IoC 컨테이너에 관리할 Bean을 Bean 설정 파일에 정의하는 대신 Bean을 정의하는 어노테이션을 Bean의 클래스에 부여하는 방식을 사용한다.
이후 이 어노테이션이 붙은 클래스를 탐색해 IoC 컨테이너에 자동으로 등록하는데 이런 탐색과정을 ComponentScan(컴포넌트 스캔)이라고 한다.
또한 의존성 주입도 이제까지처럼 명시적으로 설정하는 것이 아니라 어노테이션이 붙어있으면 IoC 컨테이너가 자동으로 필요로 하는 의존 컴포넌트를 주입하게 한다.
이러한 주입 과정을 Autowiring(오토와이어링)이라고 한다.
//UserRepository
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
....
@Component
public class UserRepositoryImpl implements UserRepository{
....
}
//BcryptPasswordEncoder
@Component
public class BcryptPasswordEncoder implements PasswordEncoder{
....
}
//UserServiceImpl
@Component
public class UserserviceImpl implements UserService{
@Autowired
public userServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder){
....
}
....
}
이렇게 @Component를 붙여 ComponentScan이 되도록 만들어 준다.
그리고 @Autowired를 생성자에 부여해 오토와이어링이 되도록 만들어준다.
오토와이어링을 사용하면 기본적으로 주입대상과 같은 타입의 Bean을 IoC 컨테이너에서 찾아 와이어링 대상에 주입하게 된다.
컴포넌트 스캔을 할때는 스캔할 범위를 지정해야 하는데 자바 기반이나 XML 기반 방식으로 설정할 수 있다.
//자바기반 컴포넌트 스캔 범위 설정
import org.springframework.context.annotation.ComponentScan
....
@Configuration
@ComponentScan("com.example.demo")
public class AppConfig{
....
}
/*
@ComponentScan 어노테이션을 부여하고 value나 basepackage 속성에
컴포넌트 스캔을 수행할 패키지를 지정한다.
이 예제의 경우 com.example.demo 패키지 이하의 범위에 있는 모든 클래스를 스캔하고
스캔 대상이 되는 어노테이션이 부여된 클래스를 IoC 컨테이너에 자동으로 등록한다.
이 속성을 생략하고 @CompoentScan 이렇게만 어노테이션을 부여할 경우
이 클래스가 속해있는 패키지 이하의 범위만 스캔한다.
*/
<!-- XML 기반 스캔 범위 설정 -->
<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.demo"/>
</beans>
<!-- 이렇게 <context:component-scan> 요소의 base-package에 패키지를 지정하면 된다. -->
IoC 컨테이너에 등록되는 Bean의 이름은 기본적으로 클래스명의 첫 글자를 소문자로 바꾼 이름과 같다.
단, 첫 글자 이후에 대문자가 연속되는 경우에는 첫 글자를 소문자로 변환하지 않고 클래스명이 그대로 Bean이름으로 사용된다.
예를들어 UserRepositoryImpl 클래스라면 userRepositoryImpl 이라는 이름으로 사용되는 것이고
USerRepositoryImpl 클래스라면 두번째 글자 역시 대문자이기 때문에 첫글자도 소문자로 변환하지 않고 USerRepositoryImpl로 사용된다.
만약 bean 이름을 명시적으로 지정하고 싶다면 @Component("name") 이런식으로 @Component 어노테이션에 원하는 이름을 넣어주면 된다.
아래는 이런 Bean 설정 방식들을 지원하기 위한 구현 클래스의 예제다.
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의 작업 디렉토리 안에서
상대경로로 설정 파일을 탐색한다.
*/
이전 포스팅에서 정리했다시피 ApplicationContext는 단독 애플리케이션에서 스프링을 사용하거나 JUnit으로 만든 테스트 케이스 안에서 스프링을 구동할 때 사용하고, 웹 애플리케이션에서는 스프링 MVC를 활용하게 되는데 이때는 WebApplicationContext를 사용한다.
2. DI 방식(의존성 주입 방식)
의존성 주입은 총 세가지의 방법으로 사용할 수 있다.
1. 설정자 기반 의존성 주입 방식(Setter-based dependency injection)
2. 생성자 기반 의존성 주입 방식(Constructor-based dependency injection)
3. 필드 기반 의존성 주입 방식(Filed-based dependency injection)
설정자 기반 의존성 주입 방식(Setter-based dependency injection)
설정자 기반 의존성 주입 방식은 설정자 메소드(Setter Method)의 인수를 통해 의존성을 주입하는 방식이다.
줄여서 세터 인젝션(Setter injection)이라고도 부른다.
이 세터 인젝션은 설정자 메소드가 만들어져 있어야 사용할 수 있다.
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
private PasswordEncoder passwordEncoder;
//default Constructor. 생략이 가능하다.
public UserServiceImpl(){
}
public void setUserRepository(UserRepository userRepository){
this.userRepository = userRepository;
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder){
this.passwordEncoder = passwordEncoder;
}
....
}
이렇게 설정자 메소드가 만들어졌다면 의존성 주입을 할 수 있다.
우선은 이 세터 인젝션을 자바 기반 설정 방식으로 표현한 예제다.
@Configuration
public class AppConfig{
@Bean
UserRepository userRepository(){
return new UserRepositoryImpl();
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
UserService userService(){
UserServiceImpl userService = new UserServiceImpl();
userService.setUserRepository(userRepository());
userService.setPasswordEncoder(passwordEncoder());
return userService;
}
....
}
예제를 보면 설정자 메소드에 다른 컴포넌트의 참조 결과를 설정했을 뿐이다.
조금 다른 방식으로 @Bean 어노테이션을 붙인 메소드에 매개변수 형태로 의존 컴포넌트를 받게 한 후, 그 값을 설정자 메소드를 통해 주입시켜도 된다.
@Configuration
public class AppConfig{
....
@Bean
UserService userService(UserRepository userRepository, PasswordEncoder passwordEncoder){
UserServiceImpl userService = new UserServiceImpl();
userService.setUserRepository(userRepository);
userService.setPasswordEncoder(passwordEncoder);
return userService;
}
....
}
이처럼 자바 기반 설정 방식으로 세터 인젝션을 하게 되면 마치 프로그램에서 인스턴스를 직접 생성하는 것 처럼 보이기 때문에 이 코드가 bean을 정의한 설정인지 체감이 안될 수 있다.
XML 방식에서 세터 인젝션을 할 때는 주입 대상을 <property> 요소에 기술하는데 <property> 요소의 name 속성에 주입할 대상의 이름을 지정하면 된다.
마지막으로 세터 인젝션을 어노테이션 기반으로 표현한 예제다.
@Component
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
private PasswordEncoder passwordEncoder;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
....
}
이 예제처럼 설정자 메소드에 @Autowired 어노테이션을 붙여주기만 하면 된다.
어노테이션 기반의 설정 방식을 이용하면 자바기반이나 XML 기반의 설정방식 처럼 별도의 설정 파일을 둘 필요가 없다.
생성자 기반 의존성 주입 방식(Consturctor-based Configuration)
생성자 기반 의존성 주입 방식은 생성자의 인수를 사용해 의존성을 주입하는 방식이다.
생성자(Constructor)를 그대로 따와 컨스트럭터 인젝션(Constructor Injection)이라고 부른다.
컨스트럭터 인젝션의 설정 방식은 Bean 설정 방식에서의 예제에서 확인할 수 있다.
//constructor injection의 자바기반 설정방식
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
....
@Configuration
public class AppConfig{
....
@Bean
UserService userService(UserRepository userRepository, PasswordEncoder passwordEncoder){
return new UserServiceImpl(userRepository, passwordEncoder);
}
...
}
<!-- constructor injection의 xml 기반 설정 방식 -->
<bean id="userService" class="com.example.demo.UserServiceImpl">
<constructor-arg ref="userRepository"/>
<construcotr-arg ref="passwordEncoder"/>
</bean>
//constructor injection의 어노테이션 기반 설정방식
@Component
public class UserserviceImpl implements UserService{
@Autowired
public userServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder){
....
}
....
}
이렇게 자바 기반 설정 방식에서는 생성자에 의존 컴포넌트를 직접 설정하고,
XML 기반 설정 방식에서는 <constructor-arg> 요소에서 참조하는 컴포넌트를 설정한다.
그리고 어노테이션 기반 설정 방식에서는 생성자에 @Autowired를 부여한다.
xml 기반 설정방식에서 생성자가 여러개의 인자를 필요로 하는 경우 <consturctor-arg> 를 여러번 정의하면 되는데
순서에 주의해야 한다.
이때 다음과 같이 index 속성을 활용하면 생성자 인수의 순서를 명시적으로 지정해 가독성을 향상시키고 추가/삭제시에 발생할 수 있는 실수를 쉽게 발견한다는 장점이 있다.
<!-- constructor injection의 xml 기반 설정 방식 -->
<bean id="userService" class="com.example.demo.UserServiceImpl">
<constructor-arg index="0" ref="userRepository"/>
<construcotr-arg index="1" ref="passwordEncoder"/>
</bean>
다른 방법으로는 name 속성에 인수명을 지정할 수도 있다.
<!-- constructor injection의 xml 기반 설정 방식 -->
<bean id="userService" class="com.example.demo.UserServiceImpl">
<constructor-arg name="userRepository" ref="userRepository"/>
<construcotr-arg name="passwordEncoder" ref="passwordEncoder"/>
</bean>
name 속성을 활용하면 인수의 순서가 바뀌거나 추가될 때도 인덱스 순서를 매번 변경하지 않아도 되는 장점이 있다.
다만 인수명 정보는 소스코드가 컴파일 되는 과정에서 없어지기 때문에 컴파일 할 때 javac 명령과 함께 디버깅 정보를 전달할 수 있는 -g 옵션을 사용하거나, JDK 8 이후부터는 메소드 매개변수의 메타 정볼르 생성할 수 있는 -parameters 옵션을 사용해야 한다.
만약 이렇게 별도의 컴파일 옵션을 주는것이 번거롭다면 아래와 같이 @ConsturctorProperties 어노테이션을 달아주는 방법도 있다.
@ConstructorProperties({"userRepository", "passwordEncoder"})
public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder) {
}
컨스트럭터 인젝션을 사용하면 필드를 final로 선언해서 생성후에 변경되지 않게 만들 수 있다.
이렇게 필드를 변경하지 못하도록 엄격하게 제한을 거는것은 다른 의존성 주입 방식으로는 처리하지 못하고 오직 컨스트럭터 인젝션에서만 가능하다.
필드 기반 의존성 주입 방식(Field dependency Injection)
필드 기반 의존성 주입 방식은 생성자나 설정자 메소드를 쓰지 않고 IoC 컨테이너의 힘을 빌려 의존성을 주입하는 방식이다.
필드 인젝션을 할 때는 의존성을 주입하고 싶은 필드에 @Autowired 어노테이션을 달아주면 된다.
이처럼 필드 기반 의존성 주입 방식을 사용하면 생성자나 설정자 메소드를 굳이 만들 필요가 없어지기 떄문에 두 메소드의 작성을 생략해 소스코드가 비교적 간결해 보이는 장점이 있다.
웹 개발을 공부하다보면 많이 볼 수 있는 방식이다.
@Component
public class UserServiceImpl implements UserService {
@Autowired
UserRepository userRepository;
@Autowired
PasswordEncoder passwordEncoder;
....
}
필드 인젝션을 사용할 때는 한가지 주의할 점이 있다.
소스코드의 양을 줄이기 위해 생성자나 설정자 메소드를 생략하고 싶다면 반드시 IoC 컨테이너를 사용한다는 것을 전제해야 한다는 것이다.
IoC 컨테이너 없이 사용되는 독립형 라이브러리로 사용될 소스코드에서 필드 인젝션을 사용하는 것은 잘못된 판단이다.
이런 스프링의 간단한 유틸리티 메소드를 이용하면 스프링 밖의 어디서라도 웹 애플리케이션의 RootApplicationContext를 얻을 수 있다.
그리고 getBean() 메소드를 사용하면 RootContext의 어떤 Bean이든 가져와 쓸 수 있다.
ServletContext는 웹 애플리케이션마다 하나씩 만들어지는 것으로, Servlet의 runtime 환경정보를 담고있다.
HttpServletRequest나 HttpSession 오브젝트를 갖고 있다면 간단히 ServletContext를 가져올 수 있는데 스프링과 연동돼서 사용할 수 있는 서드파티 웹 프레임워크는 이 방법을 사용해 스프링 Bean을 가져와 사용한다.
ServletContext에 접근할 수 있는 JSP나 일반 Servlet에서도 가능하다.
프레젠테이션 계층을 분리해서 계층구조로 ApplicationContext를 구성해두면 언제든 간단히 웹 기술을 확장하거나 변경, 조합해서 사용할 수 있으므로 당장에는 스프링 servlet 하나만 존재한다고 해도 계층 구조로 만들어두는것이 권장된다.
계층구조가 만들어지기 때문에 계층구조에서의 주의사항을 항상 염두에 두어야 한다.
계층구조에서는 부모 컨텍스트와 자식 컨텍스트가 동일한 이름의 Bean을 갖고 있다면 자식 컨텍스트의 Bean이 우선된다.
예를들어 hello라는 Bean이 ChildContext와 ParentContext에 동일하게 hello라는 이름으로 존재한다고 하고 hello Bean을 요청했을 때 ChildContext에 존재하는지 먼저 확인한 뒤에 존재하지 않으면 ParentContext에서 찾기 때문에 ChildContext에 있는 Bean을 우선적으로 가져오게 된다.
또한 자바에서 상속받았을 때 처럼 자식 컨텍스트의 Bean은 부모 컨텍스트의 Bean을 참조할 수 있지만 반대로 부모 컨텍스트가 자식 컨텍스트의 Bean을 참조하는것은 불가능하다.
웹 애플리케이션에서 계층구조는 ServletContext와 RootApplicationContext 두가지의 구조가 만들어지고 ServletContext가 자식 컨텍스트, RootApplicationContext가 부모 컨텍스트다.
그럼 Servlet Context의 Bean은 RootApplicationContext의 Bean을 참조할 수 있지만 반대는 불가능하고,
두 컨텍스트에 동일한 이름의 Bean이 존재한다면 RootApplicationContext보다 ServletContext에 존재하는 Bean이 더 우선되어 RootApplciationContext의 Bean은 무시될 수 있다.
하나의 컨텍스트에 정의된 AOp 설정은 다른 컨텍스트의 Bean에는 영향을 미치지 않는다는 점도 주의해야 한다.
스프링 애플리케이션의 XML 설정 파일은 보통 계층이나 성격에 따라 여러개의 파일로 분리해서 작성하면 편리하다.
이런 경우 각 설정파일마다 하나씩 컨텍스트를 만들고 계층구조로 묶는 방법도 가능하겠지만, 반대로 하나의 컨텍스트가 여러개의 설정 파일을 사용하도록 할 수도 있다.
같은 컨텍스트가 사용할 Bean 설정이라면 굳이 파일을 여러개 쪼개서 작성하지 않고 하나에 작성하는 것이 더 낫지 않을까 하는 생각도 할 수 있지만 등록되는 빈의 개수가 많은 경우 파일 하나에 설정정보의 양이 너무 많아지게 되고 작성하거나 관리하는데 불편해지기 때문에 계층별로 구분해두거나 자주 바뀌는 설정과 고정된 설정을 구분하는게 좋다.
2. Web Application의 context 구성방법
웹 애플리케이션의 ApplicationContext를 구성하는 방법으로는 다음 세가지를 고려해볼 수 있다.
1. ServletContext와 RootApplicationContext 계층구조
가장 많이 사용되는 기본적인 구성방법이다.
스프링 웹 기술을 사용하는 경우 웹 관련 Bean들은 Servlet의 컨텍스트에 두고 나머지는 RootApplicationContext에 등록한다.
Root 컨텍스트는 모든 Servlet 레벨 컨텍스트의 부모 컨텍스트가 된다.
스프링 웹 외에도 기타 웹 프레임워크나 HTTP 요청을 통해 동작하는 각종 서비스를 함께 사용할 수도 있다.
2. RootApplicationContext 단일구조
스프링 웹 기술을 사용하지 않고 서드파티 웹 프레임워크나 서비스엔진만을 사용해서 프레젠테이션 계층을 만든다면 스프링 Servlet을 둘 이유가 없다.
따라서 servlet의 ApplciationContext도 사용하지 않게 된다.
이때는 RootApplicationContext만 등록해주면 된다.
3. ServletContext 단일구조
스프링 웹 기술을 사용하면서 스프링 외의 프레임워크나 서비스 엔진에서 스프링의 Bean을 이용할 생각이 아니라면 RootApplicationContext를 생락할 수도 있다.
대신 Servlet에서 만들어지는 컨텍스트에 모든 Bean을 다 등록하면 된다.
계층구조를 사용하면서 발생할 수 있는 혼란을 근본적으로 피하고 단순한 설정을 선호한다면 이 방법을 선택할 수 있다.
이때는 servlet안에 만들어지는 ApplicationContext가 부모 컨텍스트를 갖지 않기 때문에 스스로 Root 컨텍스트가 된다.
이렇게 만들어지는 Servlet context는 컨텍스트 계층 관점에서 보자면 Root 컨텍스트이지만 웹 애플리케이션 레벨에 두는 공유 가능한 Root 컨텍스트와는 구별된다.
이 방법들 중 첫번째 방법은 컨텍스트 계층구조를 만드는 방법이고 나머지 두 방법은 단일구조로 컨텍스트를 하나만 사용하는 방법이다.
첫번째와 세번째 방법은 스프링 웹 기능을 사용하는 경우이고, 두번째 방법은 스프링 웹 기술을 사용하지 않을 때 적용 가능한 방법이다.
3. RootApplicationcontext, ServletApplicationContext 등록
RootApplicationContext를 먼저 정리하고 그 다음 ServletApplicationContext를 정리한다.
웹 애플리케이션 레벨에 만들어지는 Root Web ApplicationContext를 등록하는 가장 간단한 방법은 servlet의 이벤트리스너(EventListener)를 이용하는 것이다.
스프링은 웹 애플리케이션의 시작과 종료시에 발생하는 이벤트를 처리하는 리스너인 ServletContextListener를 이용한다.
ServletContextListener 인터페이스를 구현한 리스너는 웹 애플리케이션 전체에 적용 가능한 DB 연결 기능이나 logging 같은 서비스를 만드는데 유용하게 쓰인다.
이를 이용해 웹 애플리케이션이 시작될 때 RootApplicationContext를 만들어 초기화하괴, 웹 애플리케이션이 종료될 때 컨텍스트를 함께 종료하는 기능을 가진 리스너를 만들수도 있다.
ContextLoaderListener는 웹 애플리케이션이 시작할 때 자동으로 RootApplicationContext를 만들고 초기화해준다.
리스너에 별다른 파라미터를 지정하지 않으면 default로 설정된 아래의 값이 적용된다.
ApplicationContext 클래스 : XmlWebApplicationContext
XML 설정 파일 위치 : /WEB-INF/applicationContext.xml
RootApplicationContext는 웹 애플리케이션의 WEB-INF 폴더 내에 있는 applicationContext.xml 파일을 default 설정 파일로 사용한다.
Context Class와 설정파일 위치는 Servlet Context 파라미터를 선언해서 변경할 수 있다.
ContextLoaderListener가 이용할 파라미터를 <context-param> 항목 안에 넣어주면 default 설정 대신 파라미터로 지정한 내용이 적용된다.
● contextConfigLocation
default XML 설정 파일 위치는 파라미터를 선언해주는 것으로 바꿀 수 있다.
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/daoContext.xml
/WEB-INF/applicationContext.xml
<!-- 하나 이상의 XML 설정 파일을 사용할 경우
여러줄에 걸쳐 넣어주거나 공백으로 분리하면 된다. -->
</param-value>
</context-param>
이렇게 contextConfigLocation context 파라미터를 넣어주면 default 설정파일 위치인 /WEB-INF/applicationContext.xml은 무시되고 파라미터로 제공된 설정파일을 사용하게 된다.
위 예제에 /WEB-INF/applicationContext.xml 도 있는데 왜 무시된다는건가? 라고 생각했었는데 이 설정이 없으면 default 설정파일 위치인 /WEB-INF/applicationContext.xml만을 설정 파일로 사용하는 것이고, 위 예제 처럼 contextConfigLocation 컨텍스트 파라미터를 넣어주면 /WEB-INF/daoContext.xml과 /WEB-INF/applicationContext.xml을 설정 파일로 사용하게 된다.
즉, 위 예제에서 applicationContext.xml을 빼고 다른 xml 설정 파일 경로를 작성한다면 applicationContext.xml 말고 그 파일을 설정파일로 사용하게 된다는 의미다.
설정 파일의 위치는 resource loader가 사용하는 접두어를 사용해 표현할 수도 있으며 접두어를 붙이지 않으면 웹 애플리케이션의 servlet resource path로부터 파일을 찾는다.
그래서 보통 /WEB-INF/로 시작하게 된다.
servlet resource path 대신 classpath로부터 설정파일을 찾게 할 수도 있는데 다음과 같이 classpath: 를 붙여주면 된다.
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에 작성했다고 하면 아래와 같다.
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에 작성하고 테스트하면 아래처럼 하면 된다.
지정한 타입에 해당하는 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 생성자에 넣어주면 되며 하나 이상의 파일을 지정할 수도 있다.
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이 아닌 일반 오브젝트와 연동될 수 있다.
스프링에서는 오브젝트의 생성과 관계 설정, 사용, 제거 등의 작업을 애플리케이션 코드 대신 독립된 컨테이너가 담당한다.
이를 컨테이너가 코드 대신 오브젝트에 대한 제어권을 갖고 있다고 해서 IoC(Inversion of Control, 제어의 역전)라고 부른다.
객체의 생성, 생명주기의 관리까지 모든 객체에 대한 제어권이 바뀌었다는 것을 의미하고 컴포넌트 의존관계 설정(Component dependency resolution), 설정(Configuration) 및 생명주기(Lifecycle)을 해결하기 위한 디자인 패턴이다.
스프링에서 객체를 생성하고 관리하고 책임지고 의존성을 관리해주는 컨테이너를 IoC 컨테이너, DI 컨테이너, 스프링 컨테이너라고 부른다.
컨테이너는 인스턴스 생성부터 소멸까지의 인스턴스 생명주기 관리를 개발자 대신 해준다.
그럼 개발자는 객체관리 주체가 프레임워크(컨테이너)가 되기 때문에 로직에 집중할 수 있다는 장점이 있다.
IoC 컨테이너는 객체의 생성을 책임지고, 의존성을 관리한다.
POJO의 생성, 초기화, 서비스, 소멸에 대한 권한을 가진다.
개발자들이 직접 POJO를 생성할 수 있지만 컨테이너에게 맡긴다.
개발자는 비즈니스 로직에만 집중할 수 있다.
객체 생성 코드가 없으므로 의존하는 컴포넌트간의 결합도를 낮춰 테스트가 용이하다.
그리고 IoC 하면 따라오는것이 DI(Dependency Injection, 의존성 주입)이다.
DI는 IoC 디자인 패턴중 하나다.
IoC는 DL(Dependency Lookup)과 DI(Dependency Injection)으로 나눌 수 있는데
DL은 저장소에 저장되어 있는 Bean에 접근하기 위해 컨테이너가 제공하는 API를 이용해 Bean을 LookUp하는 것이고
DI는 각 클래스간의 의존관계를 Bean 설정(Bean Definition) 정보를 바탕으로 컨테이너가 자동으로 연결해주는 것이다.
이렇게 DL과 DI로 나눠지지만 DI와 보통 묶어서 정리하는 이유는 DL을 사용할 경우 컨테이너 종속이 증가하기 때문에 DI를 많이 사용하기 때문이라고 한다.
스프링에서는 IoC를 담당하는 컨테이너를 BeanFactory, ApplicationContext라고 부리기도 한다.
오브젝트의 생성과 오브젝트 사이의 런타임 관계를 설정하는 DI 관점으로 볼 때는 컨테이너를 BeanFactory라고 하고, DI를 위한 BeanFactory에 Enterprise Application을 개발하는데 필요한 여러가지 컨테이너 기능을 추가한것을 ApplicationContext라고 부른다.
BeanFactory
BeanFactory 계열의 인터페이스만 구현한 클래스는 단순히 컨테이너에서 객체를 생성하고 DI를 처리하는 기능만 제공한다.
Bean을 등록, 생성, 조회, 반환 관리를 한다.
팩토리 디자인 패턴을 구현한 것으로 BeanFactory는 빈을 생성하고 분배하는 책임을 지는 클래스다.
Bean을 조회할 수 있는 getBean() 메소드가 정의되어 있다.
보통은 BeanFactory를 바로 사용하지 않고, 이를 확장한 ApplicationContext를 사용한다.
ApplicationContext
Bean을 등록, 생성, 조회, 반환 관리하는 기능을 갖고 있으며 이 기능은 BeanFactory와 같다.
스프링의 각종 부가 기능을 추가로 제공한다.
국제화가 지원되는 텍스트 메세지를 관리해준다.
이미지 같은 파일 자원을 로드할 수 있는 포괄적인 방법을 제공해준다.
리스너로 등록된 빈에게 이벤트 발생을 알려준다.
출처 : https://dog-developers.tistory.com/12
즉, ApplicationContext는 그 자체로 IoC와 DI를 위한 BeanFactory이면서 그 이상의 기능을 가졌다고 볼 수 있다.
스프링의 IoC 컨테이너는 일반적으로 ApplicationContext를 말한다.
스프링의 BeanFactory와 ApplicationContext는 각각의 기능을 대표하는 BeanFactory와 ApplicationContext라는 두개의 인터페이스로 정의되어 있다.
그리고 ApplicationContext는 BeanFactory 인터페이스를 상속한 서브 인터페이스다.