1. 컴포넌트 스캔과 의존관계 자동 주입
지금까지 스프링 빈을 등록할 때는 자바 코드에 @Bean을 붙이거나 XML의 <bean> 태그로 설정 정보를 등록했다. 하지만 이렇게 등록하게 되면 일일이 등록하기도 귀찮고 설정정보도 커진다. 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 "컴포넌트 스캔" 기능을 제공한다. 또한 의존 관계 주입도 자동으로 해주는 @Autowired 기능도 제공한다.
@Configuration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
}
- @ComponentScan: @Component가 붙은 클래스를 찾아서 자동으로 스프링 빈으로 등록한다.
- excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class): 이전에 만든 AppConfig 클래스는 @Configuration이라는 어노테이션이 붙어있었다. 이때 @Configuration을 타고 들어가면 @Component가 존재하여 해당 클래스도 스캔 대상이 된다. 하지만 AutoAppConfig는 설정 정보를 자동으로 설정하는 것이고, AppConfig는 수동으로 설정한 것이기 때문에 AppConfig가 스캔 대상이 되면 충돌이 발생한다. 따라서 해당 문구를 넣어 스캔 대상에서 제외한다. (보통 설정 정보를 스캔 대상에서 제외하진 않지만 우리 예제 코드를 최대한 남기기 위함이다.)
위와 같이 컴포넌트 스캔을 사용하려면 @ComponentScan만 사용하면 되고, @Bean과 같이 우리가 직접 스프링 빈으로 등록할 필요가 없어졌다. (실제로 @Bean과 같은 코드도 위 클래스 안에는 존재하지 않는다.) 이제 스프링 빈으로 등록하고 싶은 클래스들에 @Component를 붙여주자.
@Component
public class MemoryMemberRepository implements MemberRepository{...}
@Component
public class RateDiscountPolicy implements DiscountPolicy{...}
@Component
public class MemberServiceImpl implements MemberService{...}
이제 @Component가 붙은 친구들은 스프링 빈으로 등록될 것이다. 하지만 주의할 점이 있다. 의존 관계 주입은 어떻게 해야할까? 기존 AppConfig에서는 어떤 생성자에 어떤 메서드(스프링 빈)를 집어넣을지 명시하였다. 하지만 AutoAppConfig는 아무것도 없다. 따라서 의존 관계를 주입할 방식이 없는 것이다. 이 때문에 자동 의존 관계 주입이 필요하다. 의존 관계를 자동으로 주입하기 위해서는 생성자에 @Autowired를 붙여주면 된다. 그러면 스프링은 @Autowired가 붙은 생성자에서는 필요한 스프링 빈을 자동으로 찾아와서 주입해준다. 만약 아래와 같은 코드가 있다고 하자.
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
위에서 @Autowired는 ac.getBean(MemberRepository.class)와 같은 동작을 수행한다고 생각하면 된다. 이와 같은 원리로 OrderServiceImpl은 아래와 같이 구성된다.
@Component
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
...
}
이와 같이 @Autowired를 사용하면 생성자에서 여러 의존관계도 한 번에 주입할 수 있다. 테스트는 아래 코드로 진행할 수 있다.
public class AutoAppConfigTest {
@Test
void basicScan() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}
}
위와 같이 AutoAppConfig를 통해 스프링 빈들을 등록하고, 빈을 가져오면 올바른 빈을 가져오는 것을 확인할 수 있다. 총 내용을 정리하면 다음과 같다.
- @ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.
- 스프링 빈의 기본 이름은 클래스 명을 사용하되 맨 앞 글자는 소문자로 변경한다. 즉, MemberServiceImpl의 경우 memberServiceImpl이 된다. 만약 스프링 빈의 이름을 지정하고 싶다면 @Component("memberService")처럼 지정할 수 있다.
- 생성자에 @Autowired를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아 주입한다. 단, 기본적으로 타입이 같은 빈을 찾아 주입한다.
2. 탐색 위치와 기본 스캔 대상
모든 자바 클래스를 모두 컴포넌트 스캔하면 시간이 오래 걸리므로 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다. 지정하는 방법은 아래와 같다.
@ComponentScan(
basePackages = "com.example.projectEx1"
)
그러면 com.example.projectEx1 패키지 안에서부터 스캔을 시작한다.
- 여러 개를 두려면 basePackages = {"com.example.ex1", com.example.ex2}와 같이 설정할 수 있다.
- basePackageClasses라는 친구도 있는데 이는 지정한 클래스의 패키지를 탐색 위치로 지정한다.
Ex) basePackageClasses = AutoAppConfig.class - 지정하지 않은 경우 @Component를 붙인 클래스의 패키지를 기준으로 스캔을 시작한다. (현재 클래스의 패키지를 보려면 맨 위에 "package"로 뭐가 붙어있는지 살펴보자. Ex) package com.exmaple.projectEx1;)
- 권장하는 방법은 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이다(즉, 디폴트 방식을 사용한다). 최근 스프링 부트도 이 방법을 기본으로 제공한다. 스프링 부트의 경우 대표 시작 정보인 @SpringBootApplication을 프로젝트 시작 루트 위치에 두는 것이 관례다. 해당 설정 안에 @ComponentScan이 들어있기 때문이다. 즉, 스프링 부트를 쓰면 @Component로 등록되어 있던 것들이 @SpringBootApplication안에 있는 @ComponentScan을 통해 스프링 빈으로 등록된다.
또한 ComponentScan은 @Component외에도 다른 것들을 스캔한다. 기본 대상이 되는 것에는 다음과 같은 것들이 있다.
- @Component: 컴포넌트 스캔에 사용
- @Controller: 스프링 MVC 컨트롤러에 사용 / 스프링 MVC 컨트롤러로 인식한다.
- @Service: 스프링 비지니스 로직에 사용 / 특별한 처리를 하진 않는다. 단, 개발자들이 핵심 비지니스 로직이 여기 있는지를 인식하도록 하는데 도움을 준다.
- @Repository: 스프링 데이터 접근 계층에서 사용 / 스프링 데이터 접근 계층으로 인식한다. 데이터 계층의 예외를 스프링 예외로 변환한다.
- @Configuration: 스프링 설정 정보에서 사용 / 스프링 설정 정보로 인식한다. 스프링 빈이 싱글톤을 유지하도록 처리한다.
이들이 스캔 대상이 되는 이유는 @Service, @Repository 등 모두 내부 코드를 살펴보면 @Component가 붙어있기 때문이다.
3. 필터
필터에는 includeFilters와 excludeFilters 두 가지가 존재한다. 이 두 가지의 특징은 다음과 같다.
- includeFilters: 컴포넌트 스캔 대상을 추가로 지정한다.
- excludeFilters: 컴포넌트 스캔에서 제외할 대상을 지정한다.
먼저 아래 처럼 포함할 클래스에 붙일 어노테이션과, 포함하지 않을 클래스에 붙일 어노테이션을 만든다.
// 포함할 클래스에 붙일 어노테이션
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
// 포함하지 않을 클래스에 붙일 어노테이션
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}
그리고 두 개의 빈을 만들고 위에서 만든 어노테이션을 각각 붙여주자.
// 스프링 빈으로 만들 클래스
@MyIncludeComponent
public class BeanA {
}
// 스프링 빈으로 만들지 않을 클래스
@MyExcludeComponent
public class BeanB {
}
그리고 아래와 같이 Filter를 적용하고 테스트 코드를 돌리면 올바르게 동작하는 것을 확인할 수 있다.
public class ComponentFilterAppConfigTest {
@Test
void filterScan() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
// beanA는 스프링 빈으로 등록되어 있을 테니 null이면 안된다.
BeanA beanA = ac.getBean("beanA", BeanA.class);
Assertions.assertThat(beanA).isNotNull();
// beanB는 스프링 빈으로 등록되지 않으므로 getBean을 하면 에러가 발생함.
org.junit.jupiter.api.Assertions.assertThrows(
NoSuchBeanDefinitionException.class,
() -> ac.getBean("beanB", 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 옵션 종류다.
- ANNOTATION: 어노테이션을 인식하여 동작함.
- ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작함.
- ASPECTJ: AspectJ 패턴 사용
- REGEX: 정규 표현식
- CUSTOM: "TypeFilter"라는 인터페이스 구현하여 처리
즉, 만약 BeanA도 제외하고 싶다면 excludeFilters에 아래를 추가하면 된다.
- @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = BeanA.class)
하지만 exclude와 include 기능은 거의 많이 사용하진 않고 excludeFilter만 가끔 사용할 때가 있다. 특히 스프링 부트는 컴포넌트 스캔을 기본적으로 제공하며 옵션을 변경하면서 사용하기 보다 기본 설정에 최대한 맞춰서 사용하는 것을 권장한다.
4. 중복 등록과 충돌
중복 등록의 경우 두 가지 상황이 존재한다.
- 자동 빈 등록 vs. 자동 빈 등록
- 수동 빈 등록 vs. 자동 빈 등록
첫 번째 경우 스프링은 "ConflictingBeanDefinitionException" 예외를 발생시킨다. 단, 두 번째 경우는 수동 빈 등록이 우선권을 가지고 수동 빈이 자동 빈을 오버라이딩 한다. 단, 스프링 부트의 경우 스동 빈과 자동 빈에서 충돌이 나는 경우 오류가 나도록 기본 값을 바꾸었다고 한다. 이때 아래 에러를 출력한다.
- Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
'BackEnd > Spring' 카테고리의 다른 글
[Spring] 빈 생명주기 콜백 (0) | 2023.08.31 |
---|---|
[Spring] 의존관계 자동 주입 + lombok 설정 (0) | 2023.08.30 |
[Spring] 싱글톤 컨테이너 (0) | 2023.08.23 |
[Spring] 스프링 컨테이너와 스프링 빈 (0) | 2023.08.20 |
[Spring] IoC, DI, 컨테이너 (0) | 2023.08.20 |