때로는 빈을 싱글톤이 아닌 다른 방법으로 만들어 사용해야 할 때가 있다.
- 싱글톤이 아닌 빈으로는 크게 프로토타입 빈, 스코프 빈이 존재
싱글톤과 달리 빈을 요청할 때마다 새로운 오브젝트를 생성해준다.
- getBean()또는 @Autowired나 <property>같은 DI 선언을 할 경우 매번 새로운 오브젝트 생성하여 DI or DL(의존관계 조회, getBean()메소드)
@Test
public void prototypeScope() {
ApplicationContext ac = new AnnotationConfigApplicationContext(
PrototypeBean.class, PrototypeClientBean.class);
Set<PrototypeBean> bean = new HashSet<PrototypeBean>();
//DL방식으로 빈을 요청했을 때 새로운 빈이 생성됨을 확인
bean.add(ac.getBean(PrototypeBean.class));
assertThat(bean.size(), is(1));
bean.add(ac.getBean(PrototypeBean.class));
assertThat(bean.size, is(2));
//DI방식으로 빈을 요청했을 때 새로운 빈이 생성됨을 확인
bean.add(ac.getBean(PrototypeClientBean.class).bean1);
assertThat(bean.size(), is(3));
bean.add(ac.getBean(PrototypeClientBean.class).bean2);
assertThat(bean.size(), is(4));
@Scope("prototype") //애노테이션을 이용해 프로토타입 빈으로 만들려면 @Scope의 기본 값을 prototype을 지정
static class PrototypeBean {}
static class PrototypeClientBean {
@Autowired PrototypeBean bean1;
@Autowired PrototypeBean bean2;
}
}
IoC의 기본 개념은 애플리케이션을 구성하는 핵심 오브젝트를 컨테이너가 관리한다는 것.
프로토타입 빈은 IoC의 기본 원칙을 따르지않음!
- 프로토타입 스코프를 갖는 빈은 요청이 있을 때마다 제공은 해주지만 그 후에 더이상 관리는 하지않음.
- DI, DL받은 빈이 컨테이너가 제공한 빈을 관리하는 형태
-- 따라서, 프로토타입 빈은 주입받은 오브젝트에 종속적일 수밖에 없음.
EX) 주입 받은 빈이 싱글톤인 경우, 프로토타입 빈도 싱글톤 주기를 따름
프로토타입 빈은 코드에서 new로 오브젝트를 생성하는 것을 대신하기 위해 사용
- 사용자의 요청에 따라 독립적인 오브젝트를 만들어야 하는데, 매번 새롭게 만들어지는 오브젝트가 컨테이너 내의 빈을 사용해야 하는 경우가 있음.
이때, 일일이 오브젝트에 DI를 적용할 것이 아니라면 프로토 타입 빈을 사용
=> 매번 새로운 오브젝트가 필요하면서 DI를 통해 다른 빈을 사용할 수 있어야 할 때 프로토 타입 빈을 사용
프로토타입 빈을 컨트롤러에서 가져오도록 getBean()메소드를 호출하는 DL방식
프로토타입 빈을 직접 DI하는건 안될까?
❌ 안된다.
웹 컨트롤러도 싱글톤이며, DI작업은 처음 만들어질 때, 딱 한 번만 진행됨
따라서, DI하기위해 컨테이너에 요청할 때, 단 한 번만 프로토타입 빈이 생성되고, 더이상 새로운 빈이 만들어지지 않음.
- 사용자가 여러 명일 경우 서로의 데이터를 덮어쓰는 치명적인 오류가 발생한다
스프링은 DL 방식을 코드에서 사용해야 할 경우를 위해 다양한 방법 제공
▪ ApplicationContext, BeanFactory
@Autowired나 @Resource를 이용해 ApplicationContext 또는 BeanFactory를 DI 받은 뒤에 getBean()메소드를 직접 호출해서 빈을 가져오는 방법
- 사용이 간단
- 코드에 스프링 API가 직접 등장한다는 단점
▪ ObjectFactory, ObjectFactoryCreatingFactoryBean
직접 애플리케이션 컨텍스트를 사용하지 않기위해 중간에 컨텍스트에 getBean()을 호출하는 팩토리를 두는 것
- ObjectFactory의 구현 클래스가 ObjectFactoryCreatingFactoryBean이다.
- 팩토리를 사용하면 오브젝트를 요구하면서도 오브젝트가 어떻게 생성되고 가져오는지 신경쓰지 않아도 됨.
//ObjectFactoryCreatingFactoryBean 등록
<bean id="serviceRequestFactory"
class="org.springframework.beans.factory.config.ObjectFactoryCreatingFactoryBean">
<property name="targetBeanName" value="serviceRequest" /> //value에 가져올 빈 이름 넣기
</bean>
@Resource //ObjectFactory타입은 여러 개 존재할 수 있으므로 이름으로 빈을 지정하는게 낫다
private ObjectFactory<ServiceRequest> serviceRequestFactory;
public void serviceRequestFormSubmit(HttpServletRequest request) {
ServiceRequest serviceRequest = this.serviceRequestFactory.getObject(); //가져오기
serviceRequest.setCustomerByCustomerNo(request.getParameter("custno"));
...
}
▪ ServiceLocatorFactoryBean
프레임워크의 인터페이스를 애플리케이션 코드에서 사용하고 싶지 않거나, 기존에 만들어둔 팩토리 인터페이스를 활용하고 싶을 경우 사용
- 스프링이 미리 정의해둔 인터페이스 대신 DL 방식으로 가져올 빈을 리턴하는 메소드가 정의된 인터페이스가 존재하면 됨
//ServiceRequest 팩토리 인터페이스
public interface ServiceRequestFactory {
ServiceRequest getServiceFactory();
}
//ServiceLocatorFactoryBean 빈 정의
<bean class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
<property name="serviceLocatorInterface" value=".. ServiceRequestFactory" /> // 팩토리 인터페이스를 지정
</bean>
@Autowired ServiceRequestFactory serviceRequestFactory;
public void serviceRequestFormSubmit(HttpServletRequest request) {
ServiceRequest serviceRequest =
this.serviceRequestFactory.getServiceFactory();
serviceRequest.setCustomerByCustomerNo(request.getParameter("custno"));
}
▪ 메소드 주입
ApplicationContext를 사용하는건 스프링 API에 의존적이게되고, ObjectFactory나 ServiceLocatorFactoryBean은 추가가 번거로움
=> 스프링은 메소드 주입 전략을 제공 (DI방식이 아님!!)
- 메소드 코드 자체를 주입하는 방법
- 추상 메소드를 만들어두고, getBean()으로 빈을 가져오는 기능을 담당하는 메소드를 런타임 시에 추가해주는 기술
- 컨테이너 없이 단위 테스트 가능
- 스프링 외의 환경에 가져다 사용 가능
- 클래스 자체가 추상 클래스이므로 테스트에서 사용 시 추상 메소드를 오버라이드 후 사용해야하는 번거로움 존재
abstract public ServiceRequest getServiceRequest();
public void serviceRequestFormSubmit(HttpServletRequest request) {
ServuceRequest serviceRequest = this.getServiceRequest(); //추상메소드 호출
serviceRequest.setCustomerByCustomerNo(request.getParameter("custno"));
...
}
//메소드 주입 빈 설정
<bean id="serviceRequestController" class="...ServiceRequestController">
<lookup-method name="getServiceRequest" bean="serviceRequest" /> //name: 추상메소드 이름 / bean : 가져올 빈의 이름
</bean>
▪ Provider<T>
가장 최근에 소개된 방법. 방식은 ObjectFactory와 유사하다.
차이점은 ObjectFactoryCreatingFactoryBean을 등록하지 않고도 사용 가능
- Provider 인터페이스를 @Inject, @Autowired, @Resource중 하나를 이용해 DI 되도록 지정해주기만 하면 스프링이 자동으로 오브젝트를 생성해서 주입해줌.
//Provider 오브젝트 팩토리 주입
@Inject Provider<ServiceRequest> serviceRequestProvider;
public void serviceRequestFormSubmit(HttpServletRequest request) {
ServiceRequest serviceRequest = this.serviceRequestProvider.get();
serviceRequest.setCustomerByCustomerNo(request.getParameter("custno"));
...
}
▪ 요청 스코프
하나의 웹 요청 안에서 만들어지고 해당 요청이 끝날 때 제거된다.
- 각 요청별로 독립적인 빈을 생성하므로 빈 오브젝트 내에 상태 값을 저장해도 안전
- 애플리케이션 코드에서 생생한 정보를 프레임워크 레벨의 서비스나 인터셉터 등에 전달하는데에 사용
- 프레임워크 레벨의 서비스나 인터셉터 등의 생생한 정보를 애플리케이션 코드에서 이용하는데에 사용
▪ 세션 스코프, 글로벌세션 스코프
HTTP 세션과 같은 존재 범위를 갖는다.
HTTP 세션을 직접 이용하는 것은 번거로움. 따라서, 해당 스코프의 빈을 사용
- HTTP세션에 저장되는 정보를 모든 계층에서 안전하게 이용 가능
- HTTP 세션은 사용자별로 만들어지기에 중복의 위험도 없음
▪ 애플리케이션 스코프
서블릿 컨텍스트에 저장되는 빈 오브젝트.
- 서블릿 컨텍스트에 있으므로 싱글톤과 비슷한 존재 범위를 가짐.
- 드물지만, 웹 애플리케이션과 애플리케이션 컨텍스트의 존재 범위가 다른 경우가 존재 - 이때, 애플리케이션 스코프 범위를 사용
스코프 빈은 프로토타입과 마찬가지로 하나 이상의 빈이 만들어짐
- 따라서 싱글톤에 DI할 수 없음. (싱글톤은 생성될때 단 한 번만 DI함)
- 스프링이 제공하는 특별한 DI방법을 사용하면 DI할 수 있다.
(프록시를 DI해 주는 것)
일반적인 세션 스코프 빈 사용하기 (DL 방식)
@Scope("session")
public class LoginUser {
String loginId;
String name;
Date loginTime;
...
}
public class LoginService {
@Autowired Provider<LoginUser> loginUserProvider; //Provider로 DI받기
public void login(Login login) {
//로그인 처리
LoginUser loginUser = loginUserProvider.get(); //같은 사용자의 세션에서는 같은 오브젝트를 가져옴
loginUser.setLoginId(...);
loginUser.setName(...);
loginUser.setLoginTime(new Date());
...
}
}
프록시 DI 방식
@Scope("session", proxyMode=ScopedProxyMode.TARGET_CLASS)
public class LoginUser {
String loginId;
String name;
Date loginTime;
...
}
public class LoginService {
@Autowired LoginUser loginUser; //Provider로 DI받기
public void login(Login login) {
//로그인 처리
this.loginUser.setLoginId(...); //세션마다 다른 오브젝트의 메소드가 호출됨
this.loginUser.setName(...);
this.loginUser.setLoginTime(new Date());
...
}
}
- 스코프 빈이지만 마치 싱글톤 빈을 사용하듯이 편하게 사용가능
- 빈의 스코프를 모르면 코드를 이해하기가 힘들 수도 있음
스프링에서는 Scope 인터페이스를 구현해서 새로운 스코프를 작성할 수 있음
- 웹 요청 스코프보다는 길고 세션 스코프보다는 짧은 범위의 스코프 생성가능
- 기본적으로 스코프를 사용한다는건 상태를 저장해두는 용도로 사용하기 위함!
(상태를 저장하고싶지 않다면, 싱글톤 빈을 사용하고 유지해야할 정보는 필드나 세션에 분산시켜 저장해두고 코드로 관리해야함)
- 스프링 웹 플로우나 제이보스 씸(Seam)과 같은 프레임워크를 이용
▪ XML 설정에서의 빈 식별자와 별칭
빈 아이디와 빈 이름이라는 두 가지 표현을 사용했었음!
두 가지 모두 특정 빈을 구분하기위해 사용되는 빈 식별자(identifier)
XML이라면 <bean>태그의 id와 name 애트리뷰트를 이용해 정의 가능
- id
- XML의 ID 타입의 기준을 지켜야 한다. 따라서 다음과 같은 작성 규칙을 따라야함
- id에는 공백이 들어갈 수 없다
- 첫 글자는 알파벳과 밑줄(_)그리고 허용된 일부 언어문자만 사용 가능
- 나머지 글자는 알파벳과 밑줄, 숫자, 점(.)을 허용. 특수문자는 사용 불가
Ex) "_hello.Service123", "사용자서비스", "userService"- id는 생략이 가능! - 스프링 컨테이너가 자동으로 빈의 아이디를 부여
- name
- id와 달리 특별한 제약이 없음
- 한번에 여러 개의 이름을 지정할 수 있음
Ex) "1234,/hello;헬로우" - (,)나 (;)로 각 이름을 구분.
- 같은 빈이지만 다른 이름으로 참조하면 편리할 때가 존재
Ex) 새로 구성한 빈들과 이름을 짓는 관례가 일치하지 않을 경우 여러 개의 이름을 부여해 양쪽에서 각기 다른 이름으로 참조하도록 함
@Component와 같은 스테레오타입 에노테이션을 부여해준 경우
- 보통 클래스이름을 그대로 빈 이름으로 이용한다.
Ex) UserService 클래스 -> userService 빈- 빈 이름을 직접 지정할 수도 있다
Ex) @Component("myUserService"), @Named("myUserService")- 자바 코드를 이용한 빈 등록 방식에서는 @Bean의 name 엘리먼트 사용
Ex) @Bean(name="myUserDao")
- 하나 이상의 빈 이름을 지정할 수도 있다.
Ex) @Bean(name={"myUserDao", "userDao"})
빈 오브젝트가 생성되고 DI 작업까지 마친 다음에 실행되는 메소드.
- DI를 통해 모든 프로퍼티가 주입된 후에야 가능한 초기화 작업이 있는 경우 사용
▪ 초기화 콜백 인터페이스
InitializingBean 인터페이스를 구현해서 빈을 작성하는 방법
별로 권장되지 않는 방법.
- 애플리케이션 빈 코드에 스프링 인터페이스를 노출시킴
- 다른 방법이 더 간결하기 때문
▪ init-method 지정
XML을 이용해 빈을 등록한다면 <bean> 태그에 init-method 애트리뷰트를 넣어 초기화 작업을 수행할 메소드 이름 지정 가능
<bean id="mybean" class="MyBean" init-method="initResource" />
- 빈 클래스에 스프링 API가 노출되지않아 코드가 깔끔함
- 코드를 이해하기에 불편할 수 있음
- XML 설정에서 init-method를 빼먹을 위험 존재
▪ @PostConstruct
Vol.1 7장에서 사용. 초기화를 담당할 메소드에 @PostConstruct 애노테이션 부여
- @PostConstruct는 자바 표준 공통 애노테이션임
- 스프링 콜백 인터페이스를 사용하는 것보다 부담이 적음
- 코드에서 초기화 메소드가 존재하다는 사실을 쉽게 파악 가능
=> 가장 사용이 권장되는 방식!
▪ @Bean(init-method)
@Bean을 이용해 빈을 정의하는 경우, @Bean 애노테이션의 init-method 엘리먼트를 사용해 초기화 메소드 지정 가능.
@Bean(init-method="initResource") public void MyBean myBean() {
▪ 제거 콜백 인터페이스
DisposableBean 인터페이스를 구현해서 destory()를 구현하는 방법.
스프링 API에 종속되는 코드를 만드는 단점 존재
▪ destroy-method
<bean> 태그에 destroy-method를 넣어서 제거 메소드 지정 가능
▪ @PreDestory
컨테이너가 종료될 때 실행될 메소드에 @PreDestory를 붙여주면 된다.
▪ @Bean(destoryMethod)
@Bean 애노테이션의 destoryMethod 엘리먼트를 이용해 제거 메소드 지정 가능
▪ FactoryBean 인터페이스
Vol.1에서 JDK 다이나믹 프록시를 빈으로 등록하기 위해 사용했었음
가장 단순하고 자주 사용되는 방법.
기술 서비스 빈이나 기반 서비스 빈을 활용할 때 주로 사용됨
▪ 스태틱 팩토리 메소드
클래스의 스태틱 메소드를 호출해서 인스턴스를 생성하는 방식
JDK를 비롯한 다양한 기술 API에서 자주 사용
<bean> 태그에서 사용 가능한 factory-method 애트리뷰트를 이용하는 것이 편리
▪ 인스턴스 팩토리 메소드
오브젝트의 인스턴스 메소드를 이용해 빈 오브젝트를 생성할 수도 있다
factory-bean, factory-method를 함께 사용
▪ @Bean 메소드
특정 빈만 팩토리 메소드를 통해 만들고 싶을 때, 일반 빈 클래스에 @Bean 메소드를 추가
- 아예 자바 코드에 의해 빈의 설정과 DI를 대폭 적용한다면 @Configuration이 붙은 설정 전용 클래스를 사용하는 것이 편리
스프링 3.1에 도입된 IoC/DI 기술은 다음 두 가지이다.
- 강화된 자바 코드 빈 설정
- 런타임 환경 추상화
가장 큰 특징은 자바 코드를 이용한 설정 메타정보 작성이 쉽다는 점
- XML을 사용하지 않거나 최소화한 채로 스프링 애플리케이션 개발이 가능
▪ 빈의 종류
애플리케이션 로직 빈
스프링 IoC/DI 컨테이너에 의해 생성되고 관리되는 오브젝트.
- 일반적으로 애플리케이션의 로직을 담고 있는 주요 클래스의 오브젝트
Ex) DAO, 비즈니스 로직과 기반 서비스를 다루는 서비스 오브젝트
애플리케이션 인프라 빈
스프링 컨테이너에 등록되는 빈이기는 하지만 애플리케이션의 로직을 직접 담당하는 것이 아닌 지원하는 빈
Ex) DataSource, TransactionManager
컨테이너 인프라 빈
스프링 컨테이너의 기능을 확장해서 빈의 등록, 생성, 관계설정, 초기화 등의 작업에 참여하는 빈
Ex) DefaultAdvisorAutoProxyCreator
컨테이너 인프라 빈은 이름이 긴 경우가 존재. 한 번에 여러 개의 빈을 동시에 설정해야 하는 경우도 많음
- 스프링은 이런 빈을 전용 태그를 사용해 간접적으로 등록하는 방법 권장
자동인식을 이용한 빈 등록을 사용하려면 XML에 <context:component-scan> 태그를 추가해야 한다.
//빈 스캐너로 등록되는 클래스
@Configuration
public class SimpleConfig {
@Autowired Hello hello;
@Bean Hello hello() {
return new Hello();
}
}
public class Hello {
@PostConstruct
public void init() {
System.out.println("init");
}
public void sayHello() { ... }
}
<beans ... >
<bean class="springbook.learningtest.spring31.ioc.SimpleConfig" />
</beans>
위의 코드를 검증해보자
ApplicationContext context = new GenericXmlApplicationContext(BeanRoleTest.class,
"beanrole.xml");
SimpleConfig sc = context.getBean(SimpleConfig.class);
sc.hello.sayHello();
SimpleConfig와 Hello클래스 둘다 빈으로 만들어져서 두 개의 빈이 생길 것 같음 ❌
위에 코드를 실행하면 SimpleConfig만 빈으로 등록되어 마지막 줄에서 NullPointerException이 발생.
- 스프링 IoC/DI 컨테이너에는 @Configuration/@Bean을 이용해 새로운 빈을 등록해주는 기능이 없기 때문!
=> 컨테이너 인프라 빈이 제공하는 기능
XML에 다음 한 줄을 추가해주면 정상적으로 동작
<context:annotation-config />
❗ 컨테이너 인프라 빈이 애노테이션의 역할을 대신하는 것? ❌
태그를 처리하는 과정에서 등록되는 빈이 스프링 컨테이너를 확장해서 컨테이너가 새로운 기능을 부여받는 것
스프링의 빈을 역할에 따라 구분한다면 세 가지로 나눌 수 있음
- 애플리케이션이 동작하는 중에 사용되는 빈(역할 값 : 0)
- 애플리케이션 인프라 빈, 애플리케이션 로직 빈- 복합 구조의 빈을 정의할 때 보조적으로 사용되는 빈(역할 값 : 1)
- 실제로는 거의 사용되지 않음- <context:annotation-config>같은 전용 태그에 의해 등록되는 컨테이너 인프라 빈(역할 값 : 2)
⁕ 3.1버전부터 @Role 이라는 애노테이션이 도입됨
- 빈을 정의할 때 역할 값을 직접 지정할 수 있음
스프링 IoC/DI 설정 방법의 역사를 잠깐 살펴보자
▪ 스프링 1.x
XML을 통한 빈 등록 방법을 주로 사용
<bean> 태그만 사용이 가능했다.
▪ 스프링 2.0
스키마와 네임스페이스를 가진 전용 태그를 제공.
▪ 스프링 2.5
빈 스캐너와 스테레오타입 애노테이션을 이용한 빈 자동등록 방식과 애노테이션 기반의 의존관계 설정 방법이 등장
- 하지만 애노테이션을 메타정보로 활용하는 빈 등록과 관계 설정 방식은 외부에서 작성된 클래스를 대상으로 적용할 수 없음.
▪ 스프링 3.0
자바 코드를 이용해 빈 설정정보 또는 빈 설정 코드를 만드는 일이 가능해짐
코드를 이용해 직접 빈 오브젝트를 생성하고 프로퍼티 지정 가능
- 애노테이션만으로도 애플리케이션 빈 설정이 가능해진 버전
하지만, 컨테이너 인프라 빈 등록에는 XML 전용 태그가 필요함.
▪ 스프링 3.1
컨테이너 인프라 빈도 자바코드로 등록이 가능해짐
- 모든 종류의 빈 설정 방식을 XML과 자바 코드 중에서 자유롭게 선택이 가능해짐
▪ @ComponentScan
@Configuration이 붙은 클래스에 사용 시 XML에서 <context:component-scan>와 같은 기능을 수행
@Configuration @ComponentScan("springbook.springtest.spring31.ioc.scanner") public class AppConfig { }
패키지 이름 대신 마커 클래스나 인터페이스를 사용하는 방법도 존재
- 코드가 간결해지고, 오타 발생 시 컴파일 과정에서 잡아줌
//마커 인터페이스
public interface ServiceMarker {
}
//마커 인터페이스 사용
@Configuration
@ComponentScan(basePackageClasses=ServiceMarker.class)
public class AppConfig {
}
스캔할 패키지를 지정할 때, 일부 클래스를 스캔 대상에서 제외하고 싶을 수 있음
- excludes 엘리먼트를 사용
@Configuration
@ComponentScan(basePackageClasses="myproject",
excludeFilters=@Filter(Configuration.class) // @Configuration이 붙은 클래스만은 제외한다
)
public class AppConfig {
}
//애노테이션이 아닌 특정클래스만을 제외하고 싶은 경우
@Configuration
@ComponentScan(basePackageClasses="myproject",
excludeFilters=@Filter(type=FilterType.ASSIGNABLE_TYPE, value=AppConfig.class) // AppConfig 클래스만은 제외한다
)
public class AppConfig {
}
▪ @Import
다른 @Configuration 클래스를 빈 메타정보에 추가할 때 사용
@Configuration
@Import(DataConfig.class) //DataConfig 클래스의 빈 메타정보를 추가
public class AppConfig {
}
@Configuration
public class DataConfig {
}
▪ @ImportResource
XML 파일의 빈 설정을 가져올 수 있음
- XML파일의 위치를 지정해주면됨
@configuration
@ImportResource("/myproject/config/security.xml") // XML파일 위치를 지정
public class AppConfig {
}
▪ @EnableTransactionManagement
@Configuration 클래스에 사용할 수 있는 애노테이션.
Vol.1 6장에서 살펴본 <tx:annotation-driven>과 같은 기능을 수행한다.
- 트랜잭션 속성을 지정할 수 있게 해주는 AOP 관련 빈을 등록해주는 기능
스프링 3.1에서는 자바 코드 설정 메타정보만을 사용하거나
혹은 자바 코드 설정을 주로 하고 XML을 보조적으로 사용가능 - 이 방법 사용
<listener>를 등록해주고 applicationContext.xml 파일 대신 @Configuration이 붙은 AppConfig 클래스를 빈 설정 메타정보로 해서 루트 애플리케이션 컨텍스트가 생성되도록한다.
//listener 등록
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
//AnnotationConfigWebApplicatioinContext를 사용하도록 변경
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
//@Configuration 클래스(AppConfig)는 contextConfigLocation 컨텍스트 파라미터로 지정
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>myproject.config.AppConfig</param-value>
</context-param>
서블릿 컨텍스트도 동일하게 변경하면 된다
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.framework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>myproject.config.WebConfig</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
▪ 빈 설정파일의 변경
메타정보를 담은 XML이나 클래스를 따로 준비해두고 환경이 변경될때마다 다른 설정파일을 사용하게 만든다.
- 내용이 다른 여러 벌의 설정 메타정보를 관리하는 것은 번거롭고 위험하다.
- 엉뚱한 정보가 실수로 들어가게되면 치명적인 사고로 이어질 수 있음
▪ 프로퍼티 파일 활용
환경에 따라 달라지는 정보를 담은 프로퍼티 파일을 활용하는 방법
메타정보를 담은 XML이나 @Configuration 클래스는 애플리케이션 로직이 바뀌지 않는다면 건드리지 않고, 환경에 따라 달라지는 외부 정보만 프로퍼티 파일 등에 두고 XML에서 읽어서 사용
- 실제 속성에 넣을 값들은 매번 읽어서 지정하도록 프로퍼티 치환자를 사용해 정의
❗ 하지만 위 방법으로 감당할 수 없는 상황이 존재함
- 환경에 따라 아예 빈 클래스가 바뀌거나 빈 구성이 달라지는 경우이다.
스프링 3.1을 사용하면 환경에 따라 빈 설정정보가 달라지는 문제를 해결 가능
- 런타임 환경 추상화를 이용
컨텍스트 내부에 Environment 인터페이스를 구현한 런타임 환경 오브젝트가 만들어져서 빈을 생성하거나 의존관계를 주입할 때 사용
런타임 환경
프로파일과 프로퍼티 소스로 구성
- 환경에 따라 프로파일과 프로퍼티 소스가 다르게 설정된 Environment 오브젝트가 사용되는 식
- 프로파일 : 환경에 따라 다르게 구성되는 빈들을 다른 이름을 가진 프로파일 안에 정의해두고 지정된 프로파일에 속한 빈들만 생성되도록 함
- XML과 자바 클래스를 이용한 설정에 모두 적용 가능
//예시
<beans profile="spring-test"> //조건에 따라 사용할지 아닌지 결정 - 테스트 환경
<jdbc:embedded-databse id="dataSource" type="HSQL">
<jdbc:script location="schema.sql" />
</jdbc:embedded-databse>
</beans>
<beans profile="dev"> //조건에 따라 사용할지 아닌지 결정 - 개발환경
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${db.driverClass}" />
<property name="url" value="${db.url}" />
<property name="username" value="${db.username}" />
<property name="password" value="${db.password}" />
</bean>
<context:property-placeholder location="database.properties" />
</beans>
<beans profile="production"> //조건에 따라 사용할지 아닌지 결정 - 운영환경
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/DefaultDS" /> //WAS(JNDI)로부터 DataSource를 가져옴
</beans>
특정 프로파일에 정의된 빈을 사용하고 싶으면 해당 프로파일을 활성 프로파일로 만들어주면 된다.
- setActiveProfiles()메소드에 사용할 프로퍼티 이름을 넣어주면 된다.
- 프로파일은 XML이 로딩되거나 refresh()메소드가 컨텍스트에서 실행되기 전에 지정해주어야 한다.
GenericXmlApplicationContext ac = new GenericXmlApplicationContext();
ac.getEnvironment().setActiveProfiles("dev"); //dev 프로파일 활성화
ac.load(getClass(), "applicationContext.xml");
ac.refresh();
만약 하나의 WAS에 여러 개의 스프링 애플리케이션이 올라가는데 각기 다른 프로파일을 지정하고 싶다면 JVM 레벨의 프로퍼티 대신 서블릿 컨텍스트 레벨이나 서블릿 레벨의 설정이 필요
// 파라미터로 활성 프로파일 지정 시 루트 애플리케이션 컨텍스트와 서블릿 컨텍스트에 모두 적용 <context-param> <param-name>spring.profiles.active</param-name> <param-value>dev</param-value> </context-param> //서블릿 컨텍스트에만 활성 프로파일을 지정하려면 <servlet> 안에 <init-param> 사용
프로파일은 한 번에 두 가지 이상을 활성화할 수도 있다.
EX) DB연결과 관련된 빈은 dsDev, dsTest, dsProduction의 세 가지 프로파일로 구분하고, 메일 서버 접속과 관련된 빈은 mailServer와 mockMailServer 프로파일로 구분했다면, 환경에 따라 어떤 빈 프로파일을 사용할지 조합해서 결정 가능
<context-param>
<param-name>spring.profiles.active</param-name>
<param-value>dsdev, mockMailServer</param-value> //개발환경DB와 목 오브젝트 사용
</context-param>
❗ 성격이 같은 두 개의 프로파일이 동시에 활성화되서는 안된다.
▪ 자바 클래스에서 프로파일 지정하기
@Configuration 클래스에 @Profile 애노테이션을 사용해 지정가능
@Configuration
@Profile("dev") //dev 프로파일 활성화
public class DevConfig {
...
}
프로파일을 사용하는 경우에도 외부 리소스에 따라 바뀔 수 있는 DB연결정보는 메타정보 외부로 독립시킬 필요가 있음
키와 그에 대응되는 값의 쌍
- XML에서 <property>태그를 사용해 만든 것
- name(키)과 value(값) 애트리뷰트를 이용해 프로퍼티 정보 표현
스프링은 프로퍼티 파일 외에도 프로퍼티 값을 지정하고 가져오는 다양한 방법 지원
▪ 환경변수
스프링 애플리케이션이 구동되는 OS의 환경변수도 프로퍼티
- System.getEnv() 메소드로 환경변수를 담아서 프로퍼티 맵으로 가져올 수 있음
- 같은 시스템에서 여러 개의 WAS를 구동하더라도 동일하게 적용되는, 매우 넓은 범위에 적용되는 프로퍼티
▪ 시스템 프로퍼티
JVM 레벨에 정의된 프로퍼티
JVM이 시작될 때 시스템 관련 정보부터 자바 관련 정보, 기타 JVM 관련 정보 등이 시스템 프로퍼티로 등록 ( -D로 지정한 커맨드라인 옵션도 포함)
- System.getProperties()로 가져올 수 있음
▪ JNDI
WAS에 여러 개의 웹 애플리케이션이 올라가고 그 중 하나의 애플리케이션에만 프로퍼티를 지정하고 싶을 때 JNDI 프로퍼티 또는 JNDI 환경 값을 사용하는 방법 고려
▪ 서블릿 컨텍스트 파라미터
서버에서 웹 애플리케이션 범위의 JNDI 값을 설정하기가 번거롭다면 web.xml에 서블릿 컨텍스트 초기 파라미터를 프로퍼티로 사용 가능
//프로퍼티 설정 <context-param> <param-name>db.username</param-name> <param-value>spring</param-value> </context-param>
- 프로퍼티 값 사용 방법
- ServletContext 오브젝트를 직접 빈에서 주입받은 뒤, ServletContext를 통해 컨텍스트 파라미터를 가져오는 방법
- ServletContextAware이나 @Autowired 사용- ServletContextPropertyPlaceholderConfigurer를 사용하는 것
- PropertyPlaceholderConfigurer 서블릿 컨텍스트 파라미터 버전
▪ 서블릿 컨픽 파라미터
개별 서블릿을 위한 설정 - 서블릿 컨텍스트가 컨픽보다 범위가 넓음
<servlet> <servlet-name>smart</servlet-name> <servlet-class>org.framework.web.servlet.DispatcherServlet </servlet-class> ... <init-param> <param-name>temp.folder</param-name> <param-value>/tmp</param-value> </init-param> ... </servlet>
- 접근 방법은 서블릿 컨텍스트 파라미터와 유사함
다양한 프로퍼티 종류와 그에 따라 달라지는 접근 방법을 스프링 3.1에서는 프로퍼티 소스라는 개념으로 추상화하고, 프로퍼티의 저장 위치에 상관없이 동일한 API를 이용해 가져올 수 있게 해줌
- 프로퍼티 소스는 프로파일과 함께 런타임 환경정보를 구성하는 핵심 정보
StandardEnvironment는 GenericXmlApplicationContext나 AnnotationConfigApplicationContext처럼 독립형 애플리케이션용 컨텍스트에서 사용되는 런타임 환경 오브젝트.
기본적으로 다음 두 가지 종류의 프로퍼티 소스를 제공
- 시스템 프로퍼티 소스
- 환경변수 프로퍼티 소스
- 두 가지 종류의 프로퍼티 소스로부터 프로퍼티를 찾고 싶으면 getProperty()사용
두 개 이상의 프로퍼티 소스를 갖고 있을 때 양쪽에 동일한 키의 프로퍼티가 중복해서 존재한다면 어떻게 될까?
- 우선순위가 높은 쪽의 프로퍼티 소스의 프로퍼티가 적용됨
- addFirst() : 현재 등록된 프로퍼티 소스보다 우선순위가 높게 지정
- addLast() : 가장 낮은 우선순위로 지정
- addBefore(), addAfter() : 특정 프로퍼티 소스를 기준으로 우선순위 지정
▪ Environment.getProperty()
Environment 오브젝트를 빈에 주입받아서 직접 프로퍼티 값을 가져옴
@Autowired를 이용하여 현재 컨텍스트에서 만들어진 환경 오브젝트 사용@Autowired Environment env;
해당 빈에서 반복적으로 사용되는 프로퍼티라면 @PostConstruct 메소드를 이용해 클래스 멤버 필드에 미리 프로퍼티 값을 저장
private String adminEmail;
@PostConstruct
public void init() {
this.adminEmail = env.getProperty("admin.email"); // 프로퍼티 값 저장
}
빈을 만들 때 필요한 프로퍼티라면 생성된 빈 오브젝트의 프로퍼티에 직접 주입
@Bean
public DataSource dataSource() {
BasicDataSource ds = new BasicDataSource(); // 빈 생성
ds.setUsername(env.getProperty("db.username")); // 빈의 프로퍼티에 주입
...
}
▪ PropertySourceConfigurerPlaceholder와 <context:property-placeholder>
필드에 프로퍼티 값을 넣고 싶은데 @PostConstruct를 사용하는 것이 번거롭다면 @Value와 ${}치환자를 사용할 수 있음
@Value("${db.username}") private String username;
치환자를 사용하기 위해서는 컨텍스트에 PropertySourcePlaceholderConfigurer빈이 등록되어 있어야 한다.
@Bean
public static PropertySourcePlaceholderConfigurer pspc() { //반드시 스태틱메소드로 등록
return new PropertySourcesPlaceholderConfigurer();
}
스태틱 메소드로 만들어야하는 이유?
BeanFactoryPostProcessor 후처리기를 @Bean과 PropertySourcesPlaceholderConfigurer 둘다 사용하기 때문에 다른 후처리기로 @Bean 메소드가 있는 클래스의 빈 설정을 가공하도록 만들 수가 없음
-@Bean 메소드를 처리할 때 후처리기를 사용하는데, PropertySourcesPlaceholderConfigurer또한 같은 후처리기를 사용하기 때문에 가공이 불가능. 그래서 해당 빈은 후처리기 생성 전에 미리 만들어서 사용
프로퍼티 파일 또한 프로퍼티 소스로 등록하고 사용 가능
- @PropertySource 사용@Configuration @PropertySource("database.properties") public class AppConfig {
프로퍼티 파일을 여러 개 동시에 지정 가능하며 이름도 넣을 수 있음
@PropertySource(name="myPropertySource", value={"database.properties", "setting.xml"})
❗ @PropertySource로 등록되는 프로퍼티 소스는 컨텍스트에 기본적으로 등록되는 프로퍼티 소스보다 우선순위가 낮음
웹 애플리케이션 컨텍스트는 StandardServletEnvironment 타입의 런타임 환경 오브젝트를 사용
- StandardServletEnvironment는 StandardEnvironment가 등록해주는 환경변수 프로퍼티 소스와 시스템 프로퍼티 소스에 더해서 JNDI 프로퍼티 소스, 서블릿 컨텍스트 프로퍼티 소스, 서블릿 컨픽 프로퍼티 소스를 추가로 등록
프로퍼티 소스의 우선순위
서블릿 컨픽 프로퍼티 > 서블릿 컨텍스트 프로퍼티 > JNDI 프로퍼티 > 시스템 프로퍼티 > 환경변수 프로퍼티
코드를 이용해 프로퍼티 소스를 추가하려면 어떻게 해야 할까?
웹 환경에서는 리스너나 서블릿에서 컨텍스트가 자동으로 생성
- 여기에 스프링 3.1에서 새롭게 추가된 애플리케이션 컨텍스트 초기화 오브젝트를 사용해 프로퍼티 소스를 추가
컨텍스트 초기화 오브젝트 만들기
//ApplicationContextInitializer 인터페이스
public interface ApplicationContextInitializer<C extends
ConfigurableAppllicationContext> {
void initialize(C applicationContext);
}
//ApplicationContextInitializer 인터페이스 구현
//컨텍스트가 생성된 후에 초기화 작업을 진행하는 오브젝트를 만들 때 사용
public class MyContextInitializer implements
ApplicationContextInitializer<AnnotationConfigWebApplicationContext> {
@Override
public void initialize(AnnotationConfigWebApplicationContext ac) {
ConfigurableEnvironment ce = ac.getEnvironment();
Map<String, Object> m = new HashMap<>();
m.put("db.username", "spring");
ce.getPropertySources().addFirst(new MapPropertySource("myPS", m));
}
}
컨텍스트 초기화 오브젝트는 contextInitializerClasses 컨텍스트 파라미터로 지정
- 루트 컨텍스트라면 <context-param> 이용
<context-param>
<param-name>contextInitializerClasses</param-name>
<param-value>MyContextInitializer</param-value>
</context-param>
- 서블릿 컨텍스트라면 <init-param> 이용
<init-param>
<param-name>contextInitializerClasses</param-name>
<param-value>MyContextInitializer</param-value>
</init-param>
❗ 프로퍼티 초기화 오브젝트는 코드를 이용한 작업이 꼭 필요한 프로퍼티 소스 등록 같은 작업에만 사용하도록 하자!