BackEnd/Spring

컴포넌트 스캔(ComponentScan)

PgmJUN 2024. 4. 12. 00:56

 

이전 글을 통해 @Configuration 애노테이션을 통한 스프링 빈 등록에 대해 알아보았다.

 

[Spring] @Configuration과 싱글톤 (with. CGLIB)

@Configuration public class AppConfig { @Bean public MemberService memberService() { return new MemberServiceImpl(memberRepository()); } @Bean public OrderService orderService() { return new OrderServiceImpl(memberRepository(), discountPolicy()); } @Bean p

pgmjun.tistory.com

 

하지만 꼭 빈을 등록할 때, @Configuration, @Bean 애노테이션을 선언해야만 할까?

 

그렇지 않다.

우리는 스프링이 지원하는 컴포넌트 스캔이라는 기능을 통해 아주 간편하게 스프링 컨테이너에 빈 등록이 가능하다.

 

컴포넌트 스캔 (Component Scan) 이란?

컴포넌트 스캔은 스캔 범위 내에 존재하는 @Component 애노테이션이 붙은 클래스를 스프링 빈으로 등록하기 위한 스캔 과정이다.

컴포넌트 스캔은 @ComponentScan 애노테이션을 통해 설정이 가능하다.

 

@SpringBootApplication

 

스프링부트 프로젝트 생성 시, Main 클래스에 설정되는 @SpringBootApplication 애노테이션은
내부에 @ComponentScan 애노테이션을 기본으로 가지고 있다.

 

덕분에 우리는 따로 @ComponentScan 애노테이션을 선언하지 않고도 @Component 애노테이션이 붙어있는 클래스들을 스프링 컨테이너에 빈으로 등록할 수 있던 것이다.

 

 

컴포넌트 스캔이 작동하는 애노테이션 종류

컴포넌트 스캔은 @Component 애노테이션이 붙어있는 클래스에만 해당되는 이야기라고 하였다.

하지만 @Component 애노테이션 외에도 몇 가지 애노테이션도 컴포넌트 스캔 범위에 해당된다.

 

그 종류는 바로 @Controller, @RestController, @Service, @Configuration, @Repository 이다.

이 애노테이션들은 왜 컴포넌트 스캔에 포함될까?

 

 

@Repository 살펴보기

 

예시로 사진을 가져와본 Repository 애노테이션의 코드를 보면 알 수 있듯이
해당 애노테이션들은 내부에 @Component 애노테이션을 포함하고 있다.

때문에 컴포넌트 스캔 범위에 해당이 되는 것이며, 우리는 이들을 스프링 컨테이너의 가호 아래에서 싱글톤으로 사용할 수 있는 것이다.

 

 


 

컴포넌트 스캔의 탐색 범위

 

그렇다면 컴포넌트 스캔의 탐색 범위는 어떻게 될까?

대규모 프로젝트라면 컴포넌트 스캔을 위해 모든 자바 클래스를 탐색하는 것은 비용 낭비일 것이다.

 

 

 컴포넌트 스캔 basePackage 설정

@Configuration
@ComponentScan(
        basePackages = "com.example"
)
public class AutoAppConfig {
}

 

컴포넌트 스캔은 위와 같이 basePackage 설정을 통해 컴포넌트 스캔의 기반 패키지를 설정할 수 있다.

basePackage를 설정하게 되면, 설정된 패키지와 그 하위의 패키지의 클래스들을 모두 탐색하며 컴포넌트 스캔을 수행한다.

 

이를 통해 프로젝트의 모든 클래스를 찾지 않고 필요한 부분만 컴포넌트 스캔을 수행할 수 있다.

 

 

@SpringBootApplication의 basePackage 설정

@SpringBootApplication(
       scanBasePackages = ""
)

 

SpringBootApplication 또한 내부에 @ComponentScan 애노테이션을 가지고 있기 때문에,
scanBasePackages 이라는 속성을 통하여 컴포넌트 스캔 범위를 설정할 수 있도록 되어있다.

 

 

컴포넌트 스캔의 Default basePackage

만약 위와 같이 컴포넌트 스캔 범위를 지정해주지 않는다면, @ComponentScan 애노테이션을 선언해준 클래스의 패키지를 basePackage로 설정한다.

때문에 해당 클래스가 존재하는 패키지를 포함하여, 그 하위의 패키지에 있는 클래스들에 대해서 컴포넌트 스캔이 이루어진다.

 

스프링 신 김영한 선생님의 권장 방식

김영한님께서 개인적으로 즐겨 사용하는 방법은 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것 이라고 한다.

 

 

basePackageClasses 옵션

@Configuration
@ComponentScan(
        basePackageClasses = {
                AutoAppConfig.class
        }
)
public class AutoAppConfig {
}

 

basePackages 외에 basePackageClasses 라는 속성도 존재하는데,
이 속성은 특정 클래스를 설정하면 그 클래스가 위치한 패키지와 그 하위 패키지에 대해 컴포넌트 스캔을 수행하여 빈을 등록한다.

 

 


 

컴포넌트 스캔 탐색 제외(exclude) / 포함(include) 속성

컴포넌트 스캔은 basePackages와 basePackageClasses 속성 외에도


특정 부분에 대해 컴포넌트 스캔을 제외(exclude) 시킬 수 있는 excludeFilters와
특정 부분에 대해 컴포넌트 스캔을 포함(include)시킬 수 있는 includeFilters 속성이 존재한다.

 

 

 includeFilters

먼저 includeFilters에 대해 살펴보자

 

@Configuration
@ComponentScan(
        includeFilters = {
                @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
        }
)
public class AutoAppConfig {
}

 

includeFilters 속성을 @ComponentScan 애노테이션의 속성으로 추가하게 되면,
해당 속성에 정의한 type, classes를 바탕으로 컴포넌트 스캔에서 포함(include) 시킬 클래스를 정의할 수 있다.

 

위 코드와 같이 includeFilters 옵션을 사용하게 되면,
컴포넌트 스캔 범위에 해당하는 클래스 + @Configuration 애노테이션이 붙은 클래스를 스프링 빈으로 등록하게 된다.

 

 

 excludeFilters

@Configuration
@ComponentScan(
        excludeFilters = {
                @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
        }
)
public class AutoAppConfig {
}

 

excludeFilters 속성을 @ComponentScan 애노테이션의 속성으로 추가하게 되면,
해당 속성에 정의한 type, classes를 바탕으로 컴포넌트 스캔에서 제외(exclude) 시킬 클래스를 정의할 수 있다.

 

위 코드와 같이 excludeFilters 옵션을 사용하게 되면,
컴포넌트 스캔 범위에 해당하는 클래스를 탐색하되, @Configuration 애노테이션이 붙은 클래스를 발견하면 스캔에서 제외하고
이외의 것들만 스프링 빈으로 등록한다.

 

 

FilterType의 종류

FilterType이 Enum으로 되어있기에, 어떤 종류가 있는 지 궁금해서 정리해보았다.

 

 

FilterType Enum 클래스는 다음과 같은 열거 상수들을 가지고 있다.

 

  • ANNOTATION : 지정된 애노테이션을 가지고 있는 클래스를 탐색
  • ASSIGNABLE_TYPE : 지정된 클래스 또는 그 클래스를 상속하는 클래스들을 탐색
  • ASPECTJ : AspectJ 패턴을 기반으로 빈을 탐색
  • REGEX : 정규 표현식을 사용하여, 정규 표현식 패턴에 일치하는 이름의 클래스들을 탐색
  • CUSTOM : 사용자 정의 필터 로직을 구현하여 원하는 방식으로 빈을 선택적으로 탐색
    (TypeFilter 인터페이스를 구현해서 처리)

 

 


 

컴포넌트 스캔 중복

컴포넌트 스캔 시에, 동일한 이름의 빈 등록이 발생하면 어떻게 될까?

 

1. 자동 빈 등록 vs 자동 빈 등록

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 만약 같은 이름으로 설정한 빈이 존재하면
스프링은 ConflictingBeanDefinitionException 예외를 발생시킨다.

 

2. 자동 빈 등록 vs 수동 빈 등록

그렇다면 만약 자동 빈 등록과 수동 빈 등록을 함께 사용할 때 빈 이름이 겹치면 어떻게 될까?

이전 스프링은 수동 빈이 자동 빈 보다 우선권을 가지도록 하였었다.

 

하지만 지금의 스프링은 자동 빈 vs 자동 빈 등록 케이스와 마찬가지로 예외를 발생시킨다.

 

 

@Configuration
@ComponentScan(
        excludeFilters = {
                @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
        }
)
public class AutoAppConfig {

    @Bean(name = "memoryMemberRepository")
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

 

다음과 같이 memoryMemberRepository를 @Configuration 클래스 내부에서 Bean으로 등록하고

 

@Repository
public class MemoryMemberRepository implements MemberRepository {
}

 

이와 동일한 MemoryMemberRepository를 @Repository 애노테이션을 통해 컴포넌트 스캔 범위로 잡아 스프링 빈으로 등록한다.

Bean의 기본 네이밍 패턴은 클래스명에서 맨 앞자만 소문자로 변경하는 형태이다.

때문에 위 Config에 수동으로 등록한 빈의 이름("memoryMemberRepository")과 동일함을 인지할 수 있다.

 

 

과거 스프링은 이러한 경우에
아래와 같은 로그를 출력하고 수동 빈 등록이 우선권을 가지게 하였다.
(수동 빈이 자동 빈을 오버라이드 해버렸다.)

Overriding bean definition for bean 'memoryMemberRepository' with a different
definition: replacing

 

 

하지만 현재 스프링은 스프링 빈의 이름이 같다면 예외를 발생시킨다.

 

그 이유는 대부분의 빈 네임 중복은 개발자의 의도가 아닌 휴먼 에러에 의해서 발생했던 것이었는데,
이로 인하여 나도 모르는 사이에 빈 네임 중복으로 인해 스프링 빈이 2개 중 1개만 생성이 되는 등 스프링 빈을 다루는 데에 문제점을 느끼게 되었다고 한다.

 

 

빈 네이밍 중복 시, 오버라이딩 시키기

Consider renaming one of the beans or enabling overriding by setting
spring.main.allow-bean-definition-overriding=true


때문에 빈 등록이 자동이건 수동이건 이름이 중복되면 위와 같은 메시지를 출력하고 무조건 예외를 발생시키도록 조치했다고 한다.

 

이때 예외 메시지를 잘 살펴보면 `spring.main.allow-bean-definition-overriding` 옵션을 `true`로 설정하면 Bean 오버라이딩이 가능해질 것이라고 한다.

 

실제로 해당 옵션을 yaml 또는 properties 파일에 추가해주면

수동 빈 등록 vs 자동 빈 등록 상황에서 빈 네이밍이 중복되어도 예외가 발생하지 않고
수동 빈이 자동 빈을 오버라이드 하도록 설정이 가능하다.