@ComponentScan 어노테이션이 붙으면 해당 클래스는 컴포넌트 스캔의 대상이 된다. @Configuration도 마찬가지로 내부에 ComponentScan 어노테이션을 포함하였기 때문에 컴포넌트 스캔의 대상이 되었다.
이러한 컴포넌트 스캔이 붙은 클래스는 @Component 어노테이션이 붙은 객체들을 찾아 스프링 빈으로 등록하게 된다.
만약 @Component가 붙은 클래스들이 객체를 주입받으려고 한다면 생성자 부분에 @Autowired 어노테이션을 붙여 의존 자동 주입을 설정해주어야 한다.
@ComponentScan 어노테이션에는 많은 옵션이 있다.
basePackages
: 탐색을 시작할 패키지를 지정함. 이 패키지를 포함한 하위 패키지를 탐색한다. (여러 시작위치 지정이 가능하다)basePackageClasses
: 지정한 클래스가 포함된 패키지를 탐색 시작 위치로 지정한다.default
: 만약 아무것도 지정하지 않으면 @ComponentScan 어노테이션이 붙은 클래스가 포함된 패키지부터 탐색이 시작된다.(★)권장하는 방법 : 따로 패키지 위치를 지정하지 않고, 설정 클래스의 위치를 프로젝트 최상단에 위치하는 것이다. 최근 스프링 부트도 이러한 default 방법을 기본으로 제공한다.
예를 들어)
com.hello
com.hello.service
com.hello.repository
와 같은 패키지들이 존재할 때
com.hello
위치에 AppConfig 같은 메인 설정 정보를 두고, @ComponentScan 어노테이션을 붙인 뒤 basePackages를 생략하는 방식이다.
이는 메인 설정 정보가 프로젝트를 대표하는 정보이기 때문에 최상단에 위치하는 것이 적절하다.
또한 스프링 부트를 사용하면 스프링 부트의 시작 정보인 @SpringBootApplication 어노테이션이 붙은 클래스가 프로젝트의 최상단에 위치하게 되며, 여기엔 @ComponentScan 어노테이션이 포함된다!
-> 즉, @ComponentScan 자체를 쓸 필요가 없어지기까지도 한다. 그냥 @Component로 스프링 빈 등록을 요구만 해도 자동으로 등록되겠지만 원리를 알아야 한다.
@Component
, @Controller
, @Service
, @Repository
, @Configuration
과 같은 어노테이션들은 기본적으로 내부에 @Component 어노테이션을 포함하고 있기 때문에 자동으로 컴포넌트 스캔의 대상이 된다.
※ 참고 : 원래 어노테이션간의 상속 관계란 없다. 위처럼 하나의 어노테이션이 다른 어노테이션을 들고 있는 것을 지원해주는 기능은 자바 언어가 지원하는 것이 아니라, 스프링이 지원하는 기능이다.
추가적으로 아래의 어노테이션들은 다음과 같은 추가 기능이 존재한다.
@Controller
: 스프링 MVC 컨트롤러로 인식
@Repository
: 스프링 데이터 접근 계층으로 인식하고, 데이터 계층 예외를 스프링 예외로 변환시켜 준다.
@Configuration
: 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 처리해준다.
@Service
: 사실 @Service는 특별한 처리를 하지 않는다. 단순히 개발자들이 핵심 비즈니스 로직이 이곳에 위치할 것이라고 인식하는데 도움이 된다.
컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 만약 이름이 같다면 스프링은 오류를 발생시킨다
-> ConflictingBeanDefinitionException 발생
수동 빈 등록이 자동 빈 등록을 오버라이딩 해버린다 (덮어쓴다.)
물론 이러한 상황을 개발자가 의도적으로 기대한다면 좋겠지만, 현실에서는 대부분 여러 설정이 꼬이면서 발생하게 된다. 따라서 이러한 버그는 잡기가 매우 힘들기 때문에 스프링 부트는 수동 빈 등록과 자동 빈 등록이 충돌하면 오류가 발생하도록 기본 값을 바꾸었다.
오해하면 안되는 것이 스프링 프레임워크의 기본 값은 Override이지만,
스프링 부트의 경우가 오류를 발생시키도록 변경한 것이다.
Optional.empty
가 입력된다.@Autowired
는 처음에 타입 매핑을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
ex)
@Autowired
private DiscountPolicy rateDiscountPolicy;
위처럼 의존주입을 받아오면 만약 DIscountPolicy의 구현체 두 개가 모두 @Component 어노테이션이 붙어서 둘 다 스프링 빈으로 등록되어도, 필드 이름으로 빈 매칭을 한다.
@Qualifier("mainDiscountPolicy") 식으로 빈 이름에 식별자를 지정해준다. 단, 빈의 이름을 변경시키는게 아니라 @Qualifier 식별자만 지정해준 것이다.
만약 @Qualifier를 통해 의존주입 시 같은 식별자를 가진 빈이 없다면, @Qualifier 지정자에 적힌 이름을 가진 빈을 가져온다. 그래도 빈이 없다면 NoSuchBeanDefinitionException 이 발생한다.
추가로 @Qualifier는 파라미터로 문자열을 넘겨주기 때문에 컴파일 단계에서 에러 체크가 안된다. 따라서 이 경우 개발자가 별도로 어노테이션을 제작하여 적용시켜주는게 좋다.
자주 사용하지만 한계가 있는 우선 순위 방식.
동일한 이름을 가진 빈이 여러개 선택될 경우 @Primary가 붙은 어노테이션이 그냥 우선순위를 가지고 주입된다.
@Primary는 기본값처럼 동작하는 것이고, @Qualifier는 매우 상세하게 동작한다. 따라서 @Qualifier가 우선권이 더 높다.
'스프링 빈으로 등록된' 클래스 내부에서 @Autowired를 이용하여 Map과 List에 특정 빈을 생성자 주입 받으면, 제네릭<T> 에 지정한 타입과 일치하는 모든 타입들을 컬렉션에 담아서 가져오게 된다.
가령 Map<String, DiscountPolicy>를 생성자 파라미터로 받아오게 되면 구현체들인 Fix, Rate 두 스프링 빈이 map에 담기게 되는 것이다.
이를 위해서는 당연히 의존 대상이 스프링 빈으로 등록되어야 하고, 객체 자신또한 스프링 빈으로 등록되어 있어야 스프링 컨테이너가 이를 인식하고 의존관계를 맺어줄 것이다.
어떤 경우에 의존자동 주입을 사용하고 어떤 경우에 수동으로 빈 등록을 해야 할까?
결론부터 말하면 스프링은 갈수록 자동주입을 선호하는 추세이다.
@Controller, @Service, @Repository처럼 계층에 맞추어 자동으로 스캔할 수 있도록 지원한다.
그럼 언제 수동으로 빈을 등록해야 할까?
애플리케이션은 크게 두 가지 종류로 나눌 수 있다.
업무 로직의 경우 계층별로 유사한 패턴이 존재해서 문제 발생 시 원인을 파악하기가 비교적 쉽기 때문에 자동 주입을 권장한다.
기술 지원 로직의 경우 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 광범위하게 영향을 미친다. 또한 기술 지원 로직은 문제 발생 시 비교적 문제가 명확하게 드러나지 않기 때문에 이러한 로직들은 가급적 수동 빈 등록을 통해서 명확하게 나타낼 필요가 있다.
추가적으로 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보면 좋다.
예를 들어
@Configuration
class DiscountPolicyConfig {
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
}
이같이 DiscountPolicy를 다형성을 활용하여 설계하였다면 이에 대한 설정 정보를 한 눈에 보이도록 해주면 더 나은 가독성을 기대할 수도 있다.
물론 위처럼 빈 등록 자체를 Config에 담으면 빈 충돌이 일어날 수 있기 때문에 이에 대한 방안은 @Primary나 @Qualifier로 해결할 수 있을 것이다.