유정잉

🎀 스프링 기본 4 - 컴포넌트 스캔 본문

개발자 공부/🎀 스프링 공부

🎀 스프링 기본 4 - 컴포넌트 스캔

유정♡ 2024. 5. 27. 12:52

@ComponentScan : 클래스를 스캔해서 스프링 빈으로 등록해줌 (기존AppConfig와 다르게 @Bean으로 등록 X)

컴포넌트 스캔을 사용하면 `@Configuration` 이 붙은 설정 정보도 자동으로 등록되기 때문에, AppConfig, TestConfig 등 앞서 만들어두었던 설정 정보도 함께 등록되고, 실행되어 버린다. 그래서 `excludeFilters` 를 이용해서 설정정보는 컴포넌트 스캔 대상에서 제외했다. 보통 설정 정보를 컴포넌트 스캔 대상에서 제외하지는 않지만, 기존 예제 코드를 최대한 남기고 유지하기 위해서 이 방법을 선택했다.

 

1) AutoAppConfig에 @ComponentScan 등록

package hello.core;


import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan( //class를 자동으로 Bean으로 등록해줌
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {


}

 

2) 각 클래스에 @Component 추가 , 생성자에 @Autowired 추가 

 

3) @Test Bean 잘 생성 됐는지 

package hello.core.scan;

import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.Assertions.*;

public class AutoAppConfigTest {

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

 


[ 탐색 위치와 기본 스캔 대상 ] 

basePackage : 탐색할 패키지의 시작 위치를 지정. 지정한 패키지를 포함해서 하위 패키지를 모두 탐색

basePackage를 지정하지 않으면 hello.core부터 시작해 모든 하위 패키지를 다 스캔(권장하는 방법은 패키지 위치를 지정하지 않는 것)

(스프링부트를 사용하면 @SpringBootApplication 사용 -> @ComponentScan 사용할 일 없음)

컴포넌트 스캔은 `@Component` 뿐만 아니라 다음과 내용도 추가로 대상에 포함한다.
`@Component` : 컴포넌트 스캔에서 사용
`@Controller` : 스프링 MVC 컨트롤러에서 사용
`@Service` : 스프링 비즈니스 로직에서 사용
`@Repository` : 스프링 데이터 접근 계층에서 사용
`@Configuration` : 스프링 설정 정보에서 사용

 


[ 필터 ]

includeFilters : 컴포넌트 스캔 대상을 추가로 지정 

excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정

 

1) scan/filter 패키지에 MyIncludeComponent와 MyExcludeComponent 두개 Annotation 생성(클래스x)

package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}
package hello.core.scan.filter;

@MyIncludeComponent
public class BeanA {
}
package hello.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}

 

 

2) @Test ComponentFilterAppConfigTest

package hello.core.scan.filter;

import org.junit.jupiter.api.Assertions;
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.assertThat;
import static org.springframework.context.annotation.ComponentScan.Filter;

public class ComponentFilterAppConfigTest {

    @Test
    void filterScan() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA", BeanA.class);
        assertThat(beanA).isNotNull();
        Assertions.assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("beanB", BeanB.class));
    }

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


`includeFilters`MyIncludeComponent` 애노테이션을 추가해서 BeanA가 스프링 빈에 등록된다.

`excludeFilters`MyExcludeComponent` 애노테이션을 추가해서 BeanB는 스프링 빈에 등록되지 않는다.

 


[ 중복 등록과 충돌 ]

컴포넌트 스캔에서 같은 빈 이름이 등록이 되면? 

   1. 자동 빈 등록 vs 자동 빈 등록

   2. 수동 빈 등록 vs 자동 빈 등록

 

1) 자동 빈 등록 vs 자동 빈 등록

    만약 OrderServiceImple과 MemberSerivceImpl의 @Component에 둘다 이름을 "service"로 설정한다면 아래와 같은 오류발생

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'service' defined in file [/Users/yujung/Documents/SpringStudy/core/out/production/classes/hello/core/member/MemberServiceImpl.class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'hello.core.member.MemberRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

 

2) 수동 빈 등록 vs 자동 빈 등록

     만약 MemoryMemberRepository에 @Component등록,

     AutoAppConfig에 @Bean(name="memoryMemberRepository") 등록

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

 

스프링 부트인 `CoreApplication` 을 실행해보면 오류를 볼 수 있다.

728x90