컴포넌트 스캔
기존에 사용하던 AppConfig
를 건드리기는 애매하니 AutoAppConfig
로 설정 파일을 생성한다.
package spring.basic;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
@Configuration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
) //@Configuration 도 @Component 이기 때문에 Scan 대상에서 제외
public class AutoAppConfig {
}
설정 파일이라는 표시인 @Configuration 어노테이션을 달고, 컴포넌트 스캔을 위한 @ComponentScan 어노테이션을 달아준다. 컴포넌트 스캔은 이름 그대로 @Component 어노테이션을 달고 있는 클래스를 전부 빈으로 등록한다. excludeFilters
로 @Configuration 어노테이션을 달고 있는 클래스는 스캔에서 제외시켰다. 왜냐하면 @Configuration 어노테이션도 안에 @Component 어노테이션을 달고 있어서 스캔 대상이 되다보니 기존에 사용하던 AppConfig
와 꼬일까봐이다.
앞서 작성했던 클래스들이 스캔 대상이 되도록 @Component 어노테이션을 달아준다.
MemoryMemberRepository
...
@Component
public class MemoryMemberRepository implements MemberRepository {
...
RateDiscountPolicy
...
@Component
public class RateDiscountPolicy implements DiscountPolicy {
...
MemberServiceImpl
...
@Component
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
AppConfig
에서는 @Bean 어노테이션과 의존관계를 포함한 모든 설정 정보를 작성했는데, AutoAppConfig
에서는 설정 정보가 없으니 @Autowired 어노테이션으로 의존관계를 주입해준다. @Autowired는 컴포넌트 스캔으로 빈을 등록하면서 생성자를 만나면 해당 타입의 빈이 등록되어 있는지 찾아보고 해당 빈을 주입하는 방식이다.
AutoAppConfigTest
package spring.basic.scan;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import spring.basic.AutoAppConfig;
import spring.basic.member.MemberService;
import static org.assertj.core.api.Assertions.*;
public class AutoAppConfigTest {
@Test
void basicScan() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
AutoAppConfig
를 테스트하기 위한 코드를 작성한다. AnnotationConfigApplicationContext를 사용하는 것은 동일하고, 실행 시키면 기존과 동일한 결과를 얻을 수 있다.
탐색 위치와 기본 스캔 대상
@ComponentScan 어노테이션에서 탐색 위치를 지정할 수 있다.
@ComponentScan(
basePackages = "spring.basic" //spring.basic를 포함한 하위패키지를 모두 탐색
basePackages = {"spring.basic.member", "spring.basic.order"} //여러개의 위치를 지정할 수 있다.
basePackageClasses
로 지정한 클래스의 패키지를 기본 탐색 위치로 지정할 수도 있으며, 아무것도 지정하지 않으면 @ComponentScan 어노테이션이 달린 클래스의 패키지가 기본 탐색 위치가 된다. 이와 같이 프로젝트 시작 위치에 메인 설정 정보를 두는 것이 일반적이다. (스프링 부트의 경우 @SpringBootApplication 어노테이션이 달린 클래스를 프로젝트 시작 위치에 둔다.)
기본 스캔 대상으로는 @Component 어노테이션을 포함한 다섯가지가 존재한다.
- @Component : 기본 컴포넌트 표시
- @Controller : 스프링 MVC에서 웹 요청을 처리하는 컨트롤러임을 표시
- @Service : 비즈니스 로직을 수행하는 서비스임을 표시하나 @Component와 기능상 차이는 없다
- @Repository : 데이터 접근 계층(DAO)임을 표시
- 데이터베이스 관련 예외를 스프링의 데이터 접근 예외로 변환하는 부가 기능이 있다
- @Configuration : 스프링 설정 정보 클래스임을 표시
필터
컴포넌트 스캔 대상으로 추가/제외 대상을 지정할 어노테이션을 만든다.
MyIncludeComponent
package spring.basic.scan.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
MyExcludeComponent
package spring.basic.scan.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}
테스트로 사용할 클래스를 작성한다.
BeanA
package spring.basic.scan.filter;
@MyIncludeComponent
public class BeanA {
}
BeanB
package spring.basic.scan.filter;
@MyExcludeComponent
public class BeanB {
}
임시로 사용할 설정 정보를 담은 테스트 코드를 작성한다.
ComponentFilterAppConfigTest
package spring.basic.scan.filter;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class ComponentFilterAppConfigTest {
@Test
void filterScan() {
ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
//Bean 이 조회가 되어야 함
BeanA beanA = ac.getBean("beanA", BeanA.class);
assertThat(beanA).isNotNull();
//Bean 이 조회가 안 되어야 함
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 {
}
}
아래 @ComponentScan에서 includeFilters
에서 MyIncludeComponent 어노테이션을 달고있으면 스캔 대상으로 포함하고, excludeFilters
에서 MyExcludeComponent 어노테이션을 달고있으면 스캔 대상으로 제외하도록 설정했다. 테스트를 돌려보면 beanA
는 정상적으로 조회가 되고, beanB
는 조회가 안 되면서 테스트가 성공한다. 실무에서는 include는 안 쓰고, exclude만 가끔씩 쓴다고 한다.
테스트에서는 FilterType
으로 어노테이션을 지정했으나, 어노테이션을 포함 다섯가지가 존재한다.
- ANNOTATION : 특정 어노테이션이 붙은 클래스만 포함/제외
- ASSIGNABLE_TYPE : 특정 타입 또는 그 하위 타입만 포함/제외
- ASPECTJ : AspectJ 패턴(예로 com.example..*Service+)을 사용
- REGEX: 정규식으로 클래스 이름 필터링
- CUSTOM : TypeFilter 인터페이스를 구현한 클래스를 사용
중복 등록과 충돌
만약 자동으로 등록된 빈끼리, 또는 수동으로 등록된 빈과 중복이 된다면 어떻게 될까? 우선 컴포넌트 스캔으로 자동으로 등록된 빈끼리 이름이 같은 경우 스프링은 ConflictingBeanDefinitionException
예외를 발생시킨다.
자동으로 등록된 빈과 수동으로 등록된 빈이 이름이 같으면 수동 빈이 우선순위를 가지며 자동 빈을 오버라이딩 해버린다. 하지만 개발자가 의도하였더라도 애플리케이션이 확장됨에 따라 애매하게 설정이 꼬이면서 잡기 어려운 버그가 탄생할 수 있으니 지양하는 것이 좋다. 최근 스프링 부트에서는 자동 빈과 수동 빈이 충돌할 경우 아예 예외를 발생시키도록 했다.
'스터디 > Spring' 카테고리의 다른 글
Spring 스터디2 - 9 (0) | 2025.05.13 |
---|---|
Spring 스터디2 - 8 (0) | 2025.05.11 |
Spring 스터디2 - 6 (0) | 2025.04.27 |
Spring 스터디2 - 5 (1) | 2025.04.19 |
Spring 스터디2 - 4 (1) | 2025.04.17 |