@ComponentScan — 스프링은 빈을 어떻게 자동으로 찾아낼까
스프링은 수백 개의 클래스 중에서 어떤 것이 빈이고 어떤 것이 아닌지를 어떻게 알아낼까요? 일일이 등록하지 않아도 자동으로 찾아내는 원리는 무엇일까요?
개념 정의
@ComponentScan은 지정된 패키지 범위에서 @Component(및 그 파생 어노테이션)가 붙은 클래스를 자동으로 찾아 빈으로 등록하는 메커니즘입니다. XML 시절의 수동 빈 등록을 대체하는 핵심 기능입니다.
왜 필요한가
빈을 일일이 수동으로 등록하면 이런 일이 벌어집니다.
@Configuration
public class AppConfig {
@Bean public UserService userService() { return new UserService(userRepository()); }
@Bean public UserRepository userRepository() { return new UserRepository(); }
@Bean public OrderService orderService() { return new OrderService(orderRepository()); }
@Bean public OrderRepository orderRepository() { return new OrderRepository(); }
// ... 수십~수백 개의 빈을 수동으로 등록
}
클래스가 늘어날 때마다 설정 파일도 수정해야 합니다. @ComponentScan은 이 문제를 해결합니다. 클래스에 @Component만 붙이면 자동으로 빈이 됩니다.
내부 동작
스캐닝 과정
1. @ComponentScan의 basePackages 확인
(미지정 시 → 현재 클래스의 패키지)
2. 해당 패키지와 하위 패키지의 모든 .class 파일 탐색
3. 각 클래스의 어노테이션 검사
(@Component, @Service, @Repository, @Controller 등)
4. 필터 조건 확인 (include/exclude)
5. 통과한 클래스를 BeanDefinition으로 등록
6. 빈 이름 결정 (클래스명의 camelCase)
스테레오타입 어노테이션
@Component // 범용 컴포넌트
├── @Service // 비즈니스 로직 계층
├── @Repository // 데이터 접근 계층 (+ 예외 변환)
├── @Controller // 웹 MVC 컨트롤러
│ └── @RestController // @Controller + @ResponseBody
└── @Configuration // 설정 클래스 (+ CGLIB 프록시)
이들은 모두 @Component를 메타 어노테이션으로 포함하고 있습니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component // ← @Service 안에 @Component가 있음
public @interface Service {
String value() default "";
}
@Repository의 특별한 기능
@Repository
public class UserRepository {
public User findById(Long id) {
// JDBC에서 SQLException이 발생하면
// → 스프링의 DataAccessException으로 자동 변환
}
}
@Repository는 @Component의 기능에 더해 PersistenceExceptionTranslationPostProcessor가 데이터 접근 예외를 스프링의 통합 예외 계층으로 변환해줍니다. @Service나 @Component는 이 기능이 없습니다.
basePackages와 basePackageClasses
// 문자열로 패키지 지정 (오타 위험)
@ComponentScan(basePackages = "com.example.app")
// 클래스 기반으로 패키지 지정 (타입 안전)
@ComponentScan(basePackageClasses = AppConfig.class)
// 여러 패키지 지정
@ComponentScan(basePackages = {"com.example.service", "com.example.repository"})
basePackageClasses를 사용하면 리팩토링 시 패키지 이름이 변경되어도 컴파일러가 잡아줍니다.
코드 예제
기본 사용법
@Configuration
@ComponentScan("com.example.app")
public class AppConfig {
// @Bean 메서드 없이도 com.example.app 하위의 모든 컴포넌트가 빈으로 등록됨
}
@SpringBootApplication과의 관계
@SpringBootApplication // 내부에 @ComponentScan 포함
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@SpringBootApplication은 @ComponentScan을 포함하므로, 메인 클래스의 패키지가 자동으로 basePackage가 됩니다. 이것이 메인 클래스를 루트 패키지에 두라고 권장하는 이유입니다.
com.example.app
├── MyApplication.java ← 여기에 @SpringBootApplication
├── controller/
│ └── UserController.java ← 스캔됨
├── service/
│ └── UserService.java ← 스캔됨
└── repository/
└── UserRepository.java ← 스캔됨
필터 사용
@Configuration
@ComponentScan(
basePackages = "com.example",
// 특정 어노테이션이 붙은 클래스만 포함
includeFilters = @ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = MyCustomAnnotation.class
),
// 특정 패턴의 클래스를 제외
excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "com\\.example\\.legacy\\..*"
)
)
public class AppConfig {}
FilterType 종류
// 어노테이션 기반
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Deprecated.class)
// 타입(클래스) 기반
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = LegacyService.class)
// 정규식 기반
@ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Stub.*")
// AspectJ 패턴
@ComponentScan.Filter(type = FilterType.ASPECTJ, pattern = "com.example..*Service+")
// 커스텀 필터
@ComponentScan.Filter(type = FilterType.CUSTOM, classes = MyTypeFilter.class)
커스텀 필터 구현
public class MyTypeFilter implements TypeFilter {
@Override
public boolean match(MetadataReader metadataReader,
MetadataReaderFactory metadataReaderFactory) {
// 클래스 메타데이터를 기반으로 포함 여부 결정
String className = metadataReader.getClassMetadata().getClassName();
return className.contains("Special");
}
}
빈 이름 충돌 처리
// com.example.service 패키지
@Service
public class UserService { }
// com.example.admin 패키지 (다른 패키지에 같은 이름)
@Service
public class UserService { }
같은 이름의 빈이 두 개 등록되면 ConflictingBeanDefinitionException이 발생합니다. 해결 방법은 다음과 같습니다.
// 방법 1: 빈 이름 명시
@Service("adminUserService")
public class UserService { }
// 방법 2: 하나를 @Primary로 지정
@Primary
@Service
public class UserService { }
컴포넌트 스캔이 실제로 빈을 찾는 과정 디버깅
# application.yml
logging:
level:
org.springframework.context.annotation: DEBUG
이 로그 레벨을 설정하면 어떤 클래스가 스캔되어 빈으로 등록되는지 로그에서 확인할 수 있습니다.
수동 등록 vs 자동 등록 우선순위
@Component
public class MyService {
// 자동 등록
}
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new CustomMyService(); // 수동 등록
}
}
스프링부트에서는 수동 등록(@Bean)이 자동 등록(@Component)을 오버라이드합니다. 다만 스프링부트 2.1부터는 기본적으로 이 오버라이드가 금지되어 에러가 발생합니다. spring.main.allow-bean-definition-overriding=true로 허용할 수 있습니다.
정리
@ComponentScan은 지정된 패키지에서@Component계열 어노테이션이 붙은 클래스를 자동으로 빈으로 등록합니다@SpringBootApplication에@ComponentScan이 포함되어 있으므로, 메인 클래스를 루트 패키지에 두면 별도 설정 없이 전체 패키지가 스캔됩니다@Service,@Repository,@Controller는@Component의 특수화이며,@Repository만 예외 변환 기능이 추가됩니다includeFilters와excludeFilters로 스캔 범위를 세밀하게 제어할 수 있습니다- 같은 이름의 빈이 충돌하면 명시적 이름 지정이나
@Primary로 해결합니다