[Spring] 토비의 스프링 Vol.2 1장 IoC컨테이너와 DI (하)

Shiba·2023년 10월 20일
0

🍀 스프링 정리

목록 보기
13/21
post-thumbnail

📗 IoC컨테이너와 DI (하)

📖 프로토타입과 스코프

때로는 빈을 싱글톤이 아닌 다른 방법으로 만들어 사용해야 할 때가 있다.
- 싱글톤이 아닌 빈으로는 크게 프로토타입 빈, 스코프 빈이 존재

📝 프로토타입 스코프

싱글톤과 달리 빈을 요청할 때마다 새로운 오브젝트를 생성해준다.
- 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를 통해 다른 빈을 사용할 수 있어야 할 때 프로토 타입 빈을 사용

◼ DI와 DL

프로토타입 빈을 컨트롤러에서 가져오도록 getBean()메소드를 호출하는 DL방식

프로토타입 빈을 직접 DI하는건 안될까?
❌ 안된다.

웹 컨트롤러도 싱글톤이며, DI작업은 처음 만들어질 때, 딱 한 번만 진행
따라서, DI하기위해 컨테이너에 요청할 때, 단 한 번만 프로토타입 빈이 생성되고, 더이상 새로운 빈이 만들어지지 않음.
- 사용자가 여러 명일 경우 서로의 데이터를 덮어쓰는 치명적인 오류가 발생한다

◼ 프로토타입 빈의 DL 전략

스프링은 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

스프링 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 설정 방법의 발전

스프링 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 관련 빈을 등록해주는 기능


📝 웹 애플리케이션의 새로운 IoC 컨테이너 구성

스프링 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 후처리기@BeanPropertySourcesPlaceholderConfigurer 둘다 사용하기 때문에 다른 후처리기로 @Bean 메소드가 있는 클래스의 빈 설정을 가공하도록 만들 수가 없음
-@Bean 메소드를 처리할 때 후처리기를 사용하는데, PropertySourcesPlaceholderConfigurer또한 같은 후처리기를 사용하기 때문에 가공이 불가능. 그래서 해당 빈은 후처리기 생성 전에 미리 만들어서 사용

◼ @PropertySource와 프로퍼티 파일

프로퍼티 파일 또한 프로퍼티 소스로 등록하고 사용 가능
- @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>

프로퍼티 초기화 오브젝트는 코드를 이용한 작업이 꼭 필요한 프로퍼티 소스 등록 같은 작업에만 사용하도록 하자!

profile
모르는 것 정리하기

0개의 댓글