Spring Framework/[인프런] Spring 핵심 원리

Chapter 06. 컴포넌트 스캔(@ComponentScan)

계란💕 2022. 8. 14. 14:09

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

 

@ComponentScan

  • 지금까지 배운 내용과 다르게 빈이 많아진다면 하나씩 등록하기 귀찮고 누락하는 문제가 생긴다.
  • 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔 기능을 제공한다.
  • @Autowired: 의존관계를 자동으로 주입한다. 생성자에 붙이면 매개변수에 맞는 것을 찾아와서 의존관계를 주입
  • @ComponentScan은 스프링 빈을 자동으로 끌어 올려준다
  • @ComponentScan을 사용하면 @Configuration이 붙은 설정 정보도 자동으로 등록되므로 AppConfig, TestConfig 등 앞서 만들어두었던 설정 정보도 함께 등록되고 실행되어 버린다.
  • @Configuration 소스코드를 열어보면 @Compomemt가 붙어있기 때문에 @Configuration은 컴포넌트스캔의 대상이 된다.
  • @ComponentScan을 붙이면 @Component이 붙은 모든 클래스를 찾아서 스프링 빈으로 등록해준다.

 

 

  Ex)

<hide/>
package hello.core;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
@Configuration
@ComponentScan(excludeFilters = @ComponentScan.Filter(
                                            type = FilterType.ANNOTATION, 
                                            classes = Configuration.class)  // 제외할 것을 지정해준다.
               )
public class AutoAppConfig {
}

    -  excludeFilters는 제외할 사항을 지정해준다. (실무에서는 굳이 제외하지 않는다.)

      -> Configuration.class를 왜 뺄까?

      -> AppConfig는 수동 등록 중이니까 자동으로 등록되면 안된다.

      -> AppConfig에 붙은 인터페이스 @Configuration에는 이미 @Component가 붙어 있다. 따라서 자동으로 컴포넌트 스캔의 대상이 된다.

 

 

  Note)

  •  MemoryMemberRepo, RateDiscountPolicy, MemberServiceImpl 에 @Component 붙인다. (스캔 대상되고 빈으로 등록)
  • @Autowired를 생성자에 붙이면 스프링이 생성자의 매개변수의 타입에 맞는 것을 가져와서 자동으로 연결해서 의존관계를 주입해준다. (@Autowired 쓰면 생성자에 여러 의존관계를 한번에 주입 가능)
  • @ComponentScan을 쓰면 매핑이 자동으로 되는데 의존관계 설정 방법이 없으니 자연스럽게 @Autowired를 쓰게 된다.

 

  • 이전에 AppConfig에서는 @Bean 으로 직접 설정 정보를 작성했고 의존 관계도 직접 명시했다. 이제는 이런 설정 정보 자체가 없기 때문에 의존관계 주입도 이 클래스 안에서 해결해야한다.

 

 

  Ex) AutoAppConfigTest

<hide/>
package hello.core.scan;
import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class AutoAppConfigTest {

    @Test
    void basicScan(){

        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

    - 테스트 클래스를 만든다.

 

  Note) 실행 결과 - 성공

    - AnnotationConfigApplicationContext를 사용하는 것은 동일한다.

    - 설정 정보로 AutoAppConfig 클래스를 넘겨준다.

    - "ClassPathBeanDefinitionScanner": 스캔할 클래스 후보에 대해 보여준다.

    - 싱글톤을 여러 개 생성했다고 나온다.

    - 주입에 관한 내용도 나온다. 

 

 

(1) @ComponentScan

 

  • @ComponentScan은 @Component가 붙은 모든 클래스를 스프링 컨테이너에 스프링 빈으로 등록한다.(싱글톤)
    • 빈 이름 기본 전략: 이 때 스프링 빈의 기본 이름은 클래스 명을 사용하되 맨 앞 글자는 소문자로 (카멜 표기법)
    • @Component() 의 괄호 안에 빈 이름을 직접 지정할 수도 있다.

 

 

(2) @AutoWired 의존 관계 자동 주입

  • Impl의 생성자에 @AutoWired를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈(MemberRepository)의 타입을 찾아서 주입한다.
    • 여기서 같은 타입이 여러 개인 경우, 충돌한다. 뒤에서 살펴볼 예정
  • 기본 조회 전략에 따라 타입이 같은 빈을 찾아서 주입한다.
    • "getBean(MemberRepository.class)"와 동일하다.
  • 생성자에 매개변수가 많아도 다 찾아서 자동으로 주입한다.

 

 

6.2 탐색 위치와 기본 스캔 대상

 

탐색할 패키지의 시작 위치 지정

  • 꼭 필요한 위치부터 스캔하도록 시작 위치를 지정할 수 있다. @ComponentScan(basePackages= 패키지명)
    • basePackages: 탐색할 패키지의 시작위치를 지정한다. 지정한 패키지부터  하위 패키지를 모두 탐색한다.
    • 지정하지 않는 경우, @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
  • 권장하는 방법: 패키지 위치를 지정하지 않고 설정 정보의 클래스 위치를 프로젝트 최상단에 두는 것이다.
  • ex) 프로젝트가 시작하는 com.hello.service, com.hello.repository 가 있는 경우 com.hello에 메인 설정 정보와 관련된 클래스 AppConfig을  위치시키고 @ComponentScan 애너테이션을 붙이고 basePackage는 생략 

 

 

ComponentScan 기본 대상

  • 컴포넌트 스캔은 @Component 뿐만 아니라 다음 내용도 추가로 대상에 포함한다. (아래의 인터페이스에 모두 @Component가 붙어있다.)
  • @Component: 컴포넌트 스캔에 사용한다. 
  • @Controller: 스프링 MVC 컨트롤러로 인식한다.
  • @Service: 스프링 비즈니스 로직이 여기에 있을거라고 예상할 수 있다. 특별한 처리는 없음.
  • @Repository: 스프링 데이터 접근 계층으로 인식한다. 데이터 계층 예외를 스프링 예외로 변환해준다. ex) JPA, Jdbc
  • @Configuration: 스프링 설정 정보로 인식하고 스프링 빈이 싱글톤을 유지하도록 추가 처리한다.
  • cf) 애너테이션은 상속 개념이 없다. 어떤 애너테이션이 특정 애너테이션에 대한 포함 여부를 인식할 수 있는 것은 자바가 아니라 스프링이 지원하는 기능이다.

 

 

컴포넌트 스캔 용도 뿐만 아니라 다음 애너테이션이 있으면 스프링은 부가 기능을 수행한다.

  • @Controller: 스프링 MVC 컨트롤러로 인식한다.
  • @Repository: 스프링 데이터 접근 계층으로 인식하고 데이터 계층의 예외를 스프링의 추상화된 예외로 바꿔준다.
  • @Configuration: 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.
  • @Service: 핵심 비즈니스 계층을 인식하는데 도움이 된다. 중요한 서비스 로직이 있다.

 

 

 

6.3 필터(filter)

 

필터

  • includeFilters: 컴포넌트 스캔 대상을 추가로 지정한다.
  • excludeFilters: 컴포넌트 스캔에서 제외할 대상을 지정한다.

 

  Ex) 필터

    - @MyIncludeComponent애너테이션을 만든다. 애너테이션이 붙은 것은 컴포넌트 스캔 대상에 추가하겠다는 의미

    - @MyExcludeComponent애너테이션이 붙으면 컴포넌트 스캔에서 제외할 것이다.

<hide/>
package hello.core.scan.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE)	// TYPE은 클래스 레벨에 붙인다는 뜻
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}
<hide/>
package hello.core.scan.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
    
}
<hide/>
package hello.core.scan.filter;
@MyIncludeComponent
public class BeanA {

}

 

<hide/>
package hello.core.scan.filter;
@MyExcludeComponent
public class BeanB {
}

 

<hide/>
@Test
void filterScan(){
    AnnotationConfigApplicationContext ac =
            new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
    BeanA beanA =  ac.getBean("beanA", BeanA.class);
    assertThat(beanA).isNotNull();
//        ac.getBean("beanB", BeanB.class); // 없으니까 예외 발생한다.
    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{
}

 

  Note) 실행 결과  - 성공

    - 빈 A는 컨테이너에 등록되고 B는 등록되지 않는다.

    - 참고로 요즘에는 @Component로 충분하기 때문에 includeFilters를 사용할 일이 없다. 

    - excludeFilters는 가끔 사용하지면 많지는 않다.

    - 스프링 부트는 컴포넌트 스캔을 기본으로 제공하니 기본 설정에 맞추어 사용한다.

 

 

FilterType옵션

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

 

 

 

6.4 중복 등록과 충돌

 

중복 등록과 충돌

  • 자동 빈 등록 vs 자동 빈 등록 => ConflictingBeanDefinitionException 예외 발생
  • 수동 빈 등록 vs 자동 빈 등록 => 수동 빈
    • 수동 빈이 자동 빈을 오버라이드해버린다. 
    • 최근 스프링 부트에서는 수동 빈과 자동 빈이 충돌하면 오류가 발생하도록 기본 값을 바꾸었다.

 

 

  Ex)

    - OrderServiceImpl클래스, MemberServiceImpl 클래스 에 다음과 같이 컴포넌트 애너테이션을 추가한다.

    - 충돌을 발생시키기위해서 둘다 똑같은 이름 "service"를 준다

<hide/>
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import org.springframework.stereotype.Component;
// 추가
@Component("service")
public class OrderServiceImpl implements  OrderService{

    private final MemberRepository memberRepository;    // = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy;    // = new RateDiscountPolicy();

    // 추가
    public MemberRepository getMemberRepository(){
        return memberRepository;
    }

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
            Member member = memberRepository.findById(memberId);    // 회원정보를 조회한다.
            int discountPrice = discountPolicy.discount(member, itemPrice); // 최종적으로 할인된 금액
            return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}
<hide/>
package hello.core.member;
import org.springframework.stereotype.Component;

@Component("service")
public class MemberServiceImpl implements  MemberService{

    private final MemberRepository memberRepository;

    public  MemberRepository getMemberRepository(){
        return  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);
    }
}

 

    - 전에 만들어둔 basicScan ()을 돌린다.

<hide/>
package hello.core.scan;
import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class AutoAppConfigTest {
    @Test
    void basicScan(){

        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

 

  Note) 실행 결과 - BeanDefinitionStoreException,  ConflictingBeanDefinitionException

    - 이미 service라는 빈이 있다고 나온다.

    - Component() 안에 넣어준 이름을 지우면 테스트를 통과한다.

 

 

 

 

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

    - 빈 네임이 memoryMemberRepository인 이유는? 원래 클래스 MemoryMemberRepository가 맨 앞 글자만 소문자로 바뀌어서 컴포넌트로 등록되기 때문이다.

<hide/>
package hello.core;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)  // 제외할 것을 지정해준다.
)
public class AutoAppConfig {
    @Bean(name = "memoryMemberRepository")
    MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}

      Note) 실행 결과

 

============================= 오류 =============================

    - 강사님 화면에는 오버라이드 관련된 설명이 나오는데 내꺼에는 안 나온다.

 

 

  Ex) 스프링 부트를 실행해보자

<hide/>
package hello.core;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CoreApplication {

	public static void main(String[] args) {
		SpringApplication.run(CoreApplication.class, args);
	}
}

 

  Note) 실행 결과

    - 이미 정의가 등록되어 있다.

    - 오바라이딩에 disabled 되어 있다.

 

    - 위 실행 화면에서 권해주는 것에 맞춰서

      -> application.properties에 spring.main.allow-bean-definition-overriding=true를 추가한다.

    - 그 다음, coreApplication을 돌리면?

 

 

 

출처 https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com