스프링 핵심 기술-03.@Component와 컴포넌트 스캔


IoC 컨테이너 4부 : @Component와 컴포넌트 스캔

스프링 3.1부터 도입이 되었다. 말 그대로 컴포넌트 스캔을 통해 컴포넌트가 붙은 어노테이션을 다 찾아서(스캔) 빈으로 등록을 한다.

동작 원리는 뒷 부분에 설명하고,
우선 컴포넌트 스캔의 가장 중요한 설정은 베이스 패키지를 설정하는 것이다.

컴포넌트 스캔 어노테이션을 살펴보자.

public @interface ComponentScan {
    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

컴포넌트 스캔의 범위

보이는 대로, basePackages, basePackageClasses 메서드
베이스 패키지로 설정, 클래스로 설정을 할 수 있다.

ex. 베이스 패키지로 설정
@ComponentScan(“com.jaeuk.myPackage”)
@ComponentScan({“com.jaeuk”, “com.gisun”})
@ComponentScan({“com.jaeuk.package.first”, “com.jaeuk.package.second”})
@ComponentScan(basePackages={“com.jaeuk”, “com.gisum”})

문자열 설정은 타입세이프 하지 않기 때문에,
basePackageClasses 메서드를 활용해서 해당 클래스가 해당하는 패키지가 베이스 패키지라고 설정을 할 수도 있다.
(=해당 클래스부터 시작해서 패키지까지 타고 들어가서 이 해당 패키지는 모두 탐색)

ex. @ComponentScan(basePackageClasses=MyMarker.class)

따라서 다른 패키지를 만든다면 스캔이 안된다.
import 받아서 코드상 사용할 수는 있지만 스캔이 되지 않고,
그래서 빈으로 등록이 안되고 오토와이어드는 사용할 수 없다.
me.whiteship.jaeuk 에서 시작하면
me.whiteship.out 탐색 안됨

그럼 타입세이프한 basePackageClasses는 여러 개 같이 등록이 안될까,
혹은 basePackages와 basePackageClasses 두가지를 같이 사용할 수 없을까?
좀 찾아보던 중 괜찮게 정리된 외국 포스트가 있어 올린다.

https://dzone.com/articles/spring-component-scan

@ComponentScan(basePackageClasses = {ExampleController.class, ExampleModel.class, ExmapleView.class})

basePackageClasses specifier with your class references will tell Spring to scan those packages (just like the mentioned alternatives), but this method is both type-safe and adds IDE support for future refactoring

이런 방식으로 basePackageClasses 도 여러 클래스 설정이 가능하고, type-safe한 방식이기 때문에 추천한다고 한다. (향후 리팩토링 시 IDE의 지원)
예를 들면, 클래스 명이 변경된다거나, 패키지 명이 변경된다거나 하면 IDE가 지원하는 리팩토링 기능으로 모두 일괄 변경이 가능하다.
하지만 문자열로 선언을 해 놓으면, 내가 직접 수기로 변경을 해야 한다.

@ComponentScan(basePackages = {
        "guru.springframework.blog.componentscan.example.demopackageA",
        "guru.springframework.blog.componentscan.example.demopackageD",
        "guru.springframework.blog.componentscan.example.demopackageE"
    },
    basePackageClasses = DemoBeanB1.class)
public class BlogPostsApplicationWithComponentScan {
    public static void main(String[] args) {

이런 식으로 같이 사용도 가능하고, 정규식으로 필터를 건다던가 등..

컴포넌트 스캔의 필터

또한 설정을 통해 특정 조건에 해당하는 것들은 스캔을 하지 않을 수도 있다. (마찬가지로 위의 문서에 정리)

@ComponentScan(
    excludeFilters = { // 필터해서 걸러주는 옵션
    @Filter(type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class}), 
    @Filter(type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class})})
public @interface SpringBootApplication {

위의 예제는 TypeExcludeFilter, AutoConfigurationExcludeFilter 두 가지 클래스를 필터 한다.

다시 한번.
컴포넌트 스캔에서 가장 중요한 것은

  1. 어디서부터 어디까지 스캔할 것이냐.(베이스 패키지 {클래시스})
  2. 스캔하는 중 어떤 걸 걸러낼 것이냐. (필터)

컴포넌트 스캔의 동작

그럼 스프링 부트 애플리케이션을 살펴보자.
@SpringBootApplication 를 살펴보면 안에 @ComponentScan을 가지고 있다.
@ComponentScan을 가지고 있는 어노테이션이 시작지점이다.
@ComponentScan을 붙이고 있는 그 클래스부터 컴포넌트 스캔을 시작한다.
설정해 놓은 패키지들 중에서,
해당 패키지와 하위 패키지에 대해서 @Component같은 스테레오 타입 어노테이션이 붙은 클래스를 모두 찾아서 빈으로 등록한다.

특징

싱글톤 스콥으로 빈들이 생성된다.
구동시 모든 빈들을 전부 생성하기 때문에, 생성해야될 빈이 많으면 구동 시간이 오래 걸릴수도 있다. 하지만 구동 타임 1회성의 시간인 것이고, 이후부터는 다시 빈을 생성한다거나, 성능을 잡아먹는게 없으니까 큰 상관은 없다.

Functional Bean Definition

하지만 정말 구동시간이 정말 중요하거나 예민한 케이스라면 다른 방법을 고려할 수 있다.

스프링 5에서 들어온 Functional을 사용한 빈 등록 방법이 존재한다.

이 방법은 리플렉션이나 프록시를 만드는 기법들 (성능에 영향을 준다 이 두가지 방법은)

Functional Bean Definition 방식은 그 두가지 기술을 사용하지 않기 때문에 성능상(구동시간)의 장점이 있다.

예제 코드

package me.whiteeship.springapplicationcontext; // 현재 패키지 

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
		// 기존 실행코드
		// SpringApplication.run(DemoApplication.class, args);

		// 자바 11부터 app 이라는 로컬 variable 변수 활용 가능
		// 하지만 전 11이 아니라.. 8에서 가능한 방식으로만 정리
		// var app = new SpringApplication(DemoApplication.class);
		SpringApplication app = new SpringApplication(DemoApplication.class);

		app.addInitializers(new ApplicationContextInitializer<GenericApplicationContext>() {
			@Override
			public void initialize(GenericApplicationContext ctx) {
				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);
    }
}

그대로인데 람다식으로 줄인 코드

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
		SpringApplication app = new SpringApplication(DemoApplication.class);
		app.addInitializers((ApplicationContextInitializer<GenericApplicationContext>) ctx -> {
			ctx.registerBean(MyService.class); // 패키지 밖에 선언한 클래스, BookService 등 이 패키지에서 주입받는게 가능해짐.
			ctx.registerBean(ApplicationRunner.class, () -> args1 -> System.out.println("재욱 패키지 Functional Bean Definition"));
		});
		app.run(args);
    }
}

정상적으로 구동되는 것을 볼 수 있다. readme

진짜 처음으로 왜 인텔리J 하는지 느꼈습니다..
회색으로 표시된 글씨 마우스 올리고 있으면, 자동 추천 변환이 뜨고 (람다식 변환)
마우스 클릭 한번으로 바뀌고
인텔리J 진짜 편하구나 새삼 느꼈네요…

무튼 다시 돌아가서
이럴 때의 장점은 아까 말한대로

  1. 코딩을 좀 더 넣을 수 있다.
    특정 조건에 따라 이런 빈을 등록한다던가
    ctx.registerBean(MyService.class);
    앞에 if 같은 조건을 넣을 수도 있고.. 등

2.구동시 성능상의 장점
리플렉션 CG라이브러리 프록시 등(성능을 잡아먹는)을 사용하지 않으니까 구동시 더 빠르다.

  1. 해당 패키지(컴포넌트 스캔이 시작되는 패키지)가 아닌 바깥의 패키지/클래스도 사용이 가능합니다.

펑션을 활용한 빈 등록에 대한 생각

구동시의 성능상 더 장점이 있다고 해서 컴포넌트 스캔 방식을 버리고 이 방식으로 빈들을 전부 선언해주는 것은 아닌 것 같다.
컴포넌트 스캔의 등장 배경에 반한다 -> 엄청난 설정 파일.. (모든 빈들..)
그러나 직접 빈으로 등록하는 경우나

@Bean
public MyService myService(){
  return new MyService();
}

해당 패키지가 아닌 경로의 사용 등(MyService) 이런 식으로 등록하게 되는 빈들의 경우 사용하는 방법도 나쁘진 않다.

컴포넌트 스캔의 동작 시점

컴포넌트 스캔은
BeanFactoryPostProcessor를 구현한 ConfigurationClassPostProcessor와 연결되어 있다.

BeanPostProcessor와 매우 비슷한데 실행 시점이 다르다.

(컴포넌트 스캔으로 등록되는 빈들이 아닌) 다른 모든 빈들을 만들기 이전에 BeanFactoryPostProcessor의 구현체들을 적용을 해준다.
= 직접 빈으로 등록하는 다른 빈들을 모두 등록하기 전에 컴포넌트 스캔을 해서 빈으로 등록을 해 준다.
(ex. java @bean, xml, registerBean 등.. 컴포넌트 스캔X 아닌 빈들)

즉, 컴포넌트 스캔에 해당하는 빈들이 먼저 등록된다. (설정파일 빈들이 등록보다 먼저)




© 2019. by jaeuk