[스프링 IoC 컨테이너와 빈]_04 @Component와 컴포넌트 스캔

4 minute read

@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가지

  1. Static 메서드를 사용한 방식

    @SpringBootApplication
    public class Demospring51Application {
        public static void main(String[] args) {
            SpringApplication.run(Demospring51Application.class, args);
        }
    }
    
  2. Builder를 사용

  3. 인스턴스를 만들어서 사용

※ 펑션을 이용해 빈을 등록 실습

  1. 인스턴스를 만들어서 사용하는 방식으로 스프링 부트 애플리케이션을 구동하여 해보자

    다음과 같이 애플리케이션을 실행할 수 있다.

     @SpringBootApplication
     public class Demospring51Application {
         public static void main(String[] args) {
             SpringApplication app = new SpringApplication(Demospring51Application.class);
             app.run(args);
         }
     }
    
  2. 중간에 동작을 추가하고 싶다면 addInitializers()를 추가해주면 된다.

    내가 원하는 ApplicationContext를 주입받을 수 있다. GenericApplicationContextregisterBean()으로 직접 주입 받는다.

    컴포넌트 스캔 범위 밖에 만들었던 클래스를 빈으로 등록해보자

    빈으로 등록했으므로 @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);
         }
    
     }
    

    ⇒ 애플리케이션을 실행해보면 오류가 나지 않는다. 즉, 빈이 성공적으로 주입되었다는 뜻이다.

  3. 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();
}
  1. 자바의 Reflection은 JVM에서 실행되는 애플리케이션의 런타임 동작을 검사하거나 수정할 수 있는 기능이 필요한 프로그램에서 사용됩니다. 쉽게 말하자면, 클래스의 구조를 개발자가 확인할 수 있고, 값을 가져오거나 메소드를 호출하는데 사용됩니다.