본문 바로가기
Study/Spring

[Spring] 컴포넌트 스캔

by dev_kong 2023. 2. 1.
728x90
728x90

김영한님의 스프링 핵심 원리 - 기본편을 바탕으로 작성하였습니다.

[Spring] 싱글톤 컨테이너 와 이어지는 포스팅입니다.
전체 소스코드 보기

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

이전 포스팅까지 스프링 빈을 등록할 때는 AppConfig 파일에 @Bean을 통해서 설정정보에 직접
등록할 스프링 빈을 작성했다.

 

근데 이거 귀찮다. 매우.

 

지금은 아주 간단한 예제코드를 작성하는 거기 때문에 별거 없네 라고 생각할 수 있지만,
실무에서는 수십, 수백개의 스프링빈을 등록해야 될거다.

 

근데 이렇게 된다면 당연하게도 누락되는 일도 생길거고,
설정정보도 지나치게 방대해지고, 가장 중요한건 귀찮다..

 

그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이란 기능을 제공한다.
또한, 의존 관계 역시 자동으로 주입해주는 @Autowired라는 기능도 제공한다.

 

어뜨케 하면 되는지 코드를 보자..!

@Configuration  
@ComponentScan(  
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)  
)  
public class AutoAppConfig {  

}

기존의 AppConfig 는 그대로 두고 새로 AutoAppConfig 를 만들었다.

 

@ComponentScan 에서 excludeFilter를 사용하여 Configuration 어노테이션이 붙은 것들은 스캔대상에서
제외한 것은 기존에 만들어 두었던 AppConfig 파일을 스캔에서 제외하기 위한 것이다.

@Configuration 의 소스코드를 열어 확인해보면,
@Component 어노테이션이 붙어있는 것을 확인할 수 있다.

실제로 작업을 할때는 이 작업을 할 필요가 없다.

 

컴포넌트 스캔은 말그대로 @Component 어노테이션이 붙은 모든 클래스를 스캔하여,
스프링 빈으로 등록한다.


기존에 AppConfig에서 등록했던 스프링 빈들의 클래스에 찾아가서,
@Component 어노테이션을 붙여주자.

 

MemberServiceImpl을 예시로 들여다 보면 아래와 같다.

@Component  
public class MemberServiceImpl implements MemberService{  

    private final MemberRepository memberRepository;  

    public MemberServiceImpl(MemberRepository memberRepository) {  
        this.memberRepository = memberRepository;  
    }  

    @Override  
    public void join(Member member) {  
        memberRepository.save(member);  
    }  

    @Override  
    public Member findMember(Long memberId) {  
        return memberRepository.findById(memberId);  
    }  

    public MemberRepository getMemberRepository() {  
        return memberRepository;  
    }}

그런데 한가지 문제점이 있다.


기존 AppConfig를 이용해 작성할 때에는 의존관계 주입을 직접 해주었는데,
지금 현재의 방법에서는 의존관계를 주입할 방법이 없다.

 

이때 사용되는게 @Autowired 어노테이션이다.


이 어노테이션을 생성자 위에 붙여주면
생성자에 인자값으로 들어가는 타입을 확인해서,
이전 포스팅에서 열심히 했던 ac.getBean(MemberRepository.class) 와 비슷한 역할을 한다.

@Component  
public class MemberServiceImpl implements MemberService{  

    private final MemberRepository memberRepository;  

    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {  
        this.memberRepository = memberRepository;  
    }  

    @Override  
    public void join(Member member) {  
        memberRepository.save(member);  
    }  

    @Override  
    public Member findMember(Long memberId) {  
        return memberRepository.findById(memberId);  
    }  

    public MemberRepository getMemberRepository() {  
        return memberRepository;  
    }}

위와같이 변경해주면 된다.


나머지 구현체들도 마찬가지로 설정해주자.

 

이제 이게 잘 돌아가는지 테스트코드를 통해 확인해보자.

public class AutoAppConfigTest {  

    @Test  
    @DisplayName("컴포넌트 스캔 확인")  
    void basicScan() {  
        ApplicationContext ac = 
        new AnnotationConfigApplicationContext(AutoAppConfig.class);  

        MemberService memberService = ac.getBean(MemberService.class);  
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);  
    }
}

테스트코드가 정상적으로 통과됨에 따라,
컴포넌트 스캔이 정상적으로 잘 동작함을 알 수 있다.

탐색위치와 기본 스캔 대상

탐색할 패키지 지정

모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래걸린다.
그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.

@Configuration  
@ComponentScan(  
        basePackages = "hello.springbasic.member",  
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)  
)  
public class AutoAppConfig {  

}

위와 같이 작성하면, member package 내의 클래스만 스캔한다.

 

만약 basePackages 를 작성 하지 않으면(디폴트 설정),
@ComponentScan 어노테이션이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.

 

따라서 패키지 위치를 따로 지정하는 것보다, 설정 정보 클래스의 위치를 최상단에 두는게 가장 속편하다.

컴포넌트 스캔 대상

컴포넌트 스캔은 @Component 뿐만 아니라 다음과 같은 내용도 추가로 대상에 포함한다.

@Controller : 스프링 MVC 컨트롤러에서 사용
@Service : 스프링 비즈니스 로직에서 사용
@Repository : 스프링 데이터 접근 계층에서 사용
@Configuration : 스프링 설정 정보에서 사용

위에서 설명했듯, @Configuration 어노테이션의 소스코드르 살펴보면
@Component 어노테이션이 달려 있다고 했는데,
위의 모든 어노테이션의 소스코드에 @Component가 달려있다.

필터

컴포넌트 스캔 대상을 추가로 지정하거나, 제외할 대상을 지정할 수 있다.
코드를 통해 확인해보자.

 

우선 어노테이션을 두개 만들자.
(이 부분은 잘 모르겠다. 따로 더 공부가 필요한 부분인듯 하다.)

@Target(ElementType.TYPE)  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
public @interface MyExcludeComponent {  
}


@Target(ElementType.TYPE)  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
public @interface MyIncludeComponent {  
}

이렇게 어노테이션 두개를 만들었다.

 

우선 알 수 있는 것 하나는, Target이 TYPE이기 때문에 클래스레벨에 붙는 어노테이션이라는것 밖에는 모르겠다.

그리고 각각 어노테이션을 하나씩 달고 있는 클래스 두개를 만든다.

@MyIncludeComponent  
public class BeanA {  
}

@MyExcludeComponent  
public class BeanB {  
}

마지막으로 설정 정보를 담을 클래스를 만들어보자.

@Configuration  
@ComponentScan(  
        includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),  
        excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)  
)  
static class ComponentFilterAppConfig {  
}

includeFilters는 스캔대상을 추가하는 것이고,
excludeFilters는 스캔대상에서 제외하는 것이다.

 

이제 이걸 테스트코드로 잘 동작하는지 확인해보자.

public class ComponentFilterAppConfigTest {  

    @Test  
    void filterScan() {  
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);  

        BeanA beanA = ac.getBean("beanA", BeanA.class);  

        assertThat(beanA).isNotNull();  
        assertThatThrownBy(() -> 
    ac.getBean("beanB",BeanB.class))
    .isInstanceOf(NoSuchBeanDefinitionException.class);  
    }  
}

테스트가 통과됨에 따라,
@MyIncludeComponent 가 붙은 BeanA는 포함된것을,
@MyExcludeComponent 가 붙은 BeanB은 스캔에서 제외된 것을 확인할 수 있다.

FilterType 옵션

  • ANNOTATION: 디폴트값, 어노테이션을 인식해서 동작한다.
  • ASSIGNABLE_TYPE : 지정한 타입과 자식타입을 인식해서 동작한다.
  • ASPECTJ : AspectJ 패턴을 사용한다.
  • REGEX : 정규식.
  • CUSTOM : TypeFilter라는 인터페이스를 구현해서 처리.

중복 등록과 충돌

컴포넌트 스캔에서 같은 빈 이름을 가진 빈이 등록되면 충돌이 발생한다.

자동등록 VS 자동등록

ConflictingBeanDefinitionException이 발생한다.

자동등록 VS 수동등록

이 경우는 수동 빈 등록이 우선권을 가진다.
(수동 빈이 자동빈을 오버라이딩 해줌.)

 

여러 설정들이 꼬여서 이런 결과가 만들어지는 경우가 대부분이다.


스프링 빈이 수십, 수백개가 되는 실무에서는 이런 상황에서의 디버깅이 매우 어렵기 때문에
스프링부트에서는 수동 빈 등록과 자동 빈등록이 충돌하면 오류가 발생하도록 기본값을 바꾸었다.

728x90
728x90

댓글