이번 포스팅에서는 스프링의 자동 빈 등록 기능인 @Component와 @ComponentScan에 대해 알아보려고 한다.
기존에는 @Bean 어노테이션을 통해 수동으로 빈을 등록했지만, 이번에는 스프링이 자동으로 빈을 등록해주는 방법을 살펴보도록 하자 👀
@Component는 스프링이 클래스를 검색해서 빈으로 등록해주는 어노테이션이다.
즉, 클래스를 생성하고 @Component 어노테이션만 붙여주면 자동으로 빈으로 등록되기 때문에 설정 관련 코드를 크게 줄일 수 있다.
간단한 예제 코드를 통해 살펴보자
Sample Code
@Component public class MemberDAO { private static long nextId = 0; private Map<String, Member> map = new HashMap<>(); ... }
이렇게 클래스에 @Component를 붙이면 스프링이 구동될 때 자동으로 빈으로 등록된다.
빈의 이름은 별도로 지정하지 않으면 클래스 이름의 첫 글자를 소문자로 바꾼 이름을 사용한다는걸 기억하자!
물론 다음과 같이 빈의 이름을 직접 지정할 수도 있다.
Sample Code
@Component("infoPrinter") public class MemberInfoPrinter { @Autowired private MemberDAO memberDAO; ... }
필자는 포스팅을 하면서 문득 @Bean과 @Component의 차이점이 궁금해졌다.
이 차이점을 한번 정리해보자
구분 | @Bean | @Component |
---|---|---|
사용 위치 | 메서드 레벨에서 사용, @Configuration 클래스 내부에서 사용 | 클래스 레벨에서 사용 |
사용 목적 | 개발자가 제어할 수 없는 외부 라이브러리 등을 빈으로 등록할 때 사용 | 개발자가 직접 작성한 클래스를 빈으로 등록할 때 사용 |
등록 방식 | 수동으로 빈을 등록, 메서드에서 리턴하는 객체를 빈으로 등록 | 자동으로 빈을 등록, 클래스 자체가 빈으로 등록 |
@Bean과 @Component를 비교하는 표를 작성해보니, 필자가 이전 포스팅에서 작성했던 방식과는 차이가 있었다.
이전에는 필자가 직접 작성한 클래스도 모두 @Bean으로 등록했지만, 실제로는 용도에 따라 구분해서 사용하는 것이 더 좋다는 것을 정리할 수 있었다.
(이번에 정리하면서 알게 되었다..)
이처럼 외부 라이브러리는 @Bean으로, 직접 작성한 클래스는 @Component로 등록하는 것이 스프링의 일반적인 사용 방식이라고 한다.
간단한 코드를 통해 한번 살펴보자
Sample Code
// @Bean 사용 예시 @Configuration public class AppConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
// @Component 사용 예시 @Component public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } }
외부 라이브러리를 스프링 빈으로 등록해야 할 때는 해당 라이브러리의 코드를 직접 수정할 수 없기 때문에, @Bean 어노테이션을 사용하여 설정 클래스에서 수동으로 등록한다.
반면에 우리가 직접 작성한 클래스들은 @Component 어노테이션을 사용하여 자동으로 스프링 빈으로 등록할 수 있다 🙌
@ComponentScan은 @Component 어노테이션이 붙은 클래스를 찾아 자동으로 빈으로 등록해주는 기능이다.
패키지 내의 클래스들을 스캔하여 빈으로 등록할 대상을 찾아준다.
예제 코드를 통해 살펴보자
Sample Code
@Configuration @ComponentScan(basePackages = {"main"}) public class AppCtx { @Bean @Qualifier("printer") public MemberPrinter memberPrinter() { return new MemberPrinter(); } @Bean @Qualifier("summaryPrinter") public MemberSummaryPrinter memberPrinter2() { return new MemberSummaryPrinter(); } }
여기서 basePackages는 스캔할 패키지의 시작 위치를 지정한다.
이렇게 설정하면 "main" 패키지와 그 하위 패키지에 있는 모든 @Component 어노테이션이 붙은 클래스를 스캔할 수 있다.
즉, @Component 어노테이션이 붙은 모든 클래스를 스프링 컨테이너에 빈으로 자동으로 등록된다!
스프링은 다음과 같은 어노테이션이 붙은 클래스를 스캔 대상으로 포함한다
기본 스캔 대상
- @Component: 일반적인 스프링 빈
- @Controller: 스프링 MVC 컨트롤러
- @Service: 비즈니스 로직 처리
- @Repository: 데이터 접근 계층
- @Aspect: AOP 관련 클래스 (공통 관심사를 분리한 프로그래밍 기법)
- @Configuration: 스프링 설정 정보
컴포넌트 스캔 과정에서 같은 이름의 빈이 존재할 경우 충돌이 발생할 수 있다.
우선 충돌이 발생할 수 있는 상황을 코드로 작성해보자
Sample Code - 충돌 발생
@Component public class MemberRegisterService { // memberRegisterService 이름으로 빈이 등록 // 이미 같은 이름의 빈이 존재하면 충돌 발생! }
Result View
이전에 살펴본 컴포넌트 스캔의 basePackages를 패키지 명으로 설정할 수 있다.
패키지 하위에는 또 다른 여러 개의 패키지가 존재할 수 있으며, 당연히 패키지 내부에서는 동일한 이름의 클래스가 존재할 수 있다.
따라서, 만약 동일한 이름으로 Main 하위의 패키지에서 사용하고 있다면, 컴포넌트 스캔 시 충돌이 발생할 수 있다.
필자는 사진과 같이 ConflictingBeanDefinitionException 예외가 발생하는 모습을 볼 수 있었다.
이러한 경우, 우리는 빈 이름을 명시적으로 지정하여 해결할 수 있다
Sample Code - 충돌 해결
@Component("ex02MemberRegisterService") public class MemberRegisterService { // 다른 이름으로 등록되어 충돌 방지 }
이전에 살펴본 빈의 이름을 지정하는 방식을 사용하면 된다 🙌
만약 개발자가 같은 빈 이름으로 수동 등록(@Bean)과 자동 등록(@Component)을 했다면, 수동 등록된 빈이 우선권을 갖는다는 것을 기억하자!
간단한 코드를 통해 예시를 살펴보자
Sample Code
// MemberDAO.java @Component public class MemberDAO { ... }
// AppCtx.java @Configuration @ComponentScan(basePackages = {"main"}) public class AppCtx { @Bean public MemberDAO memberDAO() { return new MemberDAO(); } ... }
위 코드에서는 MemberDAO가 두 가지 방식으로 등록되고 있다.
이런 경우 수동 등록이 우선권을 갖는다. 왜냐하면 수동 등록이 더 구체적인 설정이기 때문이다.
하지만 이처럼 자동 등록과 수동 등록이 섞여 있으면 빈 등록이 중복되거나 덮어쓰여질 수 있어 잠재적인 버그의 원인이 될 수 있다.
따라서 빈 등록 방식은 한 가지로 일관되게 유지하는 것이 좋다!
이번 포스팅에서는 스프링의 자동 빈 등록 기능인 @Component와 @ComponentScan에 대해 알아보았다.
이 기능들을 사용하면 반복적인 빈 등록 코드를 줄일 수 있고, 더욱 효율적으로 스프링 빈을 관리할 수 있다.
다만, 빈 이름 충돌이나 수동/자동 등록의 혼용과 같은 주의사항들을 잘 고려해서 사용하자 👊