본 글은 인프런 김영한님의 스프링 완전 정복 로드맵을 기반으로 정리했습니다.
현재까지는 @Bean
애노테이션으로 빈 객체를 수동으로 등록했다. 실제 프로젝트에서는 빈으로 등록되어야 할 객체가 많기 때문에 이를 일일히 등록하다가 누락하는 등의 문제가 발생할 수 있다. 설정파일 길이도 길어지고 반복작업도 심해진다. 이때 수동 등록 대신 사용할 수 있는것이 컴포넌트 스캔이라는 빈 자동 등록 방법이다.
@Configuration
@ComponentScan
public class AutoAppConfig {
}
@Component
public class CreateOrderServiceImpl {
}
@Component
public class MemberRepositoryImpl {
}
설정정보 클래스에 @ComponentScan
애노테이션을 붙이면 @Bean
애노테이션이 붙은 메서드를 정의해주지 않아도 자동으로 스프링이 @Component
애노테이션이 붙은 클래스의 객체들을 빈으로 등록해준다.
수동 등록에선 메서드명을 빈의 이름으로 사용했다. 컴포넌트 스캔 방식은 클래스 이름의 앞글자를 소문자로 바꿔서 사용한다.
빈 이름 | 빈 객체 |
---|---|
createOrderServiceImpl | CreateOrderServiceImpl@x01 |
memberRepositoryImpl | MemberRepositoryImpl@x02 |
@Bean("customName")
처럼 기본값 대신 사용할 빈의 이름을 지정할 수 있는 것처럼 @Component("customName")
으로 빈의 이름을 직접 지정해줄 수 있다.
수동으로 빈을 등록할때는 아래처럼 의존관계를 주입해주면 된다.
public class AppConfig {
@Bean
public CreateOrderService createOrderService() {
return new CreateOrderServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemberRepositoryImpl();
}
}
컴포넌트 스캔으로 등록된 빈은 @Autowired
애노테이션을 통해서 필요한 의존 객체들을 자동 주입받는다. 의존관계 자동 주입에 대해선 다음 글에서 다루도록 하겠다.
@ComponentScan
애노테이션을 붙였다고 해서 프로젝트의 모든 클래스를 컴포넌트 스캔 하지는 않는다. 그러면 쓸데없이 외부 라이브러리까지 탐색해야 되므로 비효율적이다. 애노테이션에 아무런 옵션도 주지 않으면 해당 클래스가 정의된 패키지부터 그 하위 패키지까지 컴포넌트 스캔한다.
@Configuration
@ComponentScan(
basePackages = "hello.service"
)
public class AutoAppConfig {
}
옵션을 주면 해당 패키지부터 하위 패키지까지 모두 스캔한다. basePackages = {"hello.repository", "hello.service"}
처럼 배열 형식으로 시작 위치를 여러개 지정할 수 있다.
컴포넌트 스캔 설정 정보는 프로젝트의 핵심적인 정보이기 때문에 프로젝트 최상단에 두고 아무런 옵션도 주지 않는 것이 이상적이다. 스프링 부트도 이 방법을 사용한다.
스프링 부트 프로젝트엔 프로젝트 최상단에 스프링 애플리케이션을 시작하는 메인 메서드가 있는 클래스가 있다. 이 클래스는 @SpringBootApplication
애노테이션이 있는데 이 애노테이션은 @ComponentScan
애노테이션을 포함하고 있다. 참고로, 자바 애노테이션에는 상속 관계가 없지만 스프링에선 애노테이션이 들고 있는 애노테이션을 인식하는 기능을 지원한다.
컴포넌트 스캔에서 특정 대상을 제외하고 싶은 특수한 경우가 있다. 이 때 사용하는 것이 @ComponentScan
의 excludeFilters와 @Filter
애노테이션이다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyExcludeComponent {
}
@Configuration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,
classes = MyExcludeComponent.class)
)
public class AppConfigWithFilter {
}
type 속성에 FilterType.ANNOTATION
, classes 속성에 필터로 사용할 애노테이션 타입을 줬다. @MyExcludeComponent
애노테이션이 붙은 클래스는 컴포넌트 스캔 대상에서 제외될 것이다. 여러 값을 지정하고 싶으면 classes 속성을 배열 형태로 쓸 수 있다.
FilterType
에는 ANNOTATION을 포함해서 5가지 옵션을 제공한다.
TypeFilter
인터페이스를 구현해서 처리다음 애노테이션을 붙인 클래스는 컴포넌트 스캔 대상이 된다.
@Component
: 컴포넌트 스캔 대상으로 지정된다.@Controller
: 스프링 MVC 패턴에서 Controller에 해당된다.@Service
: 스프링 비즈니스 로직 처리에 해당된다.@Repository
: 스프링 데이터 접근 계층에 해당된다. 데이터베이스 예외를 스프링 예외로 변환해준다.@Aspect
: 스프링 AOP 처리에 해당된다.@Configuration
: 스프링 설정 정보에 해당된다.타입이 다르고 이름만 같은 클래스가 모두 컴포넌트 스캔의 대상이 될 수 있다. 이 상태로 스프링 애플리케이션을 시작하면 ConflictingBeanDefinitionException
이 터진다. 이 문제는 @Component
애노테이션을 통해 이름을 바꿔주면 해결할 수 있다.
@Component
public class MemberRepository {
}
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemberRepository();
}
}
수동 등록한 빈과 컴포넌트 스캔이 된 빈이 충돌하는 경우가 있다. 이 경우 수동 등록한 빈이 컴포넌트 스캔이 된 빈을 덮어쓴다. 즉, MemberRepository 타입의 빈을 두 개 등록했지만 최종적으로 컨테이너에 한 개만 존재하게 된다. 그러나, 이렇게 컴포넌트 스캔이 된 빈이 덮어씌워져서 사라지는 것은 의도하지 않은 경우가 대부분이며 잡기 어려운 버그의 원인이 될 수 있다.
그래서 스프링 부트 최신 버전에서는 수동 등록과 컴포넌트 스캔이 충돌할 경우 애플리케이션 로딩에 오류를 발생시키도록 기본값을 바꿨다. application.properties
파일에 spring.main.allow-bean-definition-overriding=true
옵션을 줘서 기본값을 덮어쓰는 행위를 허용할 수 있지만 권장되는 방법은 아니다.
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository2() {
return new MemberRepository();
}
}
위와 같이 기본이름 대신 다른 이름을 지정해주면 같은 타입의 빈이 충돌 없이 두 개 존재하게 된다. 이 경우 MemberRepository 타입만으로는 빈을 구별할 수 없으므로 @Qualifier
애노테이션으로 알맞은 빈을 지정해줘야 한다. @Qualifier
애노테이션에 관해선 의존관계 자동주입에서 다루도록 하겠다.