Programming/Spring

[Spring] Component Scan과 Function을 사용한 빈 등록 방법

byeong07 2022. 5. 29. 23:11

1. @ComponentScan

@ComponentScan 애노테이션은 spring 3.1부터 도입됐으며 설정된 시작 지점부터 컴포넌트 클래스를 scanning하여 빈으로 등록해주는 역할을 한다.

컴포넌트 클래스는 다음 애노테이션이 붙은 클래스를 의미한다.

 

  • @Component
  • @Repository
  • @Service
  • @Controller
  • @Configuration

 

@ComponentScan의 가장 중요한 두 가지 속성은 component를 scan할 시작 지점을 설정하는 속성과 scan한 component 중 빈으로 등록하지 않을 클래스를 제외하는 필터 속성이다.

 

1) Scan 시작 지점 설정 - basePackages()와 basePackageClasses()

basePackages()와 basePackageClasses()는 component를 scan할 시작 지점을 설정하는 속성이다.

basePackages()에는 scan을 시작할 패키지를 문자열로 지정하고 basePackageClasses()에는 scan을 시작할 클래스 타입을 지정한다.

둘 중 하나의 속성을 택하여 scan 시작 지점을 설정하면 된다.

 

basePackages 설정 예

위와 같이 설정하면 scan 시작 패키지가 com.atoz_develop.demospring51로 설정된다.

com.atoz_develop.demospring51 패키지와 포함된 하위 패키지의 모든 component를 scan해서 빈으로 등록해준다.

 

basePackageClasses 설정 예

위와 같이 설정하면 마찬가지로 scan 시작 패키지가 com.atoz_develop.demospring51로 설정된다.

TestConfiguration 클래스가 포함된 패키지가 시작 패키지가 된다.

 

둘 중 basePackageClasses()로 설정하는 것이 더 type safe한 방법이다.

 

basePackages()나 basePackageClasses()를 설정해주지 않으면 기본적으로 @ComponentScan이 붙어있는 configuration 클래스가 자동으로 시작 지점이 된다.

 

따라서 위 세 가지 예제는 모두 scan 시작 지점이 동일하며 out 패키지에 있는 클래스는 scan 대상이 아니게 된다.

 

2) 빈 필터링 - excludeFilters

Component scan을 통해 빈을 등록할 때 execludeFilters 속성을 사용해서 특정 빈은 등록되지 않도록 설정할 수 있다. 설정 방법은 다양하며 다음은 애노테이션을 이용해서 필터링하는 방법이다.

 

IgnoreDuringScan 애노테이션을 새로 정의한다.

이 애노테이션이 붙은 컴포넌트는 필터링하여 빈으로 등록하지 않을 것이다.

 

1
2
3
4
5
6
7
8
9
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreDuringScan {
}
cs

 

필터링할 컴포넌트 클래스에 @IgnoreDuringScan 애노테이션을 붙인다.

1
2
3
4
5
6
import org.springframework.stereotype.Service;
 
@Service
@IgnoreDuringScan
public class AnotherBookService {
}
cs

 

@Service 애노테이션이 붙어있으므로 기본적으로 필터링 설정이 없으면 이 클래스는 빈으로 등록된다.

 

Configuration에 필터 설정을 추가한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
 
@Configuration
@ComponentScan(
        basePackageClasses = ApplicationConfig.class,
        excludeFilters = @Filter(
                type = FilterType.ANNOTATION,
                classes = {IgnoreDuringScan.class}
        )
)
public class ApplicationConfig {
}
cs

excludeFilters 속성을 사용해 @IgnoreDuringScan 애노테이션이 붙어있는 컴포넌트 클래스는 빈으로 등록하지 않도록 한다.

 

설정한 대로 AnotherBookService가 빈으로 등록되지 않았는지 확인해보기 위해 Application 클래스에서 출력해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import java.util.Arrays;
 
public class Demospring52Application {
 
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        System.out.println(context.containsBean("anotherBookService"));
    }
}
cs

ApplicationContext의 containsBean()은 지정한 빈이 IoC에 등록돼있는지 여부를 리턴한다.

 

실행 결과


2. Function을 사용한 빈 등록 방법

Function을 사용해서 빈을 등록하는 방법은 spring 5부터 추가되었다.

빈 등록 방법은 다음과 같다.

 

Spring boot 어플리케이션을 구동하는 방법에는 SpringApplication.run()과 같이 static 메소드를 호출하는 방법 외에 인스턴스를 생성해서 구동하는 방법이 있다.

Application 클래스를 다음과 같이 수정한다.

Local 변수 var를 사용하기 위해서는 JDK 10 이상이 필요하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Demospring52Application {
 
    public static void main(String[] args) {
//        SpringApplication.run(Demospring52Application.class, args);
        var app = new SpringApplication(Demospring52Application.class);
        // 여기에 빈 등록 코드 작성
        app.run(args);
    }
}
cs

기존에 run() 호출부를 주석처리하고 SpringApplication 인스턴스를 생성해서 run()을 호출한다.

SpringApplication 인스턴스 생성과 run() 호출 사이에 빈을 등록하는 코드를 다음과 같이 작성해주면 된다.

 

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
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.support.GenericApplicationContext;
import out.MyService;
 
@SpringBootApplication
public class Demospring52Application {
 
    @Autowired
    MyService myService;
 
    public static void main(String[] args) {
//        SpringApplication.run(Demospring52Application.class, args);
        var app = new SpringApplication(Demospring52Application.class);
 
        app.addInitializers(new ApplicationContextInitializer<GenericApplicationContext>() {    // GenericApplicationContext로 설정
            @Override
            public void initialize(GenericApplicationContext ctx) {
                ctx.registerBean(MyService.class);
            }
        });
 
        app.run(args);
    }
}
cs

ctx.registerBean()이 빈을 등록하는 부분이다.

@ComponentScan을 사용하는 방법과 달리 function을 사용한 빈 등록 방법은 스캔 범위에 구애받지 않는다.

 

MyService는 Application 클래스와 다른 패키지에 있으며 @Component 애노테이션이 붙어있지 않은 클래스이다.

 

이 클래스를 ctx.register()를 이용해 빈으로 등록하고 정상적으로 등록되는지 확인해보기 위해 @Autowired로 필드에 주입받았다.

 

실행 결과

위와 같이 어플리케이션이 정상적으로 실행되는 것으로 빈이 잘 등록된 것을 확인할 수 있다.

 

이 코드는 람다식을 이용해서 짧게 표현할 수 있다.

 

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
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.support.GenericApplicationContext;
import out.MyService;
 
@SpringBootApplication
public class Demospring52Application {
 
    @Autowired
    MyService myService;
 
    public static void main(String[] args) {
        var app = new SpringApplication(Demospring52Application.class);
 
        /*app.addInitializers(new ApplicationContextInitializer<GenericApplicationContext>() {    // GenericApplicationContext로 설정
            @Override
            public void initialize(GenericApplicationContext ctx) {
                ctx.registerBean(MyService.class);
            }
        });*/
 
        // Lambda ~
        app.addInitializers((ApplicationContextInitializer<GenericApplicationContext>) ctx -> ctx.registerBean(MyService.class));
 
        app.run(args);
    }
}
cs

 

참고로 registerBean()을 이용해서 다음과 같이 ApplicationRunner를 빈으로 등록할수도 있다.

 

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
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.support.GenericApplicationContext;
import out.MyService;
 
import java.util.function.Supplier;
 
@SpringBootApplication
public class Demospring52Application {
 
    @Autowired
    MyService myService;
 
    public static void main(String[] args) {
        var app = new SpringApplication(Demospring52Application.class);
 
        app.addInitializers((ApplicationContextInitializer<GenericApplicationContext>) ctx -> {
            ctx.registerBean(MyService.class);
 
            // 메시지를 출력하는 ApplicationRunner를 생성해서 빈으로 등록
            ctx.registerBean(ApplicationRunner.classnew Supplier<ApplicationRunner>() {
                @Override
                public ApplicationRunner get() {
                    return new ApplicationRunner() {
                        @Override
                        public void run(ApplicationArguments args) throws Exception {
                            System.out.println("Hello World!!");
                        }
                    };
                }
            });
        });
 
        app.run(args);
    }
}
cs

registerBean()의 첫 번째 파라미터로 ApplicationRunner의 타입을 전달하고 함수형 인터페이스 Supplier를 이용해서 두 번째 파라미터로 ApplicationRunner 타입의 인스턴스를 제공해준다.

간단하게 "Hello World!!"를 출력하는 ApplicationRunner를 만들어서 빈으로 등록하는 코드이다.

 

실행 결과

 

마찬가지로 lambda식으로 짧게 표현할 수 있다.

 

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
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.support.GenericApplicationContext;
import out.MyService;
 
@SpringBootApplication
public class Demospring52Application {
 
    @Autowired
    MyService myService;
 
    public static void main(String[] args) {
        var app = new SpringApplication(Demospring52Application.class);
 
        app.addInitializers((ApplicationContextInitializer<GenericApplicationContext>) ctx -> {
            ctx.registerBean(MyService.class);
 
            // 메시지를 출력하는 ApplicationRunner를 생성해서 빈으로 등록
            ctx.registerBean(ApplicationRunner.class, () -> args1 -> System.out.println("Hello World!!"));
        });
 
        app.run(args);
    }
}
cs

3. Component Scanning VS Functional

@ComponentScan 방식은 싱글톤 scope인 빈들을 모두 초기에 생성한다.

따라서 등록해야할 빈이 많을 경우 어플리케이션의 초기 구동시간이 오래 걸릴 수 있다는 단점이 있다.

 

Function을 사용한 빈 등록 방법은 @ComponentScan을 사용하는 방법보다 어플리케이션 초기 구동 시간이 더 짧다. 

또 다른 장점은 조건에 따라 빈을 등록한다던지 하는 로직을 추가할 수 있다는 것이다.

즉 빈을 등록하는데 있어서 프로그래밍적인 컨트롤이 가능해진다.

 

두 방법 중 하나를 반드시 택해야 하는건 아니고 둘 다 동시에 사용할 수 있다.

이 경우 @ComponentScan 방식의 빈이 먼저 등록된다.

@ComponentScan은 BeanFactoryPostProcessor를 구현한 ConfigurationPostProcessor와 연관이 돼있다.

BeanFactoryPostProcessor의 구현체들은 다른 모든 빈들을 만들기 이전에 적용된다.

즉 다른 빈들을 등록하기 전에 @ComponentScan으로 빈을 먼저 등록한다.

여기서 다른 빈들이란 @Bean 또는 function을 사용해서 직접 등록한 빈을 말한다.

 

Functional한 빈 등록은 모든 빈을 직접 등록해줘야 하기 때문에 번거롭고 코드가 복잡해진다.

따라서 @ComponentScan 방식을 완전히 대체해서 사용하기 보다는 @ComponentScan으로 등록하는 빈 외에 @Bean으로 직접 등록해야하는 빈이 있을 경우 functional한 방법을 사용하는 것을 고려해보는게 좋다.