컴포넌트 스캔

권영태·2024년 1월 5일
0

스프링

목록 보기
17/18

출처 : 스프링 핵심 원리 - 기본편


스프링 빈을 등록할 때는 자바 코드의 @Bean 또는 XML의 <bean>을 통해 직접 등록할 스프링 빈을 나열했다.
예제에선 4개 정도로 적었지만 실제 서비스에서는 몇 배로 늘어나게 될 것이고, 이는 누락 또는 반복 문제가 발생한다.
그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔 기능을 제공한다.
또 의존관계도 자동으로 주입하는 @Autowired 기능도 제공한다.

컴포넌트 스캔

@Configuration
@ComponentScan(     // @Component가 붙은 모든 클래스들을 스캔한다.
//        basePackages = "hello.core.member",     // 해당 패키지 하위만 찾아서 컴포넌트 스캔한다.
//        basePackageClasses = AutoAppConfig.class,   // 지정한 클래스의 패키지를 탐색 시작 위치로 지정
        // 위 2가지 다 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
        // excludeFilters == 내가 스캔하고 싶지 않은것들을 제거
)
public class AutoAppConfig {
}

@Configuration을 통해 설정 파일로 설정하고, @ComponentScan을 작성하면 탐색 위치에 있는 클래스들 중 @Component가 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.

  • @Comfiguration이 컴포넌트의 스캔 대상이 된다.
    -> @Comfiguration 안에 @Component가 붙어 있기 때문!

자동 의존관계 주입

이전 AppCofig에서는 @Bean으로 직접 설정 정보를 작성한 뒤, 의존관계도 직접 명시했다.

/**
* 의존관계 직접 명시
*/
public class MemberServiceImpl implements MemberService{

	private final MemberRepository memberRepository;

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

@Component를 사용하면 의존관계를 직접 명시하지 않고, @Autowired를 사용해 의존관계를 자동으로 주입할 수 있다.

@Component
public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    @Autowired // == ac.getBean(MemberService.class)
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
  • @Autowired를 사용하면 생성자에서 여러 의존관계도 한번에 주입받을 수 있다!

제대로 의존관계가 주입되는지 직접 확인해보자.

@Test
void basicScan() {
	AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
    MemberService memberService = ac.getBean(MemberService.class);

    Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}


로그를 확인하니 컴포넌트 스캔과 의존관계 주입이 제대로 된 것을 확인할 수 있다.

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

1. @ComponentScan

@ComponentScan@Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.

  • 이때 스프링 빈의 기본 이름은 클래스 명을 사용하되 맨 앞글자만 소문자를 사용한다.
    • 만약 빈 이름을 직접 지정하고 싶다면 @Component("원하는 이름") 처럼 작성하면 된다.
  • 스프링 컨테이너는 기본적으로 싱글톤 패턴을 사용함으로 컴포넌트 스캔을 통해 등록된 빈들도 각 인스턴스 1개씩만 저장되어 있다.

2. @Autowired 의존관계 자동 주입

생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.

  • 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입하고, 이미 등록된 타입이 있다면 오류가 발생된다.
    자세한 내용은 나중에 공부할 예정!

  • 만약 생성자에 파라미터가 많아도 다 찾아서 자동으로 주입되니 걱정말자.

컴포넌트 탐색 위치와 기본 탐색 대상

컴포넌트 탐색 위치

모든 자바 클래스와 부가 라이브러리를 다 컴포넌트 스캔하는 일을 상상해보자.
얼마나 오래 걸리고 끔찍한가? 그래서 꼭 필요한 위치만 탐색하도록 시작 위치를 지정할 수 있다.

@Configuration
@ComponentScan(     // @Component가 붙은 모든 클래스들을 스캔한다.
		basePackages = "hello.core.member",     // 해당 패키지 하위만 찾아서 컴포넌트 스캔한다.
		basePackageClasses = AutoAppConfig.class,   // 지정한 클래스의 패키지를 탐색 시작 위치로 지정
        //만약 위 2가지 다 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
)
public class AutoAppConfig {
	...
}
  • basePackages: 탐색할 패키지의 시작 위치를 지정한다. 해당 위치 ~ 하위 패키지까지 모두 탐색한다.
  • basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.
  • 만약 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.

최근 스프링 부트는 설정 정보 클래스(@Comfiguration)의 위치를 프로젝트 최상단에 두는 방법을 권장한다.

컴포넌트 기본 탐색 대상

컴포넌트의스캔은 @Component뿐만 아니라 다음과 같은 애노테이션도 추가로 대상에 포함한다.

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

참고로 애노테이션에는 상속관계는 없다.
한 애노테이션이 특정 애노테이션을 들고 있는 것이 인식되는 것은 자바 언어가 지원하는 기능이 아니라,
스프링이 지원하는 기능이다.

필터

컴포넌트 스캔 대상을 추가로 지정하거나, 제외할 수 있는 기능이다.

@MyIncludeComponent
public class BeanA {
}

@MyExcludeComponent
public class BeanB {
}

두 Component가 존재하고, 설정 정보 클래스의 @ComponentScan은 아래와 같다.

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


위 컴포넌트와 설정 정보가 담긴 파일 경로는 다음과 같을 때, 이때 기본 탐색 전략을 통해 결과를 유추해보면 ComponentFilterAppConfigTest.class와 같은 경로인 BeanA, BeanB 모두 탐색 될 것이다.
실제 테스트 코드를 통해 확인해보자

@Test
void basicScan() {
	ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
    
    BeanA beanA = ac.getBean("beanA", BeanA.class);
    BeanB beanB = ac.getBean("beanB", BeanB.class);
    
    assertThat(beanA).isNotNull();
    assertThat(beanB).isNotNull();
}

테스트 실패다. 이유는 간단하다.
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
설정 파일 클래스에서 위 필터 조건을 걸었기 때문에 해당 컴포넌트 클래스는 제외하고 컴포넌트 스캔이 이루어졌다.
그렇다면 BeanA는 빈으로 등록되고, BeanB는 등록되지 않는다는 가정으로 테스트를 진행하자.

@Test
void filterScan() {
	ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
    BeanA beanA = ac.getBean("beanA", BeanA.class);
    assertThat(beanA).isNotNull();

        org.junit.jupiter.api.Assertions.assertThrows(NoSuchBeanDefinitionException.class,
    () -> ac.getBean("beanB", BeanB.class));
}


시원하게 성공이다.

위 조건 코드는 FilterType으로 5가지 옵션이 존재한다.

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

주로 ANNOTATION를 사용하고 가끔 ASSIGNABLE_TYPE을 사용한다니 간단하게 2가지만 알아두면 될 것 같다.

중복 등록과 충돌

컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까?
다음 두가지 상황이 있다.
1. 자동 빈 등록 vs 자동 빈 등록
2. 수동 빈 등록 vs 자동 빈 등록

자동 빈 등록 vs 자동 빈 등록

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우에는 스프링은 오류를 발생시킨다.

  • 이름 중복 예외: ConflictingBeanDefinitionException 예외 발생

수동 빈 등록 vs 자동 빈 등록

수동으로 빈을 등록시켰다는건 개발자의 의도가 더 들어가 있다고 판단하여, 수동 빈 등록이 우선권을 가져 자동 빈을 오버라이딩 한다.

  • 하지만 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.




정리

  • @Configuration@Component를 포함하고 있으며, 이때 애노테이션 간의 상속관계는 존재하지 않고 스프링이 제공하는 기능이다.
  • @ComponentScan는 별도의 설정 정보 없이 자동으로 스프링 빈을 등록시켜주는 기능이며, @Autowired을 통해 의존관계를 자동으로 주입하는 기능과 함께 사용할 수 있다.
  • @ComponetScan@Componet를 탐색해 싱글톤 패턴을 유지하면서 스프링 빈으로 등록하는데 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자로 변경해 사용한다.
  • 탐색 위치와 설정 파일 경로에 따라 모든 자바 클래스와 부가 라이브러리들까지 탐색하는 불상사가 생길 수 있으니, 설정 파일 경로와 탐색 위치를 잘 지정해야된다.

생각 정리

이 부분도 마찬가지로 지난 해커톤과 사이드 프로젝트를 진행할 때 아무 생각없이 @Component@Controller를 함께 작성한 적이 있었다. 아마 오류는 발생하지 않았던걸로 기억하는데... 참.. 컴포넌트 스캔을 공부한 오늘 뒤돌아 생각하면 말도 안되는 코드라고 생각한다..
김영한 강사님의 스프링 기본편을 듣기 전에 너무 어려운 개념들이면 어쩌지.. 라는 생각이 있는데 아직까지는 너무 어렵지 않고, 글로 다시 정리하며 차근차근 생각해보니 잘 이해되고 연결이 잘 된다.
더 열심히 공부해서 과거의 이상한 코드를 작성하지 않고 알맞게 작성할 수 있도록 하자!

profile
GitHub : https://github.com/dudxo

0개의 댓글