[Spring 레퍼런스] 4장 IoC 컨테이너 #11
FRAMEWORK/SPRING REF 2014. 9. 29. 16:53이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.
4.11 JSR 330 표준 어노테이션 사용
스프링 3.0부터는 JSR-330 표준 어노테이션 (의존성 주입)을 지원한다. 이러한 어노테이션들은 스프링 어노테이션과 같은 방법으로 스캔한다. 클래스패스에 적절한 jar를 두기만 하면 된다.
메이븐을 사용한다면 표준 메이븐 저장소(http://repo1.maven.org/maven2/javax/inject/javax.inject/1/)에서 javax.inject artifact를 사용할 수 있다. pom.xml 파일에 다음 의존성을 추가할 수 있다.
1 2 3 4 5 | < 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 | 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 | 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 | 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 | 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 | < beans > < context:component-scan base-package = "org.example" /> </ beans > |
4.11.3 표준 접근의 한계
표준 어노테이션들을 사용해서 작업할 때 다음 표에 나온 것처럼 몇가지 중요한 기능들을 사용할 수 있다는 것을 꼭 알아야 한다.
Table 4.6. 스프링 어노테이션 vs. 표준 어노테이션
Spring | javax.inject.* | javax.inject의 제약 / 설명 |
---|---|---|
@Autowired | @Inject | @Inject에는 'required'속성이 없다 |
@Component | @Named | - |
@Scope("singleton") | @Singleton | JSR-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 | @Configuration public class AppConfig { @Bean public MyService myService() { return new MyServiceImpl(); } } |
스프링 <beans/> XML에 익숙하다면 앞의 AppConfig 클래스는 다음과 같을 것이다.
1 2 3 | < 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 | 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 | < beans > < context:component-scan base-package = "com.acme" /> </ beans > |
위의 예제에서 com.acme 팩키지는 스캔되고 @Component 어노테이션이 붙은 클래스들을 찾고 이러한 클래스를 컨테이너내 스프링 빈 정의로 등록할 것이다. AnnotationConfigApplicationContext에는 같은 컴포넌트 스캔 기능을 하는 scan(String...) 메서드가 있다.
1 2 3 4 5 6 | public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.scan( "com.acme" ); ctx.refresh(); MyService myService = ctx.getBean(MyService. class ); } |
@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 | < 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 | @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 | 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 | @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 | @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 | @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 | @Configuration public class AppConfig { private @Autowired DataSource dataSource; public @Bean AccountRepository accountRepository() { return new JdbcAccountRepository(dataSource); } public @Bean TransferService transferService() { return new TransferService(accountRepository()); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 > |
1 2 3 4 | jdbc.properties jdbc.url=jdbc:hsqldb:hsql: //localhost/xdb jdbc.username=sa jdbc.password= |
1 2 3 4 5 | public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext( "classpath:/com/acme/system-test-config.xml" ); TransferService transferService = ctx.getBean(TransferService. class ); // ... } |
위의 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 | 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 | @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); } } |
1 2 3 4 | properties-config.xml < beans > < context:property-placeholder location = "classpath:/com/acme/jdbc.properties" /> </ beans > |
1 2 3 4 | jdbc.properties jdbc.url=jdbc:hsqldb:hsql: //localhost/xdb jdbc.username=sa jdbc.password= |
1 2 3 4 5 | 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 | @Configuration public class AppConfig { @Bean public TransferService transferService() { return new TransferServiceImpl(); } } |
앞의 설정은 다음 스프링 XML과 완전히 똑같다.
1 2 3 | < beans > < bean id = "transferService" class = "com.acme.TransferServiceImpl" /> </ beans > |
두 선언은 모두 ApplicationContext에서 사용할 수 있는 transferService라는 빈을 만들고 TransferServiceImpl 타입의 인스턴스에 바인딩한다.
1 | transferService -> com.acme.TransferServiceImpl |
1 2 | < font style = "font-weight: bold;" size = "3" >4.12.4.2 의존성 주입</ font > @Bean들이 다른 빈에 대한 의존성이 있을 때 의존성을 나타내는 것은 하나의 빈 메서드가 다른 메서드를 호출하는 것만큼 간단하다. |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @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 | 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 | @Configuration public class AppConfig { @Bean public Foo foo() { Foo foo = new Foo(); foo.init(); return foo; } // ... } |
자바로 직접 작업할 때 객체로 할 수 있는 모든 것을 할 수 있고 항상 컨테이너 라이프사이클에 의존한 필요가 없다!
4.12.4.4 빈 범위 지정하기
@Scope 어노테이션의 사용
@Bean 어노테이션으로 정의한 빈이 특정 범위를 갖도록 지정할 수 있다. Bean Scopes 섹션에서 지정한 표준 범위들은 모두 사용할 수 있다.
기본 범위는 singleton이지만 @Scope 어노테이션으로 이 기본 범위를 오버라이드할 수 있다.
1 2 3 4 5 6 7 8 | @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 | // 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 | 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 | @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 | @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 | @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 | @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과 함께 서브클래스가 된다. 서브클래스에서 자식 메서드는 부모 메서드를 호출하고 새로운 인스턴스를 생성하기 전에 캐싱된 (범위를 가진) 빈이 컨터이너에 있는지 먼저 확인한다.
이 동작은 빈의 범위에 따라 다를 수 있다. 여기서는 싱글톤에 대해서 얘기한 것이다.
JavaConfig가 동작하도록 하려면 의존성 리스트에 CGLIB jar를 반드시 포함시켜야 한다는 것을 주의해라.
CGLIB은 시작할 때 동적으로 기능을 추가하기 때문에 몇 가지 제약사항이 있다.
- Configuration 클래스는 final이 될 수 없다.
- Configuration 클래스에는 아규먼트가 없는 생성자가 있어야 한다.
관련 글
- Chapter 3. The IoC container - Spring
- [Spring 레퍼런스] 4장 IoC 컨테이너 #1 - Outsider's Dev Story
- [Spring 레퍼런스] 16장 웹 MVC 프레임워크 #1 - Outsider's Dev Story
- Spring Framework Reference Documentation
- Scope (Spring Framework API 2.5)
'FRAMEWORK > SPRING REF' 카테고리의 다른 글
[Spring 레퍼런스] 5장 리소스 #1 (0) | 2014.09.29 |
---|---|
[Spring 레퍼런스] 4장 IoC 컨테이너 #12 (0) | 2014.09.29 |
[Spring 레퍼런스] 4장 IoC 컨테이너 #10 (0) | 2014.09.29 |
[Spring 레퍼런스] 4장 IoC 컨테이너 #9 (0) | 2014.09.29 |
[Spring 레퍼런스] 4장 IoC 컨테이너 #8 (0) | 2014.09.29 |