들어가며

  • 이전 포스트에서 자동으로 클래스를 스프링 빈으로 등록해주는 컴포넌스 스캔에 대하여 알아보았다.
  • 이번 포스트에서는 컴포넌트 스캔의 범위 지정과 컴포넌트 스캔 대상, 필터와 중복된 컴포넌트가 존재할 경우에 대해 다루어 볼 것이다.
 

Spring Framework - 컴포넌트 스캔 - 1. 컴포넌트 스캔과 의존관계 자동 주입

들어가며 이번 포스트부터는 스프링 프레임워크가 @Bean으로 직접 수동으로 스프링 빈을 등록하지 않는 상태에서도 어떻게 객체들을 스프링 빈으로 등록하는지와 의존관계를 자동 주입하는 방

sehun5515.tistory.com

 

 

 

컴포넌트 스캔의 시작 위치

  • 스프링 프레임워크도 결국 JVM위에서 돌아가는 일종의 자바 소스 코드의 집합에 불과하단 사실을 인지하고 있어야 한다. 이 말은 결국 컴포넌트 스캔을 위해 어떤 클래스파일이 @Component 어노테이션을 가지고 있는지는 결국 모두 뒤져보아야 한다는 것이다.
  • 물론 우리가 브루트포스 알고리즘등을 통해 알고 있는 사실이지만 모든 데이터를 전수조사하는 것은 많은 시간이 소요되기 때문에 별로 좋은 방식은 아니다.
  • 만약 어떤 자바 패키지에 대부분의 @Component 어노테이션이 존재한다면 해당 패키지부터 컴포넌트 스캔을 수행하면 꽤나 효율적으로 수행할 수 있을 것이다. 그래서 스프링 프레임워크는 컴포넌트 스캔의 시작 위치를 지정하는 기능을 제공한다.
@ComponentScan(
    basePackages = "hello.core",
)
@Configuration
public class AutoAppConfig{

}
  • 이전 포스트의 AutoAppConfig와 다른 점은 @ComponentScan 어노테이션에 basePackages라는 지정자가 들어간 것이다. 이 지정자를 통해 탐색할 패키지의 시작 위치를 지정할 수 있다.
  • 탐색은 해당 패키지를 포함해 그 하위에 존재하는 모든 패키지들도 자동으로 탐색한다.
  • 만약 복수의 위치에서 컴포넌트 스캔을 시작하고 싶은 경우에도 다음과 같이 적음으로서 가능하게 할 수 있다.
@ComponentScan(
    basePackages = {"hello.core", "hello.service"},
)
@Configuration
public class AutoAppConfig{

}
  • 만약 지정한 클래스의 패키지를 탐색 시작 위치로 지정하고 싶다면 basePackageClasses를 통해 결정할 수도 있다.
@ComponentScan(
    basePackageClasses = {
        something.class
    }
)
@Configuration

public class AutoAppConfig{

}
  • 물론 여러 개의 클래스들을 넣어 시작하도록 세팅할 수도 있다.
  • 만약 @ComponentScan에 어떤 설정 지정도 되어 있지 않다면 @ComponentScan 어노테이션이 붙은 클래스가 소속된 패키지가 탐색 시작 위치로 지정된다.

일반적인 탐색 시작 위치

  • 그렇다면 어디에서 시작하는게 일반적으로 가장 좋을까라는 질문이 생길 수 있다.
  • 우리가 어떤 프로젝트를 시작했다고 할 때 스프링 프레임워크를 사용한다면 결국 컴포넌트 스캔을 사용한 자동 빈 등록이던, @Bean을 사용하여 등록한 수동 빈 등록이던 일종의 Config Class를 사용하여 그곳에 넣을 것이다.
  • 그렇다면 사람들이 이 프로젝트를 볼 때 어떤 스프링 빈이 있고 의존관계 주입이 어떻게 어디서 일어나는지에 대해 알아보기 위해서는 Config Class를 보게될 것이다. 즉 이 프로젝트를 대표하는 정보가 된다.
    • 프로젝트를 대표하는 정보가 어느 구석에 박혀있는 것은 좋은 현상은 아닐 것이다. 이 프로젝트에 대해 확인하고 싶은데 설정 파일의 위치를 뒤지고 있다고 상상해보라
  • 따라서 이 정보는 프로젝트 시작 루트 위치에 두어야 할 것이다. 그렇다면 누군가가 이 프로젝트에 대해 정보를 얻기 위하여 설정 파일을 찾는 수고로움이 사라진다.
  • 그런데 이 설정 파일에 @ComponentScan이 붙고 어떠한 지정자도 붙지 않는다면 해당 설정 클래스가 소속된 패키지인 프로젝트 루트 패키지가 컴포넌트 스캔 시작점이 됨과 동시에, 하위의 모든 패키지를 스캔하여 놓치는 컴포넌트가 없도록 할 수 있을 것이다.
  • 즉 프로젝트 루트 패키지에 설정 클래스파일을 넣는게 좋을 것이다.
    • SpringBoot에서는 @SpringBootApplication이라는 대표 시작 정보 어노테이션이 루트 패키지에 두는 것이 관례인데 @SpringBootApplication어노테이션을 살펴보면 내부에 @Component가 내재되어 있다.

 

 

 

컴포넌트 스캔 기본 대상

  • 컴포넌트 스캔은 스캔할 대상으로 @Component 뿐 아니라 다음의 어노테이션이 붙은 클래스들도 같이 스캔 대상으로 인지한다.
    • @Controller - 스프링 MVC의 컨트롤러에 사용된다.
    • @Service - 스프링 MVC의 서비스에 사용된다. 서비스는 메인 비즈니스 로직이 들어가는 파트로 생각하면 좋다.
    • @Repository - 스프링 MVC의 데이터 접근 계층에 사용된다. DAO와 비슷한 역할을 한다고 생각하면 좋다.
    • @Configuration - 설정 정보에 붙는 어노테이션이다.
  • 각 어노테이션의 내부를 살펴 보면 모두 @Component를 가지고 있는 것을 알 수 있다.
    • 이것을 보고 @Controller 어노테이션이 @Component를 상속받는가 라는 생각을 할 수 있다. 하지만 어노테이션 상에서는 상속관계가 존재하지 않는다.
    • @Controller 등의 어노테이션이 @Component를 가지고 있는것을 인식할 수 있는 것은 자바 컴파일러에서 지원하는 것은 아니고, 스프링 프레임워크에서 지원하는 기능이다.
  • @Controller, @Service, @Repository의 경우 Spring MVC 파트에서 자주 사용하는 어노테이션으로 해당 파트에서 설명할 기회가 된다면 설명한다.

 

 

 

필터(Filter)

  • 만약 특정 컴포넌트를 부득이하게 빼거나, 특정 컴포넌트를 넣어야 하는 상황이 있을 수 있다.
  • 이때 사용하는 것이 필터로 @ComponentScan 내부에 지정자로 지정해줄 수 있다. 각각 includeFilters와 excludeFilters로 설정이 가능하다.
  • 여기서는 우리가 직접 컴포넌트 스캔에 추가할 어노테이션과 컴포넌트 스캔에서 배제할 어노테이션을 만들어 볼 것이다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent{

}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent{

}
  • 임의의 어노테이션을 만들었다. 이제 포함시킬 스프링 빈에 @MyIncludeComponent을, 배제시킬 스프링 빈에 @MyExcludeComponent를 붙일 것이다.
@MyIncludeComponent
public class BeanA{
    ...
}
@MyExcludeBean
public class BeanB{
    ...
}
  • 스프링 빈에 등록시킬 클래스와 등록시키지 않을 클래스를 완성했다. 이제 이런 클래스가 있을 때 컴포넌트 스캔의 필터를 어떻게 설정하면 되는지에 대해 코드로 살펴본다.
@ComponentScan(
    includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
    excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class),
)
@Configuration
public class ComponentFilterAppConfig{

}
  • @Filter 어노테이션에 존재하는 type = FilterType.ANNOTATION을 통해 어노테이션 타입의 필터에서 MyIncludeComponent 어노테이션이 붙은 클래스들을 포함시키겠다는 것을 나타낸다.
  • 동일하게 MyExcludeComponent 어노테이션이 붙은 클래스들은 배제시키겠다는 것을 나타낸다.

FilterType 옵션

  • FilterType은 5가지의 옵션이 존재한다.
    • FilterType.ANNOTATION : Default로 어노테이션을 인식하여 동작한다.
    • FilterType.ASSIGNABLE_TYPE : 지정한 타입과 그 자식 타입을 인식하여 동작한다.
    • FilterType.ASPECTJ : AspectJ 패턴을 사용한다.
    • FilterType.REGEX : 정규표현식 패턴을 인식하여 동작한다.
    • FilterType.CUSTOM : TypeFilter 인터페이스를 구현하여 그것을 사용해 처리한다.
  • 만약 위의 코드에서 BeanA도 동일하게 배제하고 싶다면 다음과 같은 코드를 통해 배제시킬 수 있다.
@ComponentScan(
    includeFilters = {
        @Filter(
            type = FilterType.ANNOTATION, 
            classes = MyIncludeComponent.class
        )
    },
    
    excludeFilters = {
        @Filter(
            type = FilterType.ANNOTATION, 
            classes = MyExcludeComponent.class
        ),
        @Filter(
            type = FilterType.ASSIGNABLE,
            classes = BeanA.class
        )
    }
)
@Configuration
public class ComponentFilterAppConfig{

}
  • 코드를 보면 FilterType.ASSIGNABLE을 통해 지정한 클래스와 그 자식 타입 클래스 전체를 배제시키는 것을 확인할 수 있다.

 

 

 

수동 빈 등록과 자동 빈 등록

  • 만약 수동 빈 등록 설정 파일인 AppConfiguration과 자동 빈 등록 설정 파일인 AutoAppConfiguration이 둘 다 존재하여 중복된 빈 이름이 등록되면 어떻게 될까?
  • 이는 수동 빈 등록이 우선권을 가져간다. 정확히는 자동 빈 등록이 실행되어 스프링 빈이 이미 존재하는 상황에서 수동 빈으로 빈 이름이 겹쳐질 경우, 이름이 겹치는 빈이 수동 빈 등록에서 해당 빈을 오버라이딩 해버린다.
  • 이는 일반적으로 개발자가 의도해서 만들어지기 보다는 설정의 꼬임이나 누락, 실수로 인하여 발생한다.
    • 사실 생각해보면 자동으로 등록해놓았는데 수동으로 다시 덮어씌운다고 가정한다면 그냥 미리 수동으로 넣고 자동에서 배제시키거나, 자동 빈 등록에서 수동 빈 등록에 있는 로직을 넣어주면 될 것이다.
  • 이런 경우에 찾기 어려운 에러나 버그가 발생할 수 있다. 분명 자동 빈에서 잘 처리되는 것을 확인하였는데도 동작이 이상하다는 것이기 때문에 원인을 쉽게 생각할 수 없는 종류의 에러일 것이다.
  • 이러한 상황을 미연에 회피하기 위해서 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록 파일이 존재하여 두 등록 파일에서 빈 이름이 충돌할 시 더 이상 진행하지 않고, 에러를 발생시키도록 세팅을 바꾸었다.
    • 세팅을 바꾼 것 뿐이므로 만약 정말로 위와 같이 수동으로 덮어씌워야 하는 일이 발생하는 곳에서는 세팅을 다시 덮어씌울 수 있도록 바꾸어 줄 수 있을 것이다.
복사했습니다!