이 글은 인프런 - 스프링 핵심 원리 기본편을 보고 공부한 것을 정리한 글입니다.
기존에는 스프링 빈을 @Bean
으로 직접 등록했다. 하지만 이제는 자동으로 스프링빈을 등록하고 의존관계 또한 자동으로 주입하는 방법을 알아보자.
@Configuration
@ComponentScan(
// 기존에 있는 AppConfig 클래스를 컴포넌트 스캔에서 제외시킴
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
}
@Configuration
을 달아주어 Config 클래스임을 알려준다.@ComponentScan
을 달아준다.AutoAppConfig
안에는 아무것도 없다.이렇게 어노테이션으로 두 가지만 달아주고나서, 스프링 빈을 등록하고 싶은 구현체에 @Component
를 달아주자.
그럼 자동으로 스프링이 스캔하여 해당 객체를 스프링 컨테이너에 싱글톤으로 빈 등록을 자동으로 해준다.
이제 의존관계 자동주입을 해보자.
OrderServiceImpl
과 MemberServiceImpl
에 있는 생성자 위에 @Autowired
를 붙여주자.
private final MemberRepository memberRepository; // 추상화에만 의존
@Autowired // ac.getBean(MemberRepository.class); 와 같은 역할. 의존관계 자동주입.
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@ComponentScan
은 @Component
가 붙은 클래스를 스프링 빈에 등록한다.@Component("memberService2")
라고 이름을 직접적으로 명시할 수 있지만, 웬만하면 이렇게 하지 않음)@Autowired
를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아 주입한다.이 컴포넌트 스캔을 내가 원하는 범위만큼 탐색하도록 할 수도 있다.
패키지 내 모든 클래스(외부 라이브러리 포함)를 모두 스캔하면 시간도 오래걸릴것이고 비효율적이다.
따라서 꼭 필요한 위치부터 탐색하도록 해야 한다.
이는
@ComponentScan(
basePackages = "hello.core"
)
@ComponentScan(
basePackages = {"hello.core", "hello.service"}
)
이와 같이 설정할 수 있다.
baseClasses
도 있는데, 이 방법은 지정한 클래스의 최상위 패키지부터 탐색을 시작한다.
만약 탐색위치를 지정하지 않으면 @ComponentScan
이 붙은 클래스의 최상위 패키지부터 탐색을 시작한다.
설정 정보 클래스를 프로젝트의 최상단에 위치시키고, 따로 탐색 위치를 지정하지 않는 것이다.
참고로, 스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication
를 이 프로젝트 시작 루트 위치에 두는 것이 관례이다.
왜냐하면 @SpringBootApplication
안을 들여다보면 @ComponentScan
이 있기 때문이다.
컴포넌트 스캔은 @Component
뿐만 아니라 @Controller
, @Service
, @Repository
, @Conguration
도 추가로 스캔한다.
실제로 위 네 가지의 어노테이션을 타고 들어가면 @Component
가 있다.
사실 자바언어에는 어노테이션의 상속관계는 없다. 이는 스프링이 지원하는 기능이다.
그리고 다음과 같은 어노테이션이 있으면 스프링은 부가 기능을 수행한다.
1. @Controller
: 스프링 MVC 컨트롤러로 인식
2. @Repository
: 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
3. @Configuration
: 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤으로 유지하도록 추가 처리한다.
4. @Service
: 사실 @Service
는 특별한 처리를 하지 않지만, 개발자가 이 어노테이션을 보고 '여기에 핵심 비즈니스 로직이 있겠구나' 라고 인식하는 데에 도움을 준다.
참고로 useDefaltFilters
옵션은 디폴트로 켜져 있는데, 이 옵션을 끄면 기본 스캔 대상들이 제외된다.
includeFilter
: 컴포넌트 스캔 대상을 추가로 지정
excludeFilter
: 컴포넌트 스캔 제외할 대상 지정
public class ComponentFilterAppConfigTest {
@Test
void filterScan() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean(BeanA.class);
assertThat(beanA).isNotNull();
assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean(BeanB.class));
}
@Configuration
@ComponentScan (
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentFilterAppConfig {
}
}
FilterType에는 5가지 옵션이 있다.
ANNOTATION
: 기본값, 애노테이션을 인식해서 동작한다.ASSIGNABLE_TYPE
: 지정한 타입과 자식 타입을 인식해서 동작한다.ASPECTJ
: AspectJ 패턴 사용REGEX
: 정규 표현식CUSTOM
: TypeFilter 이라는 인터페이스를 구현해서 처리참고로, @Component
면 충분하기 때문에 여러가지 이유로 간혹 excludeFilter
를 사용하는 경우 말고는 필터를 잘 사용하지 않는다.
최대한 스프링 기본 설정에 맞추어 사용하는 것이 권장된다!
만약 스프링 컨테이너에 스프링 빈이 같은 이름으로 등록되면 어떻게 될까?
이 경우엔 두 가지 경우가 있다.
우선 1번의 경우 스프링이 ConflictingBeanDefinitionException
예외를 발생시킨다.
2번의 경우 수동 빈이 우선권을 가지게 되어, 자동 빈을 오버라이딩한다.
실제 로그를 보면 Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing
라고 뜬다.
하지만!! 개발자가 의도를 가지고 이렇게 할 수 있지만, 대부분의 경우 개발자가 의도적으로 이런 결과를 설계하기보다, 여러 설정들이 꼬여서 이런 결과를 초래한다. 이러면 정말 애매한 버그가 생기는데, 이는 매우 고치기 힘든 버그이다.
그래서 최근 스프링 부트는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 변경하였다.
실제 로그: Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
이름을 바꾸거나, application.properties
에 들어가 spring.main.allow-bean-definition-overriding=true
를 추가하면 오류가 해결된다.
하지만 가장 좋은 방법은 애초에 중복 등록이 되지 않도록 하는 것과, 만약 겹친다면 이름을 알기 쉽게 바꾸는 것이 좋겠다.