framework/spring

⭐️ 스프링 핵심 원리 이해 5 - 컴포넌트 스캔

wooweee 2023. 10. 28. 06:33
728x90

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

  • 컴포넌트 스캔 필요성
    • 등록해야 할 spring bean이 수백개가 되면 일일이 등록하기 힘들고, 설정 정보도 커지고, 누락하는 문제도 발생
    • bean 등록을 해주는 컴포넌트 스캔과 더불어 의존관계를 주입 해주는 @Autowired도 제공
  • @Bean 등록 Overiding, 예외 처리 조건
    • SpringBootApplication 실행 시 : 동일 @Bean 명일 경우 NoUniqueBeanDefinitionException 발생
    • SpringApplication 실행 시 : 동일 @Bean 명이여도 overriding 해버림 - 위험
    • 단위 Test 실행 시 : 동일 @Bean 명이여도 overriding 해버림 - 위험
    • Spring Test 실행 시 : 동일 @Bean 명이여도 overriding 해버림 - 아직 안해봄
  • @ComponentScan : @Component 등록된 것들을 @Bean으로 자동 등록, @ComponentScan을 탐색하진 않는다.
    • java에서 수행한 @ComponentScan
      1. test 경로를 제외한 java에서 @ComponentScan이 시작되는 동일, 하위 경로 탐색
      2. 경로 상관없이 java에서 static으로 설정된 @Configuratoin 파일인 경우
    • test에서 수행한 @ComponentScan
      1. test 경로를 포함한 java에서 @ComponentScan이 시작되는 동일, 하위 경로 탐색
      2. 경로 상관없이 test에서 static으로 설정된 @Configuratoin 파일인 경우
  • @Configuration : 내부에 @Component 존재

  • 정리
    1. java project가 run 할 때는 시발점이 @SpringBootApplication 뿐이다. 해당 파일의 조건에 맞는 @Configuration과 @Component만 bean으로 등록
      cf> @ComponentScan 파일을 만들었다고 무조건 작동한다고 착각하지 말 것

    2. 단위 test의 경우 내가 등록한 applicationContext의 매개변수로부터 @Bean 등록
      cf> 이 때는수동 등록 파일(@AppConfig)인지 자동 등록 파일(@AutoAppConfig)인지 잘 인지해서 사용

@Bean 등록 환경 중복 @Bean 발생 시, 동작 방식
SpringBootApplication NoUniqueBeanDefinitionException
SpringApplication Overriding 과 warnning
단위 Test Overriding 과 warnning
SpringBootTest NoUniqueBeanDefinitionException

 

등록 방식 실행 경로 적용
@ComponentScan(자동) java package java package의 @ComponentScan 시작 경로부터 하위 경로
java package의 경로 상관없이 @Configuration static Class {}
@ComponentScan(자동) test package (단위 test) java package의 @ComponentScan 시작 경로부터 하위 경로
java package의 경로 상관없이 @Configuration static Class {}
test package의 경로 상관없이 @Configuration static Class {}

 

2. 실습 코드

  • 주의점
    • @SpringApplicationContext에서 수행되는 @ComponentScan은 수동등록인 @Configuration과 중복되어도 정상 동작

    • 정상동작한 이유는 등록된 bean은 내용상 같지만 bean의 이름이 다르기 때문에 정상적으로 동작한 것 (헷갈리지 말 것)
      - 사실상, 중복 Bean 이기 때문에 수동 등록 혹은 자동 등록 2개 중 하나만 할 것
      - 만약 자동등록이나 수동등록 파일의 bean이름을 동일하게 지정시, 에러 발생

      * 수동 bean 명 : method명
      * 자동 bean 명 : class명

2.1. 컴포넌트 스캔 실습

    2.1.1. 컴포넌트 스캔 공통

  • java 실행 파일
@SpringBootApplication
public class CoreApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(CoreApplication.class, args);
        for (String beanDefinitionName : context.getBeanDefinitionNames()) {
            System.out.println("beanDefinitionName = " + beanDefinitionName);
        }
    }
}
  • test 실행 파일
public class AutoAppConfigTest {
    @Test
    void basicScan() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        for (String beanDefinitionName : ac.getBeanDefinitionNames()) {
            System.out.println("name = " + beanDefinitionName);
        }
    }
}
  • 수동 설정 파일
package hello.core;

@Configuration // 내부에 @Component 존재
public class AppConfig {
    // select object
    @Bean // spring container 에 넣기
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}
  • 자동 설정 파일
package hello.core;

@Configuration
@ComponentScan(
        basePackages = "hello.core"
//        excludeFilters = @ComponentScan.Filter(
//                type = FilterType.ANNOTATION,  // 타입에 어노테이션
//                classes = Configuration.class) // @Configuration public class Class명 {}
)
public class AutoAppConfig {
}

 

    2.1.2. java 내에서 예외 처리 되는 과정

  • ComponentScan 파일
package hello.core.member;

@Component
public class MemoryMemberRepository implements MemberRepository {
    // 임시 저장소 (db와 동일한 역할): store
    private static Map<Long, Member> store = new HashMap<>(); // 동시성 이슈 때문에 concurrentHashMap을 실무에서는 사용한다.

    @Override
    public void save(Member member) { store.put(member.getId(), member); }
    @Override
    public Member findById(Long memberId) { return store.get(memberId); }
}
  • 실행 결과 : NoUniqueBeanDefinitionException 발생
  • 자동 설정 파일의 @Configuration 때문에 중복 @Bean 설정 됨
  • 만약 자동 설정 파일 @Configuration이 없으면 수동과 자동 등록만 존재하므로 정상 동작 (헷갈림 주의)

 

    2.1.3. test 파일에서는 overriding 되는 과정

  • 실행 결과 : Overriding 예외만 발생 및 test의 static @Configuration Bean도 등록
  • 이렇게 되는 상황이 위험한 경우 이기 때문에 test 정확도를 높이기 위해서 필요없는 @bean은 등록하지 않기 위해서 excludeFilters 조건을 건다.

 

2.2.  컴포넌트 스캔 적용

  • spring container 설정 과정
    1. 모든 @Component 찾아서 등록 (의존관계 주입을 바로 수행하지 않는다.)
    2. 의존 관계 주입을 수행 (@Component가 다 등록 된 이후에 수행)
  • 하지만 예외적으로 생성자 주입이나 수동 bean 설정 같은 경우에는 java code 특성상 1,2, 단계가 동시에 진행

    2.2.1. spring bean 등록

  • @ComponentScan - @Component
    1. @ComponentScan : @Component 붙은 클래스를 스캔해서 스프링 빈으로 등록
    2. @Component: springContainer에 bean으로 등록
      • @Bean 이름 : class명 소문자로 작성 - default
      • @Component("직접지정하는 Bean 이름")
  • @Configuration : @Component 가 존재하므로 스프링빈으로 등록

  • 심화
    • @ComponentScan이 적용된 AppConfig는 실제로 @Configuration이 필요가 없다.
      하지만 관례상 작성을 한다.
    • @Component 로 spring Contaioner에 등록 된 객체는 proxy와 CGLIB를 통해서 싱글톤이 유지된다.

    2.2.2. spring bean 의존 관계 주입

  • @Autowired
    • 이전 처럼 생성자 주입 java code 작성을 통한 의존관계 주입이 불가
    • 실제 @Component class의 생성자에 @Autowired를 작성하여 DI를 수행

    • DI 과정 및 문제
      • 먼저 springContainer 내에 동일 type의 bean 찾고 DI 수행
        • ex) MemberRepository type 이면 bean type이 MemberRepository부터 하위 타입 모두 찾는다.
        • getBean(매개변수의 type.class)로 찾는다.  ex) getBean(MemberRepository.class)
      • 문제 상황
        1. 빈 없으면 NoSuchBeaDefinitionException 발생
        2. getBean할 때 동일 type bean 많으면 NounqieException 발생
@Component
public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) { // type: MemberRepository
        this.memberRepository = memberRepository;
    }
}
  • AppConfig
// CompenentScan 적용
@Configuration // 관례상 작성, ComponentScan으로 다 해결 가능
@ComponentScan
public class AppConfig{}

// ComponentScan 하기 전
@Configuration
public class AppConfig{
    @Bean
    public MemberService memberService(){ return new MemberServiceImpl(memberRepository()); }
    @Bean
    public Orderservice orderService(){ return new OrderServiceImpl(memberRepository(), discoutnPolicy()) }
    @Bean
    public MemberRepository memberRepository(){ return new MemoryMemberRepository(); }
    @Bean
    public DiscountPolicy discoutnPolicy(){ return new FixDiscountPolicy(); }
}
  • @Component, @Autowired code
// MemoryMemberRepository
@Component
public class MemoryMemberRepository implements MemberReposotory{
	''' 생략 '''
}

// RateDiscountPolicy
@Component
public class RateDiscountPolicy implements DiscountPlicy{
	''' 생략 '''
}

// MemberServiceImpl
@Component
public class MemberServiceImpl implements MemberService{
	
    private final MemberRepository memberRepository;
    
    @Autowired
    public MemberServiceImpl(MemoryMemberRepository memberRepository){
    	this.memberRepository = memberRepository;
    }
    
	''' 생략 '''
}

// OrderServiceImpl
@Component
public class OrderServiceImpl implements OrderService{

	private final MemberRepository memberRepository;
    pirvate final DiscountPolicy discountPolicy;
    
    @Autowired
    OrderServiceImpl(MemberRepository memberRepository, DiscountPolciy discountPolicy){
    	this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}
더보기

AppConfig의 방식을 생각

1. Spring bean으로 등록 되야 하는 부분을 @component로 등록 - 구체화가 되어진 class에 붙인다. bean에는 객체가 들어가져야 한다.

2. 의존 관계 주입은 ServiceImpl에 생성자를 보면 이 부분이 DI를 하는 부분이므로 @Autowired를 넣어준다.

    그러면 스프링 컨테이너가 @Autowired 된 생성자의 paramsTYPE과 같은 bean을 찾는다. 존재하면 의존관계를 주입한다.

    생성자에 params가 아무리 많아도 다 찾아서 자동 주입한다.

  • 등록 과정 그림

component 등록 - 의존관계 주입

 

3. 탐색 위치와 기본 스캔 대상

3.1. 탐색 범위

  • 탐색 범위를 지정하는 이유는 모든 자바 클래스를 다 스캔하려면 시간이 오래 걸리기 때문

  • 탐색 위치
    • default: @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치
    • basePackages 존재시, 해당 경로가 시작 위치
package hello.core; // basPackages 지정안할 경우 default scanning 시작 위치

@Configuration
@ComponentScan(
        basePackages = "hello.core.member",
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
}
  • 탐색 위치 지정
    1. 자동
      • 설정 정보 클래스의 위치를 프로젝트 최상단에 두기 (권장)
      • 최근 스프링 부트도 이 방법을 기본으로 제공
      • springboot: @SpringBootApplication 프로젝트 시작 루트 위치에 두는 것이 관례
    2. 수동
      • @ComponentScan( basePackages = "hello.core")
      • basePackages: 탐색할 패키지의 시작 위치 지정. 해당 패키지 부터 하위 패키지 까지

 

3.2. 탐색 대상

  • ComponentScan 기본 대상 - 아래 어노테이션들이 @Component를 포함
    • @Component : 컴포넌트 스캔에서 사용
    • @Controller : 스프링 MVC 컨트롤러에서 사용
    • @Service : 스프링 비즈니스 로직에서 사용
    • @Repository : 스프링 데이터 접근 계층에서 사용
    • @Configuration : 스프링 설정 정보에서 사용
    • @ControllerAdvice : 전역에 존재하는 예외를 잡아주는 역할

 

  • 참고
    • ComponentScan은 useDefaultFilters  옵션은 기본으로 켜져있는데, 이 옵션을 끄면 기본 스캔 대상들이 제외
    • 사실 애노테이션에는 상속관계라는 것이 없다.
    • 애노테이션이 특정 애노테이션을 들고 있는 것을 인식할 수 있는 것은 자바 언어가 지원하는 기능은 아니고, 스프링이 지원하는 기능

  • 스프링 지원 기능
    • @Component 기능을 포함하고 부가 가능을 수행 가능
      • @Controller : 스프링 MVC 컨트롤러로 인식  
      • @Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환
      • @Configuration : 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가처리   
      • @Service : @Service는 특별한 처리안한다. 개발자들이 핵심 비즈니스 로직 계층을 인식하는데 도움

 

4. 필터

  • 사전 주의
    • @Component면 충분하기 때문에 includeFilters(거의 안씀), excludeFilter(간혹 씀) 로 옵션 변경해가면서 사용하지 말 것
    • spring의 기본 설정에 최대한 맞추어 사용하는 것을 권장
  • includeFilters: @ComponentScan 대상을 추가로 지정
  • excludeFilters: @ComponentScan에서 제외할 대상을 지정

4.1. code

  • include interface & class
package hello.core.scan.filter;

import java.lang.annotation.*;

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

@MyIncludeComponent
public class BeanA {}
  • exclude interface & class
package hello.core.scan.filter;

import java.lang.annotation.*;

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

@MyExcludeComponent
public class BeanB {}

 

package hello.core.scan.filter;

public class ComponentFilterApaConfigTest {

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

        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 {}
}

 

4.2. FilterType 옵션

  1. ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다. - 이것만 거의 씀. 나머지는 그냥 있다 정도
  2. ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다. - 어쩌다가 씀

  3. ASPECTJ: AspectJ 패턴 사용
  4. REGEX: 정규 표현식
  5. CUSTOM:  TypeFilter 이라는 인터페이스를 구현해서 처리

 

 

5. 중복과 충돌 (다른 책으로 한번 더 찾아보기 - 애매함)

  • case
    1. 자동 vs 자동
    2. 수동 vs 자동

5.1.  자동 빈 등록 vs 자동 빈 등록

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

 

 

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

// 수동 bean 등록
public class AutoAppConfig {
    @Bean(name = "memoryMemberRepository")
    MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}
  • 스프링
    • 수동 빈 등록이 우선권을 가진다.
    • 수동 빈이 자동 빈을 오버라이딩 해버린다.
    • 그래도 주의 log는 날려준다.
log: Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing

 

  • 스프링 부트 -> 충돌이 안나는 케이스도 있고 너무 애매함. 다른 문서도 찾아봐서 확실히 할 필요가 있다.
    • 추측
      1. 확실한 것은 수동x수동으로 빈이름이 중복날 가능성이 낮기 때문에 이부분들이 되게 애매하게 예외처리가 되고
      2. 중복 발생 가능성이 높은 자동x수동, 자동x자동의 경우는 무조건 예외가 터지도록 되어있다.

    • 만약 잡기 어려운 bean 중복 에러가 발생 혹은 수동 bean 등록을 할 경우에는 더 조심해야하고 이부분에 대해선 tdd를 꼭 수행하도록 하자. 명확하지 않기 때문

    • 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값이 설정 되었다.
    • @SpringBootApplication 으로 동작할 때 에러 발생
    • 만약 spring 처럼 overriding 하고 싶은 경우 설정 변경 방법
# application.properties
# spring 처럼 자동, 수동 bean 동일 이름일 때 overriding 하기 위한 설정
spring.main.allow-bean-definition-overriding=true; # default가 false

 

 

 

이전 발행글 : 스프링 핵심 원리 이해 4 - 싱글톤 컨테이너

 

다음 발행글 : 스프링 핵심 원리 이해 6 - 의존관계 자동 주입

 

 

 


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