[Spring] 컴포넌트 스캔

kdkdhoho·2022년 2월 23일
0

Spring

목록 보기
10/26

이 글은 인프런 - 스프링 핵심 원리 기본편을 보고 공부한 것을 정리한 글입니다.

컴포넌트 스캔과 의존관계 자동 주입 시작하기

기존에는 스프링 빈을 @Bean으로 직접 등록했다. 하지만 이제는 자동으로 스프링빈을 등록하고 의존관계 또한 자동으로 주입하는 방법을 알아보자.

@Configuration
@ComponentScan(
        // 기존에 있는 AppConfig 클래스를 컴포넌트 스캔에서 제외시킴
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}
  1. @Configuration을 달아주어 Config 클래스임을 알려준다.
  2. 컴포넌트 스캔을 사용하기 위해 @ComponentScan을 달아준다.
  3. AutoAppConfig 안에는 아무것도 없다.

이렇게 어노테이션으로 두 가지만 달아주고나서, 스프링 빈을 등록하고 싶은 구현체에 @Component를 달아주자.
그럼 자동으로 스프링이 스캔하여 해당 객체를 스프링 컨테이너에 싱글톤으로 빈 등록을 자동으로 해준다.

이제 의존관계 자동주입을 해보자.
OrderServiceImplMemberServiceImpl에 있는 생성자 위에 @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;
}

컴포넌트 스캔과 자동 의존관계 주입 동작 과정

  1. @ComponentScan@Component가 붙은 클래스를 스프링 빈에 등록한다.
    이때 스프링 빈의 기본 이름은 클래스 명이되, 맨 앞은 소문자로 바꿔준다. (ex. MemberServiceImpl -> memberServiceImpl)
    (@Component("memberService2") 라고 이름을 직접적으로 명시할 수 있지만, 웬만하면 이렇게 하지 않음)
  2. 생성자에 @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가지 옵션이 있다.

  1. ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.
    ex) org.example.SomeAnnotation
  2. ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
    ex) org.example.SomeClass
  3. ASPECTJ: AspectJ 패턴 사용
    ex) org.example..*Service+
  4. REGEX: 정규 표현식
    ex) org.example.Default.*
  5. CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리
    ex) org.example.MyTypeFilter

참고로, @Component면 충분하기 때문에 여러가지 이유로 간혹 excludeFilter를 사용하는 경우 말고는 필터를 잘 사용하지 않는다.
최대한 스프링 기본 설정에 맞추어 사용하는 것이 권장된다!

중복 등록과 충돌

만약 스프링 컨테이너에 스프링 빈이 같은 이름으로 등록되면 어떻게 될까?

이 경우엔 두 가지 경우가 있다.

  1. 자동 빈 등록과 자동 빈 등록
  2. 수동 빈 등록과 자동 빈 등록

우선 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를 추가하면 오류가 해결된다.

하지만 가장 좋은 방법은 애초에 중복 등록이 되지 않도록 하는 것과, 만약 겹친다면 이름을 알기 쉽게 바꾸는 것이 좋겠다.

profile
newBlog == https://kdkdhoho.github.io

0개의 댓글