[스프링 IoC 컨테이너와 빈]_04 @Component와 컴포넌트 스캔
@Component와 컴포넌트 스캔
컨포넌트 스캔 주요 기능
● 스캔 위치 설정
● 필터: 어떤 애노테이션을 스캔 할지 또는 하지 않을지
@Component
● @Repository
● @Service
● @Controller
● @Configuration
동작 원리
● @ComponentScan은 스캔할 패키지와 애노테이션에 대한 정보
● 실제 스캐닝은 ConfigurationClassPostProcessor라는 BeanFactoryPostProcessor에 의해 처리 됨.
펑션을 사용한 빈 등록
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(Demospring51Application.class)
.initializers((ApplicationContextInitializer<GenericApplicationContext>)
applicationContext -> {
applicationContext.registerBean(MyBean.class);
})
.run(args);
}
컨포넌트 스캔 주요 기능
지금까지 @Service, @Repository 어노테이션을 붙이면 해당 클래스가 빈으로 등록이 되고 사용이 가능했다. 그 이유는 @SpringBootApplication
을 확인해보면 이미 @ComponentScan
이 정의되어 있기 때문이다.
@ComponentScan
을 좀 더 알아보자
컴포넌트 스캐닝 범주
-
가장 중요한 설정이
basePackages(scanBasePackages)
,basePackageClasses(scanBasePackageClasses)
이다.basePackages(scanBasePackages)
은 문자열을 입력받기 때문에 Type Safety 하지 않는다.따라서 Type Safety하게 설정할 수 있는
basePackageClasses(scanBasePackageClasses)
속성이 있다. -
basePackageClasses(scanBasePackageClasses)
에 전달된 클래스를 기준으로 컴포넌트 스캔을 시작한다. -
Default로는 @Component을 붙인 Configuration 부터 컴포넌트 스캔을 시작한다.
@SpringBootApplication을 붙인 (main메서드가 있는) 클래스가 시작지점이 된다.
해당 클래스가 들어있는 패키지 하위의 모든 클래스와 모든 패키지를 스캐닝 한다.
패키지만 추가하면 다른 패키지여도 코드 내에서 다른 패키지의 클래스를 사용할 수 있지만, 컴포넌트 스캐닝 범위에 들어가지 않아서 빈으로 등록 되지 않기 때문에 @Autowired로 빈 주입을 할 수 없다.
실행결과 → 빈을 찾을 수 없어서 에러 발생
***************************
APPLICATION FAILED TO START
***************************
Description:
Field myService in dev.solar.demospring51.Demospring51Application required a bean of type 'dev.solar.out.MyService' that could not be found.
The injection point has the following annotations:
- @org.springframework.beans.factory.annotation.Autowired(required=true)
Action:
Consider defining a bean of type 'dev.solar.out.MyService' in your configuration.
⇒ 빈 주입 관련 에러 발생 시, 어디서 부터 컴포넌트 스캐닝이 이루어졌는지 범위를 따져봐야 한다.
Filter 설정
컴포넌트 스캐닝을 한다고 해서 모든 어노테이션들을 다 처리해서 빈으로 등록하는 것은 아니다. 제외할 대상을 지정할 수 있다.
- exclude(), includeFilters(), excludeFilters() etc …
- SpringBootApplication에 2가지 Filter가 기본적으로 설정돼있다.
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
- AutoConfigurationExcludeFilter : 스프링 부트가 제공해주는 AutoConfiguration 관련 필터….. 뭔지는 모르겠음
- 위에 두가지 필터 내용이 중요한게 아님
@Component
기본적으로 @Component 어노테이션이 붙은 클래스들은 빈으로 등록이 된다.
- @Repository, @Service, @Controller, @Configuration
※ 단점
이런 빈들은 싱글톤 스코프의 빈들은 초기에 다 생성을 한다. 등록해야하는 빈이 많은 경우에 초기 구동시간이 오래걸릴 수 있다.
(빈의 기본 스코프는 싱글톤이며 따로 스코프를 지정할 수는 있다.)
초기 구동시간 외에 한번 실행이 되고 난 이후에는 빈을 생성한다고 성능을 낮추지 않기 때문에 크게 신경쓸 부분이 아니라고 생각이지만, 구동 시간 성능을 높이고 싶다면 스프링 5버전에서 사용가능한 펑션을 사용한 빈 등록 방식을 쓰면 된다.
펑션을 사용한 빈 등록
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(Demospring51Application.class)
.initializers((ApplicationContextInitializer<GenericApplicationContext>)
applicationContext -> {
applicationContext.registerBean(MyBean.class);
})
.run(args);
}
Reflection1과 Proxy를 만드는 기법(CGlib)은 성능에 영향을 준다. 펑션을 사용한 빈 등록방법은 이러한 기술을 사용하지 않기 때문에 성능상의 이점을 조금이나마 가져갈 수 있다.
여기서의 성능은 애플리케이션 구동 타임의 성능이다.
※ 스프링 부트 구동방식 3가지
-
Static 메서드를 사용한 방식
@SpringBootApplication public class Demospring51Application { public static void main(String[] args) { SpringApplication.run(Demospring51Application.class, args); } }
-
Builder를 사용
-
인스턴스를 만들어서 사용
※ 펑션을 이용해 빈을 등록 실습
-
인스턴스를 만들어서 사용하는 방식으로 스프링 부트 애플리케이션을 구동하여 해보자
다음과 같이 애플리케이션을 실행할 수 있다.
@SpringBootApplication public class Demospring51Application { public static void main(String[] args) { SpringApplication app = new SpringApplication(Demospring51Application.class); app.run(args); } }
-
중간에 동작을 추가하고 싶다면
addInitializers()
를 추가해주면 된다.내가 원하는 ApplicationContext를 주입받을 수 있다.
GenericApplicationContext
의registerBean()
으로 직접 주입 받는다.컴포넌트 스캔 범위 밖에 만들었던 클래스를 빈으로 등록해보자
빈으로 등록했으므로 @Autowired로 빈을 주입받아 사용 가능하다.
@SpringBootApplication public class Demospring51Application { @Autowired //빈 주입 MyService myService; public static void main(String[] args) { SpringApplication app = new SpringApplication(Demospring51Application.class); app.addInitializers(new ApplicationContextInitializer<GenericApplicationContext>() { @Override public void initialize(GenericApplicationContext ctx) { ctx.registerBean(MyService.class); //빈 등록 } }); app.run(args); } }
⇒ 애플리케이션을 실행해보면 오류가 나지 않는다. 즉, 빈이 성공적으로 주입되었다는 뜻이다.
-
registerBean()에 Suplier를 주는 방식
ApplicationRunner.class 타입을 등록하고, Suplier를 전달
- Suplier : ApplicationRunner.class 타입에 해당하는 인스턴스를 제공
@SpringBootApplication public class Demospring51Application { @Autowired MyService myService; public static void main(String[] args) { SpringApplication app = new SpringApplication(Demospring51Application.class); app.addInitializers(new ApplicationContextInitializer<GenericApplicationContext>() { @Override public void initialize(GenericApplicationContext ctx) { // if 조건 추가 ctx.registerBean(MyService.class); ctx.registerBean(ApplicationRunner.class, new Supplier<ApplicationRunner>() { @Override public ApplicationRunner get() { return new ApplicationRunner() { @Override public void run(ApplicationArguments args) throws Exception { System.out.println("Functional Bean Definition!!"); } }; } }); } }); app.run(args); } }
이렇게 펑셔널하게 빈을 등록할 때의 장점은 추가적인 코딩을 할 수 있다.
조건에 따라 빈을 생성하는 등의 프로그래밍적인 컨트롤이 가능하다.
동작 원리
- @ComponentScan은 스캔할 패키지와 애노테이션에 대한 정보
- 실제 스캐닝은 ConfigurationClassPostProcessor라는 BeanFactoryPostProcessor에 의해 처리 됨.
BeanPostProcessor
와 비슷하지만 실행되는 시점이 다르다.
다른 모든 빈들을 만들기 이전에 BeanFactoryPostProcessor
구현체들을 적용해준다.
다른 빈들을 모두 등록하기 전에 컴포넌트 스캔을 해서 빈으로 등록해준다.
여기서 다른 빈은 우리가 직접 등록하는 빈 또는 @Bean어노테이션으로 등록하는 빈들 등등을 포함한다.
구동 시의 성능 이점을 위해 컴포넌트 스캔으로 등록할 수 있는 빈을 펑션을 사용한 방식으로 대체해서 쓴다? ⇒ 반대
구현하기 어렵고, 너무 어마어마한 설정 파일이 만들어지게 될 것이다. 컴포넌트 스캔이 나오게된 배경 이전 시점으로 돌아가는 것과 같을 듯
즉, 컴포넌트 스캔을 대신해서 사용한다기 보다 컴포넌트 스캔 말고, 직접 빈으로 등록하게 되는 경우가 있다. 그런 경우에 펑셔널한 빈 등록 방식을 사용하는 것도 나쁘지 않다.
즉, 다음 코드를 대체해서 펑셔널한 등록 방식을 사용하는 것은 괜찮다.
@Bean
public MyService myService() {
return new MyService();
}
-
자바의 Reflection은 JVM에서 실행되는 애플리케이션의 런타임 동작을 검사하거나 수정할 수 있는 기능이 필요한 프로그램에서 사용됩니다. 쉽게 말하자면, 클래스의 구조를 개발자가 확인할 수 있고, 값을 가져오거나 메소드를 호출하는데 사용됩니다. ↩