[Info]Tags categorized posts and contents patterns..

[AJAX] Ajax Code E xamples.. [Book] About the book.. [CSS] CSS Code E xamples.. [DB] Sql Code E xamples.. [DEV] All development stor...

2016년 4월 19일 화요일

[JAVA]4장 IoC 컨테이너 #11..

출처 : Outsider's Dev Story https://blog.outsider.ne.kr/

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.

4.11 JSR 330 표준 어노테이션 사용
스프링 3.0부터는 JSR-330 표준 어노테이션 (의존성 주입)을 지원한다. 이러한 어노테이션들은 스프링 어노테이션과 같은 방법으로 스캔한다. 클래스패스에 적절한 jar를 두기만 하면 된다.



Note
메이븐을 사용한다면 표준 메이븐 저장소(http://repo1.maven.org/maven2/javax/inject/javax.inject/1/)에서 javax.inject artifact를 사용할 수 있다. pom.xml 파일에 다음 의존성을 추가할 수 있다.

1
2
3
4
5
6
7
Xml

<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>


4.11.1 @Inject와 @Named를 사용한 의존성 주입
@Autowired 대신 @javax.inject.Inject 를 다음처럼 사용할 수 있다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Java

import javax.inject.Inject;

public class SimpleMovieLister {

  private MovieFinder movieFinder;

  @Inject
  public void setMovieFinder(MovieFinder movieFinder) {
      this.movieFinder = movieFinder;
  }
  // ...
}

@Autowired를 사용하듯이 클래스레벨, 필드레벨, 메서드레벨, 생성자 아규먼트레벨에서 @Inject를 사용하는 것이 가능하다. 주입되어야 하는 의존성에 검증된 이름을 사용하기를 좋아한다면 다음과 같이 @Named 어노테이션을 사용할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Java

import javax.inject.Inject;
import javax.inject.Named;

public class SimpleMovieLister {

  private MovieFinder movieFinder;

  @Inject
  public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
      this.movieFinder = movieFinder;
  }
  // ...
}

4.11.2 @Named: @Component 어노테이션과 동일한 표준
@Component 대신에 @javax.inject.Named를 다음과 같이 사용할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Java

import javax.inject.Inject;
import javax.inject.Named;

@Named("movieListener")
public class SimpleMovieLister {

  private MovieFinder movieFinder;

  @Inject
  public void setMovieFinder(MovieFinder movieFinder) {
     this.movieFinder = movieFinder;
  }
  // ...
}

컴포넌트에 이름을 지정하지 않고 @Component를 사용하는 것은 아주 일반적이다. 동일한 방법으로 @Named를 사용할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Java

import javax.inject.Inject;
import javax.inject.Named;
  
@Named
public class SimpleMovieLister {

  private MovieFinder movieFinder;

  @Inject
  public void setMovieFinder(MovieFinder movieFinder) {
      this.movieFinder = movieFinder;
  }
  // ...
}

@Named를 사용하면 스프링 어노테이션을 사용할 때와 완전히 같은 방법으로 컴포넌트 스캔을 사용할 수 있다.

1
2
3
4
5
Xml

<beans>
    <context:component-scan base-package="org.example"/>
</beans>

4.11.3 표준 접근의 한계
표준 어노테이션들을 사용해서 작업할 때 다음 표에 나온 것처럼 몇가지 중요한 기능들을 사용할 수 있다는 것을 꼭 알아야 한다.

Table 4.6. 스프링 어노테이션 vs. 표준 어노테이션

Springjavax.inject.*javax.inject의 제약 / 설명
@Autowired@Inject@Inject에는 'required'속성이 없다
@Component@Named-
@Scope("singleton")@SingletonJSR-330의 기본 범위는 스프링의 prototype과 비슷하다. 하지만 스프링의 일반적인 기본값과 일관성을 유지하기 위해 스프링 컨테이너에서 선언된 JSR-330 빈은 기본적으로 singleton이다. The JSR-330 default scope is like Spring's prototype. singleton 대신 다은 범위를 사용하려면 스프링의 @Scope 어노테이션을 사용해야 한다.

javax.inject도 @Scope 어노테이션을 제공한다. 그렇기는 하지만 이 어노테이션은 자신만의 어노테이션을 생성할 때만 사용하도록 만들어졌다.
@Qualifier@Named-
@Value-동일한 것이 없다
@Required-동일한 것이 없다
@Lazy-동일한 것이 없다


















4.12 자바기반의 컨테이너 설정
4.12.1 기본 개념: @Configuration와 @Bean
스프링의 새로운 자바설정 지원의 핵심부분은 @Configuration 어노테이션이 붙은 클래스다. 이러한 클래스들은 스프링 IoC 컨테이너가 관리하는 객체의 인스턴스화, 설정, 초기화 로직을 정의하는 @Bean 어노테이션이 붙은 메서드들을 주로 이루어져 있다.

클래스에 @Configuration 어노테이션을 붙히는 것은 스프링 IoC 컨테이너가 해당 클래스를 빈 정의의 소스로 사용한다는 것을 나타낸다. 가장 간단한 @Configuration 클래스는 다음과 같을 것이다.


1
2
3
4
5
6
7
8
9
Java

@Configuration
public class AppConfig {
  @Bean
  public MyService myService() {
      return new MyServiceImpl();
  }
}

스프링 <beans/> XML에 익숙하다면 앞의 AppConfig 클래스는 다음과 같을 것이다.

1
2
3
4
5
Xml

<beans>
  <bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>

여기서 보듯이 @Bean 어노테이션은 <bean/> 요소와 같은 역할을 한다. @Bean 어노테이션은 이번 섹션 후반에 더 깊게 살펴볼 것이다. 하지만 우선 자바기반의 설정을 사용해서 스프링 컨테이서를 생성하는 여러가지 방법을 살펴보자.

4.12.2 AnnotationConfigApplicationContext를 사용하는 스프링 컨테이너 예제 살펴보기
이 섹션에서는 스프링 3.0의 새 기능인 AnnotationConfigApplicationContext를 설명한다. 이 다재다능한 ApplicationContext 구현체는 인풋으로 @Configuration 클래스뿐만 아니라 평범한 @Component 클래스와 JSR-330 메타데이터로 어노테이션이 붙은 클래스들도 받아들일 수 있다.

인풋으로 @Configuration클래스를 받았을 때 @Configuration 클래스 자체가 빈 정의로 등록되고 해당 클래스내의 선언된 모든 @Bean 메서드들도 빈 정의로 등록된다.

@Component와 JSR-330 클래스들이 제공되었을 때 이 클래스들은 빈 정의로 등록되고 해당 클래스내에서 필요한 곳에 @Autowired나 @Inject 같은 DI 메타데이터가 사용되었다고 가정한다.

4.12.2.1 간단한 구성
ClassPathXmlApplicationContext를 인스턴스화 할 때 인풋으로 스프링 XML 파일을 사용하는 방법과 거의 동일하게 AnnotationConfigApplicationContext를 인스턴스화 할 때 @Configuration 클래스들을 인풋으로 사용할 것이다. 이를 통해 전혀 XML을 사용하지 않고 스프링 컨테이너를 사용할 수 있다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Java

public static void main(String[] args) {
  AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
  ctx.register(AppConfig.class, OtherConfig.class);
  ctx.register(AdditionalConfig.class);
  ctx.refresh();
  MyService myService = ctx.getBean(MyService.class);
  myService.doStuff();
}

4.12.2.3 scan(String...)으로 컴포넌트 스캔 가능하게 하기
경험있는 스프링 사용자들은 다음과 같이 일반적으로 사용되는 스프링의 context: 네임스페이스로 XML을 선언하는데 익숙할 것이다.

1
2
3
4
5
Xml

<beans>
  <context:component-scan base-package="com.acme"/>
</beans>

위의 예제에서 com.acme 팩키지는 스캔되고 @Component 어노테이션이 붙은 클래스들을 찾고 이러한 클래스를 컨테이너내 스프링 빈 정의로 등록할 것이다. AnnotationConfigApplicationContext에는 같은 컴포넌트 스캔 기능을 하는 scan(String...) 메서드가 있다.

1
2
3
4
5
6
7
8
Java

public static void main(String[] args) {
  AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
  ctx.scan("com.acme");
  ctx.refresh();
  MyService myService = ctx.getBean(MyService.class);
}




Note
@Configuration 클래스들은 @Component로 메타 어노테이션이 붙은 클래스라는 것을 기억해라. 그래서 이 클래스들은 컴포넌트 스캔의 후보들이 된다. 위의 예제에서 com.acme 팩키지 (또는 그 하위의 어떤 팩키지)내에 AppConfig가 선언었다는 것을 가정하고 이는 scan()을 호출하는 동안 선택될 것이고 클래스의 모든 @Bean 메서드들을 refresh() 할 때 컨테이너내 빈 정의로 처리되고 등록될 것이다.

4.12.2.4 AnnotationConfigWebApplicationContext를 사용한 웹 어플리케이션 지원
AnnotationConfigApplicationContext의 WebApplicationContext 변형은 AnnotationConfigWebApplicationContext로 사용할 수 있다. 이 구현체는 스프링 ContextLoaderListener 서블릿 리스너, 스프링 MVC DispatcherServlet 등을 설정할 때 사용할 수 있다. 다음은 전형적인 스프링 MVC 웹 어플리케이션을 설정하는 web.xml의 예제이다. contextClass context-param과 init-param의 사용방법을 보여준다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Xml

<web-app>
  <!-- 기본 XmlWebApplicationContext 대신 AnnotationConfigWebApplicationContext를 
       사용하는 ContextLoaderListener를 설정한다 -->
  <context-param>
      <param-name>contextClass</param-name>
      <param-value>
          org.springframework.web.context.support.AnnotationConfigWebApplicationContext
      </param-value>
  </context-param>

  <!-- 설정 위치는 반드시 콤마나 공백을 구분자로 사용하는 하나 이상의 정규화된 @Configuration 
       클래스들로 구성되어야 한다. 정규화된 팩키지는 컴포넌트 스캔으로 
       지정될 수도 있다. -->
  <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>com.acme.AppConfig</param-value>
  </context-param>

  <!-- 평소처럼 ContextLoaderListener를 사용해서 루트 어플리케이션 시작하기 -->
  <listener>
      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <!-- 평소처럼 스프링 MVC DispatcherServlet 선언 -->
  <servlet>
      <servlet-name>dispatcher</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
      <!-- 기본 XmlWebApplicationContext 대신 AnnotationConfigWebApplicationContext를 
           사용한 DispatcherServlet 설정 -->
      <init-param>
          <param-name>contextClass</param-name>
          <param-value>
              org.springframework.web.context.support.AnnotationConfigWebApplicationContext
          </param-value>
      </init-param>
      <!-- 다시한번, 설정 위치는 반드시 콤마나 공백을 구분자로 사용하는 하나 이상의 정규화된 
           @Configuration 클래스들로 구성되어야 한다. -->
      <init-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>com.acme.web.MvcConfig</param-value>
      </init-param>
  </servlet>

  <!-- /app/*에 대한 모든 요청을 디스패쳐 서블릿에 매핑한다 -->
  <servlet-mapping>
      <servlet-name>dispatcher</servlet-name>
      <url-pattern>/app/*</url-pattern>
  </servlet-mapping>
</web-app>

4.12.3 자바 기반으로 설정 구성하기
4.12.3.1 @Import 어노테이션의 사용
스프링 XML 파일에서 설정 모듈화에 <import/>요소를 사용하기는 하지만 @Import 어노테이션은 다른 설정 클래스에서 @Bean 설정을 로딩한다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Java

@Configuration
public class ConfigA {
  public @Bean A a() { return new A(); }
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {
  public @Bean B b() { return new B(); }
}

이제 컨텍스트를 인스턴스화 할 때 ConfigA.class와 ConfigB.class를 둘 다 지정해야하는 대신 ConfigB만 명시적으로 제공하면 된다.

1
2
3
4
5
6
7
8
9
Java

public static void main(String[] args) {
  ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

  // now both beans A and B will be available...
  A a = ctx.getBean(A.class);
  B b = ctx.getBean(B.class);
}

이 접근은 개발자가 설정을 구성하는 동안 잠재적으로 많은 수의 @Configuration 클래스들을 기억해야 하는 대신 하나의 클래스만 다루면 되므로 컨테이너 인스턴스화를 단순화한다.

임포트한 @Bean 정의에서 의존성 주입하기
위의 예제는 동작하기는 하지만 너무 간단하다. 대부분의 실무에서는 빈에 다른 설정 클래스들에 대한 의존성이 있을 것이다. XML을 사용할 때 컴파일러가 관여하지 않고 그냥 ref="someBean"만 선언한 뒤 스프링이 컨테이너를 인스턴스화 하면제 제대로 동작하기를 믿으면 되기 때문에 의존성 자체는 이슈가 아니었다. 물론 @Configuration를 사용할 때 자바 컴파일러는 다른 빈에 대한 참조는 유효한 자바문법이어야 한다는 제약을 설정 모델에 둔다.

다행히도 이 문제의 해결책은 간단하다. @Configuration 클래스들은 결국 컨테이너내의 다른 빈일 뿐이라는 것을 기억해라. 이는 @Configuration 클래스들이 다른 빈처럼 @Autowired 주입 메타데이터의 이점을 취할 수 있다는 것을 의미한다!

다른 빈들에 선언된 빈에 따라 각각 다수의 @Configuration 클래스들이 있는 더 현실적인 시나리오를 생각해 보자.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Java

@Configuration
public class ServiceConfig {
  private @Autowired AccountRepository accountRepository;

  public @Bean TransferService transferService() {
      return new TransferServiceImpl(accountRepository);
  }
}

@Configuration
public class RepositoryConfig {
  private @Autowired DataSource dataSource;

  public @Bean AccountRepository accountRepository() {
      return new JdbcAccountRepository(dataSource);
  }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
  public @Bean DataSource dataSource() { /* return new DataSource */ }
}

public static void main(String[] args) {
  ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
  // 모든 것들은 설정 클래스들 사이에서 연결된다...
  TransferService transferService = ctx.getBean(TransferService.class);
  transferService.transfer(100.00, "A123", "C456");
}

네비게이션을 편하게 하기 위해 임포트한 빈들을 정규화하기
위의 시나리오에서 @Autowired는 잘 동작하고 원하는 모듈화를 제공하지만 정확히 자동연결된 빈정의가 어디인지 결정하는 것은 여전히 약간 모호하다. 예를 들어 한 개발자가 ServiceConfig를 보듯이 정확히 어디에 @Autowired AccountRepository 빈이 선언되었는지 어떻게 알 수 있는가? 이는 코드에서 명백하지 않지만 괜찮을 것이다. SpringSource Tool Suite가 모든 것들이 어떻게 연결되는지 보여주는 그래프를 그리는 도구를 제공한다. 이 그래프가 당신이 바라는 전부일 것이다. 게다가 자바 IDE는 쉽게 모든 선언과 AccountRepository의 사용을 찾을 수 있고 해당 타입을 리턴하는 @Bean 메서드의 위치를 빠르게 보여줄 것이다.

이 애매모호함을 받아들일 수 없고 한 @Configuration 클래스에서 다른 클래스까지 IDE에서 직접 네비게이션하기를 원하는 경우에는 설정 클래스들 자체를 자동연결하는 것을 고려해 봐라.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java

@Configuration
public class ServiceConfig {
  private @Autowired RepositoryConfig repositoryConfig;

  public @Bean TransferService transferService() {
      // 설정 클래스 '전체에서' @Bean 메서드를 탐색한다!
      return new TransferServiceImpl(repositoryConfig.accountRepository());
  }
}

위의 상황에서는 어디 AccountRepository가 정의되었는지가 아주 명백하다. 하지만 이제 ServiceConfig가 RepositoryConfig에 강하게 연결되어 있다. 이는 트레이드 오프(tradeoff)다. 인터페이스 기반이나 추상 클래스 기반의 @Configuration 클래스들을 사용해서 이 강한 커플링을 약간 완화할 수 있다. 다음을 보자.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Java

@Configuration
public class ServiceConfig {
  private @Autowired RepositoryConfig repositoryConfig;

  public @Bean TransferService transferService() {
      return new TransferServiceImpl(repositoryConfig.accountRepository());
  }
}

@Configuration
public interface RepositoryConfig {
  @Bean AccountRepository accountRepository();
}

@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {
  public @Bean AccountRepository accountRepository() {
      return new JdbcAccountRepository(...);
  }
}

@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // 구체적인(concrete) 설정을 임포트한다!
public class SystemTestConfig {
  public @Bean DataSource dataSource() { /* DataSource를 리턴한다 */ }
}

public static void main(String[] args) {
  ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
  TransferService transferService = ctx.getBean(TransferService.class);
  transferService.transfer(100.00, "A123", "C456");
}

이제 ServiceConfig는 관련된 구체적인(concrete) DefaultRepositoryConfig에 약하게 연결되어 있고 내장 IDE 도구는 여전히 유용하다. 개발자는 쉽게 RepositoryConfig 구현체들의 타입 계층을 얻을 수 있을 것이다. 이 방법으로 @Configuration 클래스들과 그 의존성들을 네비게이션 하는 것은 인터페이스 기반의 코드를 네비게이션하는 일반적인 과정과 다르지 않아졌다.

4.12.3.2 자바와 XML 설정을 조합하기
스프링의 @Configuration 클래스 지원은 스프링의 XML을 100% 완전히 대체하지 못한다. 스프링 XML 네임스페이스같은 몇몇 기능들은 여전히 컨테이너를 설정하는 이상적인 방법이다. XML이 편리하거나 필수적인 상황에서 선택권이 있다. ClassPathXmlApplicationContext등을 사용한 "XML 중심적인" 방법으로 컨테이너를 인스턴스화하거나 AnnotationConfigApplicationContext와 필요한 XML을 임포트하는 @ImportResource 어노테이션을 사용하는 "자바 중심적인"방법 중에서 선택할 수 있다.

@Configuration 클래스의 XML 중심적인 사용
애드훅으로 @Configuration 클래스와 함께 XML로 스프링 컨테이너를 시작하는 것을 선호할 수도 있다. 예를 들어 스프링 XML을 사용하는 많은 양의 코드가 이미 있는 경우 필요한만큼의 원리에 따라 @Configuration 클래스를 생성하고 존재하는 XML파일에서 이 클래스들을 포함하는 것을 쉬울 것이다. 아래에서 "XML 중심적인" 상황에 위와 같은 경우에서 @Configuration 클래스들을 사용하는 옵션들을 볼 것이다.

평범한 스프링 <bean/> 요소처럼 @Configuration 클래스 선언하기
@Configuration는 결국은 그냥 컨테이너의 빈 정의라는 것을 기억해라. 이 예제에서 AppConfig라는 @Configuration 클래스를 생성하고 이 클래스를 <bean/>정의로 system-test-config.xml안에 포함시켰다. <context:annotation-config/>를 활성화 했기 때문에 컨테이너는 @Configuration 어노테이션을 인식하고 AppConfig에 선언된 @Bean 메서드를 적절히 처리할 것이다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Java

@Configuration
public class AppConfig {
  private @Autowired DataSource dataSource;

  public @Bean AccountRepository accountRepository() {
      return new JdbcAccountRepository(dataSource);
  }

  public @Bean TransferService transferService() {
      return new TransferService(accountRepository());
  }
}

Xml

system-test-config.xml
<beans>
  <!-- @Autowired와 @Configuration같은 어노테이션 처리가 가능하게 하기 -->
  <context:annotation-config/>
  <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

  <bean class="com.acme.AppConfig"/>

  <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
      <property name="url" value="${jdbc.url}"/>
      <property name="username" value="${jdbc.username}"/>
      <property name="password" value="${jdbc.password}"/>
  </bean>
</beans>

C-like

jdbc.properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=

Java

public static void main(String[] args) {
  ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
  TransferService transferService = ctx.getBean(TransferService.class);
  // ...
}



Note
위의 system-test-config.xml에서 AppConfig<bean/>에 id 요소를 선언하지 않았다. 이렇게 하는 것은 허용되지 않으며 이를 참조하는 빈이 없으므로 필요하지 않다. 그리고 이름으로 컨테이너에서 명시적으로 가져올 가능성도 없다. DataSource 빈과 마찬가지로 이는 타입으로만 자동연결되므로 명시적인 빈 id가 엄격하게 필요하지 않다.

@Configuration를 선택하는 <context:component-scan/>의 사용
@Configuration에 @Component 메타 어노테이션이 붙었으므로 @Configuration 어노테이션이 붙은 클래스들은 자동적으로 컴포넌트 스캔의 후보가 된다. 위와 같은 시나리오에서 컴포넌트 스캔의 이점을 얻기 위해 system-test-config.xml를 재정의 할 수 있다. 이 경우에 <context:component-scan/>가 같은 기능을 모두 사용가능하게 하므로 명시적으로 <context:annotation-config/>를 선언할 필요가 없다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Xml

system-test-config.xml
<beans>
  <!--  정의로 AppConfig를 선택하고 등록한다 -->
  <context:component-scan base-package="com.acme"/>
  <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

  <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
      <property name="url" value="${jdbc.url}"/>
      <property name="username" value="${jdbc.username}"/>
      <property name="password" value="${jdbc.password}"/>
  </bean>
</beans>

@ImportResource로 @Configuration 클래스 중심적인 XML의 사용
@Configuration클래스가 컨테이너를 설정하는 주요 메카니즘인 어플리케이션에서 여전히 최소한 약간의 XML은 사용할 필요가 있을 것이다. 이러한 시나리오에서 간단히 @ImportResource를 사용하고 필요한만큼의 XML을 정의한다. 이렇게 해서 최소한의 XML만으로 컨테이너를 설정하는 "자바 중심적인" 접근을 할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Java

@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {
  private @Value("${jdbc.url}") String url;
  private @Value("${jdbc.username}") String username;
  private @Value("${jdbc.password}") String password;

  public @Bean DataSource dataSource() {
      return new DriverManagerDataSource(url, username, password);
  }
}

Xml

properties-config.xml
<beans>
  <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>

C-like

jdbc.properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=

Java

public static void main(String[] args) {
  ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
  TransferService transferService = ctx.getBean(TransferService.class);
  // ...
}

4.12.4 @Bean 어노테이션의 사용
@Bean는 메서드 레벨의 어노테이션이고 XML <bean/> 요소와 바로 대응되는 요소이다. 이 어노테이션은 init-method, destroy-method, autowiring, name처럼 <bean/>가 제공하는 속성들의 일부를 지원한다.

@Configuration 어노테이션이나 @Component 어노테이션이 붙은 클래스에서 @Bean 어노테이션을 사용할 수 있다.

4.12.4.1 빈 선언하기
빈을 선언하려면 메서드에 @Bean 어노테이션을 붙히면 된다. 메서드가 리턴하는 값으로 지정한 타임의 ApplicationContext내에서 빈 정의를 등록하기 위해 이 메서드를 사용한다. 기본적으로 빈 이름은 메서드의 이름과 같을 것이다. 다음은 @Bean 메서드를 선언하는 간단한 예제이다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Java

@Configuration
public class AppConfig {

  @Bean
  public TransferService transferService() {
      return new TransferServiceImpl();
  }
}

앞의 설정은 다음 스프링 XML과 완전히 똑같다.

1
2
3
4
5
Xml

<beans>
  <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>  

두 선언은 모두 ApplicationContext에서 사용할 수 있는 transferService라는 빈을 만들고 TransferServiceImpl 타입의 인스턴스에 바인딩한다.

1
2
3
C-like

transferService -> com.acme.TransferServiceImpl

4.12.4.2 의존성 주입
@Bean 들이 다른 빈에 대한 의존성이 있을 때 의존성을 나타내는 것은 하나의 빈 메서드가 다른 메서드를 호출하는 것만큼 간단하다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Java

@Configuration
public class AppConfig {

  @Bean
  public Foo foo() {
      return new Foo(bar());
  }

  @Bean
  public Bar bar() {
      return new Bar();
  }
}
위의 예제에서 foo은 생성자 주입으로 bar에 대한 참조를 받는다.

4.12.4.3 라이프사이클 콜백 받기
@Configuration 어노테이션이 붙은 클래스에서 선언한 빈들은 정규 라이프사이클 콜백을 지원한다. @Bean 어노테이션으로 정의된 어떤 클래스들도 JSR-250의 @PostConstruct와 @PreDestroy 어노테이션을 사용할 수 있다. JSR-250의 자세한 내용은 JSR-250 annotations를 봐라.

정규 스프링 라이프사이클 콜백을 완전히 지원한다. 빈이 InitializingBean, DisposableBean, Lifecycle를 구현하면 컨테이너가 각각의 메서드를 호출한다.

BeanFactoryAware, BeanNameAware, MessageSourceAware, ApplicationContextAware 등과 같은 *Aware 인터페이스 표준 세트도 완전히 지원한다.

스프링 XML의 bean 요소에서 init-method와 destroy-method 속성처럼 @Bean 어노테이션은 임의의 초기화와 파괴 콜백 메서드 지정하는 것을 지원한다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Java

public class Foo {
  public void init() {
      // 초기화 로직
  }
}

public class Bar {
  public void cleanup() {
      // 파괴 로직
  }
}

@Configuration
public class AppConfig {
  @Bean(initMethod = "init")
  public Foo foo() {
      return new Foo();
  }
  @Bean(destroyMethod = "cleanup")
  public Bar bar() {
      return new Bar();
  }
}

물론 위의 Foo의 경우는 생성과정동안 init() 메서드를 유효하게 직접 호출하는 것과 같다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Java

@Configuration
public class AppConfig {
  @Bean
  public Foo foo() {
     Foo foo = new Foo();
     foo.init();
     return foo;
  }

  // ...
}




Tip
자바로 직접 작업할 때 객체로 할 수 있는 모든 것을 할 수 있고 항상 컨테이너 라이프사이클에 의존한 필요가 없다!

4.12.4.4 빈 범위 지정하기
@Scope 어노테이션의 사용
@Bean 어노테이션으로 정의한 빈이 특정 범위를 갖도록 지정할 수 있다. Bean Scopes 섹션에서 지정한 표준 범위들은 모두 사용할 수 있다.

기본 범위는 singleton이지만 @Scope 어노테이션으로 이 기본 범위를 오버라이드할 수 있다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Java

@Configuration
public class MyConfiguration {
  @Bean
  @Scope("prototype")
  public Encryptor encryptor() {
      // ...
  }
}

@Scope와 범위를 가진 프록시
스프링은 범위를 가진 프록시를 통해서 범위를 가진 의존성과 동작하도록 하는 편리한 방법을 제공한다. XML 설정을 사용할 때 이러한 프록시를 생성하는 가장 쉬운 방법은 <aop:scoped-proxy/> 요소이다. 자바로 @Scope 어노테이션이 붙은 빈을 설정할 때 proxyMode 속성으로 동일한 기능을 제공한다. 기본값은 프록시가 없는 것이지만 (ScopedProxyMode.NO) ScopedProxyMode.TARGET_CLASS나 ScopedProxyMode.INTERFACES를 지정할 수 있다.

XML 레퍼런스 문서의 범위를 가진 프록시 예제를 (앞의 링크를 봐라) 자바를 사용하는 @Bean으로 포팅하면 다음과 같을 것이다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Java

// HTTP 세션 범위를 가진 빈을 프록시로 노출한다
@Bean
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public UserPreferences userPreferences() {
 return new UserPreferences();
}

@Bean
public Service userService() {
 UserService service = new SimpleUserService();
 // 프록시 userPreferences 빈에 대한 참조
 service.setUserPreferences(userPreferences());
 return service;
}

검색 메서드 주입
앞에서 얘기했듯이 검색 메서드 주입은 드물게 사용하는 고급 기능이다. 이 기능은 싱글톤 범위를 가진 빈이 프로토타입 범위를 가진 빈에 의존성이 있는 경우에 유용하다. 자바로 이러한 타입의 설정을 사용한다는 것은 이 패턴을 구현한다는 자연스러운 의미가 된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Java

public abstract class CommandManager {
  public Object process(Object commandState) {
      // 적절한 Command 인터페이스의 새로운 인스턴스를 획득한다
      Command command = createCommand();

      // (완전히 새로운) Command 인스턴스에 상태를 설정한다
      command.setState(commandState);
      return command.execute();
  }

  // 괜찮다... 하지만 이 메서드의 구현은 어디에 있는가?
  protected abstract Command createCommand();
}

자바로 설정을 사용할 때 새로운 (프로토타입) 커맨드 객체를 검색하는 방법같은 추상 createCommand() 메서드를 오버라이드한 CommandManager의 서브클래스를 생성할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Java

@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
  AsyncCommand command = new AsyncCommand();
  // 필요한 대로 여기서 의존성을 주입한다
  return command;
}

@Bean
public CommandManager commandManager() {
  // 새로운 프로토타입 Command 객체를 리턴하도록 command()를 오버라이드한 CommandManager의 
  // 새로운 익명 구현체를 리턴한다
  return new CommandManager() {
      protected Command createCommand() {
          return asyncCommand();
      }
  }
}

4.12.4.5 빈 이름 커스터마이징하기
기본적으로 설정 클래스들은 @Bean 메서드의 이름을 생성된 빈의 이름으로 사용한다. 하지만 이 기능은 name 속성으로 오버라이드 할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Java

@Configuration
public class AppConfig {

  @Bean(name = "myFoo")
  public Foo foo() {
      return new Foo();
  }
}

4.12.4.6 빈 별칭짓기
Section 4.3.1, “빈 이름짓기”에서 얘기했듯이 때로는 빈 별칭짓기로 알려진 단일 빈에 여러 가지 이름을 주어야 한다. @Bean 어노테이션의 name 속성은 이 용도를 위해서 문자열 배열을 받아들인다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Java

@Configuration
public class AppConfig {

  @Bean(name = { "dataSource", "subsystemA-dataSource", "subsystemB-dataSource" })
  public DataSource dataSource() {
      // 인스턴스화하고 설정하고 DataSource 빈을 리턴한다...
  }
}

4.12.5 자바기반의 설정의 내부동작에 대한 추가적인 내용
다음 예제는 @Bean 어노테이션이 분은 메서드가 두번 호출되는 것을 보여준다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Java

@Configuration
public class AppConfig {

  @Bean
  public ClientService clientService1() {
    ClientServiceImpl clientService = new ClientServiceImpl();
    clientService.setClientDao(clientDao());
    return clientService;
  }
  @Bean
  public ClientService clientService2() {
    ClientServiceImpl clientService = new ClientServiceImpl();
    clientService.setClientDao(clientDao());
    return clientService;
  }

  @Bean
  public ClientDao clientDao() {
    return new ClientDaoImpl();
  }
}

clientDao()는 clientService1()에서 한번 clientService2()에서 한번 호출된다. 이 메서드가 ClientDaoImpl의 새로운 인스턴스를 생성하고 리턴하기 때문에 2개의 인스턴스를 (각 서비스마다 하나씩) 기대하는 것이 정상적이다. 이는 명확하게 문제의 소지가 있다. 스프링에서 인스턴스화 된 빈들은 기본적으로 singleton 범위를 가진다. 여기가 마법이 일어나는 곳이다. 모든 @Configuration 클래스는 시작할 때(startup-time) CGLIB과 함께 서브클래스가 된다. 서브클래스에서 자식 메서드는 부모 메서드를 호출하고 새로운 인스턴스를 생성하기 전에 캐싱된 (범위를 가진) 빈이 컨터이너에 있는지 먼저 확인한다.


Note
이 동작은 빈의 범위에 따라 다를 수 있다. 여기서는 싱글톤에 대해서 얘기한 것이다.


Note
JavaConfig가 동작하도록 하려면 의존성 리스트에 CGLIB jar를 반드시 포함시켜야 한다는 것을 주의해라.


Note
CGLIB은 시작할 때 동적으로 기능을 추가하기 때문에 몇 가지 제약사항이 있다.

  • Configuration 클래스는 final이 될 수 없다.
  • Configuration 클래스에는 아규먼트가 없는 생성자가 있어야 한다.

댓글 없음:

댓글 쓰기