스프링 컨테이너, 컴포넌트 스캔

    들어가기 전

    이 글은 인프런 김영한님의 강의를 복습하며 작성한 글입니다. 

     


    스프링 컨테이너와 ComponentScan

    앞서 스프링 컨테이너에서는 @Configuration + @Bean 조합을 통해서 스프링 컨테이너에 등록할 빈을 지정했다. 이것은 스프링 빈을 등록하는 설정 파일을 스프링 빈으로 등록하고, 그 설정파일 빈을 바탕으로 필요한 스프링 빈으로 추가로 등록하는 작업을 한 것이다. 

    등록해야 할 스프링 빈이 얼마없다면 이것으로 충분하지만, 등록할 빈이 수백 개가 넘어간다면 시간 낭비가 될 수 있고, 등록해야 할 스프링 빈이 누락되는 문제가 발생할 수 있다. 이런 것들을 보완하기 위해 'Component Scan'이라는 기능을 스프링은 제공한다. Component Scan은 간편하게 스프링 빈을 등록할 수 있는 방법을 제공한다. 

    Component Scan은 스프링 빈을 생성하는 작업을 한다. Component Scan에서 의존성 주입을 하기 위해서는 @Autowired 어노테이션을 이용하면 된다. 

    컴포넌트 스캔을 이용해서 스프링 빈을 등록하기 위해서는 아래 작업을 진행하면 된다. 

    1. AutoAppConfig 클래스를 하나 만들고, @Configuration + @ComponentScan 어노테이션을 달아준다.
    2. 스프링 빈으로 등록하고 싶은 클래스에세 @Component 어노테이션을 달아준다. 
    3. @Component가 달린 클래스들 중 의존관계 설정이 필요한 클래스들의 의존관계를 @Autowired를 이용해서 설정한다. 

    @Confiugarion도 컴포넌트 스캔 대상이다.

    Component Scan은 @Component 어노테이션을 가진 모든 클래스를 스프링 빈으로 등록한다. @Configuration은 내부에 @Component 어노테이션을 가지고 있다. 따라서 @Configuration이 붙은 클래스는 컴포넌트 스캔의 대상이 되어 스프링 빈으로 등록된다. 

     

     


    실습해보기

    아래에서는 Component Scan을 이용한 스프링 빈 등록 및 의존성 주입을 실행해보는 실습 코드를 작성했다. 순서에 맞춰서 진행하면 자동으로 스프링 빈이 등록된다. 

    1. AutoAppConfig 자바 클래스 만들기

    @Configuration
    @ComponentScan(
            // 기존에 있던 AppConfig를 불러오지 않기 위해 excludeFilter를 등록해준다.
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
    public class AutoAppConfig {
    }

    위의 자바 클래스를 만들어준다. AutoAppConfig 클래스만 있다고 가정했을 때는 excludeFilters는 따로 넣을 필요가 없다. ( excludeFilters의 용도는 기존에 등록되어있는 AppConfig를 자동으로 스캔하지 않기 위해 설정한다.) @Configuration이 컴포넌트 스캔의 대상이 된 이유는 @Component 어노테이션이 붙어있기 때문이다.

     

    2. 각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component 어노테이션을 추가해준다.

    package myname.core2.discount;
    import myname.core2.member.Grade;
    import myname.core2.member.Member;
    import org.springframework.stereotype.Component;
    
    @Component
    public class RateDiscountPolicy implements DiscountPolicy{
        private int discountRate = 10;
        @Override
        public int discount(Member member, String itemName, int itemPrice) {
            if (member.getGrade() == Grade.VIP){
                return (int) (itemPrice * discountRate / 100);
            }else{
                return 0;
            }
        }
    }
    • @Configuration + @Bean 조합이 적용되었던 클래스들에 대해서 @Component를 붙여준다.

     

    3. @Component가 붙은 클래스들의 의존관계를 설정한다. 

    public class OrderServiceImpl implements OrderService{
    //    DiscountPolicy discountPolicy = new FixDiscountPolicy();
    //    MemberRepository memberRepository = new MemoryMemberRepository();
    
        DiscountPolicy discountPolicy;
        MemberRepository memberRepository;
    
        @Autowired
        public OrderServiceImpl(DiscountPolicy discountPolicy, MemberRepository memberRepository) {
            this.discountPolicy = discountPolicy;
            this.memberRepository = memberRepository;
        }
    • 이전에는 @Configuration이 붙은 설정 클래스 내에서 @Bean을 통해 직접 스프링 빈을 주입했다. 이 때, 의존관계는 설정 파일 내부에서 메서드를 호출하는 형태로 명시했었다. 
    • 컴포넌트 스캔에서는 직접 명시할 수 없기 때문에 의존관계 설정을 위해 @Autowired 어노테이션을 이용해야한다. 

     

    컴포넌트 스캔 테스트 코드

        @Test
        @DisplayName("Component Scan 진행")
        void test1(){
            ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
            String[] beanDefinitionNames = ac.getBeanDefinitionNames();
    
            for (String beanDefinitionName : beanDefinitionNames) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("beanDefinitionName = " + beanDefinitionName + " bean = " + bean);
            }
        }

    위 코드를 작성해서 컴포넌트 스캔이 정상 동작하는지 확인할 수 있다. 아래 콘솔처럼 정상적으로 Component path를 따라서 생성되고 있는 것을 확인할 수 있다.

     


    컴포넌트 스캔 + 자동 의존관계 주입 동작 방식

    1. @ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다. 이 때 스프링 빈의 기본 이름은 클래스명이며, 가장 앞글자만 소문자다.

    2. AutoWired 자동관계 주입

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

    생성자에 @Autowired 어노테이션이 있으면, 스프링 컨테이너는 매개변수에 해당되는 스프링 빈을 찾아서 주입한다. 스프링 빈을 찾는 기본 전략은 타입이 같은 스프링 빈을 먼저 찾는다. 만약 타입이 같은 스프링 빈이 여러 개가 있다면, 매개변수의 이름과 같은 스프링 빈을 등록한다. 

     


    컴포넌트 스캔 탐색위치 설정 → 컴포넌트 스캔 시간 단축 

    컴포넌트 스캔은 기본적으로는 모든 자바 클래스를 대상으로 스캔을 한다. 따라서 @ComponentScan에 아무런 설정도 하지 않을 경우 모든 자바 클래스를 살펴보기 때문에 컴포넌트 스캔에 많은 시간이 소요될 수 있다.

    컴포넌트 스캔 시간을 단축하기 위해서 컴포넌트 스캔의 탐색 위치를 설정할 수 있다. 

     

    package myname.core2;
    @Configuration
    @ComponentScan(
            // 기존에 있던 AppConfig를 불러오지 않기 위해 excludeFilter를 등록해준다.
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class),
            basePackages = {"myname.core2"})
    public class AutoAppConfig {
    }
    • BasePackages
      • 탐색할 패키지의 위치를 지정한다. 이 패키지를 포함한 하위 패키지까지 전부 탐색한다. 콤마를 사용해 여러 개 설정도 가능하다
      • 지정하지 않았을 경우 : @ComponentScan이 붙은 패키지 전체를 살펴본다.

    myname.core2 패키지 전체를 탐색한다.

    권장하는 방법은 @ComponentScan을 프로젝트 최상단에 위치하게 둔 후 아무것도 설정하지 않는 방법이다. 프로젝트 최상단부터 아래 패키지까지만 살펴보게 된다. 이렇게 작성하는 이유는 여러 개발자가 협업하는 경우 어디까지가 컴포넌트 스캔의 대상이 되는지 명확하지 않기 때문에 다음과 같이 작성한다. SpringBoot에서도 유사한 형태로 사용하는 것을 확인할 수 있다. 

    SpringBoot 코어 어플리케이션에는 @SpringBootApplication 어노테이션이 붙어있다. 어노테이션을 타고 들어가보면 @ComponentScan이라고 붙어있다. SpringBoot 코어는 항상 Java Project 최상단에 존재한다는 것을 감안한다면, 거의 똑같다고 볼 수 있겠다.

     


    컴포넌트 스캔 탐색 대상

    컴포넌트 스캔의 기본 탐색 대상은 @Component 어노테이션이 붙은 클래스가 대상이 된다.  일반적으로는 @Component, @Controller, @Service, @Repository, @Configuration이 되는데, 이것은 @Configuration, @Service, @Repository 어노테이션이 내부적으로 @Component 어노테이션을 가지고 있기 때문이다. 

    이렇게 어노테이션을 '상속'하는 것처럼 보이는 것은 Java 언어가 지원하는 것은 아니다. 스프링 프레임워크가 지원하는 기능이다. 어노테이션이 붙으면, Component 스캔에 들어가는 것뿐만 아니라 지정된 특정 기능을 수행하기도 한다. 예를 들어 @Controller는 스프링 MVC 컨트롤러로 인식된다.

     


    필터를 활용한 컴포넌트 스캔 제어

    @ComponentScan 어노테이션에 includeFilters, excludeFilters를 지정하면 컴포넌트 스캔 대상을 추가할 수도 있고 뺄 수도 있다. 아래 테스트 코드에서 예시를 살펴볼 수 있다.  수동 설정 Annotation은 한 칸 더 아래 코드에서 확인이 가능하다. 

     

    코드 설명

    • static class에서 includeFilters를 통해서 @Component를 제외한 다른 어노테이션을 넣는 코드를 작성했다.  @Component를 제외한 다른 어노테이션도 컴포넌트 스캔의 대상이 된다.
    • 동작은 다음과 같다.
      • filterType.ANNOTATION을 설정하고 MyIncludeComponent를 지정했다. 이 말은 @MyIncludeComponent 어노테이션을 가진 모든 클래스를 Component Scan의 대상으로 사용하라는 의미다. 
      • 그런데 이렇게 하기 보다는 @Component 어노테이션을 하나 붙이는 것이 더 편리하고 적절할 것 같다. 
    // 테스트 코드
    public class ComponentFilterAppConfigTest {
        @Test
        @DisplayName("필터 이름 써서 해보기")
        void test1(){
            ApplicationContext ac = new AnnotationConfigApplicationContext(TestAppConfigTest.class);
            BeanA beanA = ac.getBean("beanA", BeanA.class);
            System.out.println("beanA = " + beanA);
    
            //아래 Bean은 Filter 처리 되었음으로 당연히 없다.
            //BeanB beanB = ac.getBean("beanB", BeanB.class);
            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 TestAppConfigTest {
        }
    }

    아래는 테스트 코드 구동을 위한 나머지 코드다.

    • 먼저 어노테이션을 생성했고, 어노테이션의 이름은 MyIncludeComponent, MyExcludeComponent다.
    • 각 어노테이션은 내부에 @Component 어노테이션이 없어도 괜찮다. 왜냐하면 Component Scan의 대상에 포함될지, 배제될지를 @ComponentScan의 includeFilters, excludeFilters를 이용해서 설정하기 때문이다. 

     

    package myname.core2.scan.filter;
    @MyIncludeComponent
    public class BeanA {
    }
    
    package myname.core2.scan.filter;
    @MyExcludeComponent
    public class BeanB {
    }
    
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface MyExcludeComponent {
    }
    
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface MyIncludeComponent {
    }

     


    컴포넌트 스캔 필터 타입

    컴포넌트 스캔의 필터 타입은 아래 종류가 있고, 각각은 아래 내용을 묘사한다고 한다. 그런데 위에서 이야기 했던 것처럼 includeFilter는 거의 사용하지 않는다고 한다. 왜냐하면 @Component만으로 충분하기 때문이다. @ExcludeFilters는 간혹 사용하기는 한다고 하지만, 마찬가지로 크게 사용하진 않는다고 한다.

    ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다

    ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.

    AspectJ : Aspect J 패턴 사용

    REGEX: 정규 표현식

    CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리

     

    컴포넌트 스캔 중복 등록과 충돌

    컴포넌트 스캔을 했을 때, 같은 빈 이름이 두 개가 있을 때는 어떻게 될까?

    1. 자동 빈 등록 + 자동 빈 등록

    컴포넌트 스캔에 의해 자동으로 빈 등록이 된다. 만약 이 때, 이름이 같은 경우 스프링이 자동으로 오류를 띄워준다. 예를 들어 MemberServiceImpl과 OrderServiceImpl에 @Component("Service")라는 이름을 달아주었다고 하자. 이 경우, Service를 둘다 달아주면 이름이 충돌해서 자동 빈 등록이 실패된다. AutoConfigTest를 돌려보면 충돌되는 것이 확인된다.

     

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

    수동 빈 등록 + 자동 빈 등록을 하게 되면, 수동 빈 등록이 우선권을 가진다. 예전에 상속 관계에서 오버라이딩 된 것처럼 수동 빈 등록이 오버라이딩 된다고 한다. 아래처럼 AutoAppConfig에서 @Bean을 수동으로 등록을 해준다. @Bean을 수동으로 등록을 해줄 때 이름을 지정을 할 수 있다.

    return new MemoryMemberRepository()에 있는 생성자가 스프링 빈 컨테이너에 등록될 것인데, 이 때 MemoryMemberRepository의 이름을 그대로 쓰되, 가장 앞에 있는 글자만 소문자가 되는 것을 알고 있기 때문에 빈 이름을 저렇게 설정했다.

    public class AutoAppConfig {
        @Bean(name = "memoryMemberRepository")
        MemberRepository memberRepository(){
            // MemoryMemberRepository는 @Component이기 때문에 소문자로 memoryMemberRepository가 생성될 것임.
            return new MemoryMemberRepository();
        }
    }

    테스트 코드는 아래를 참고

    public class BasicScan {
    
        @Test
        @DisplayName("Component Scan 진행")
        void test1(){
            ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
            String[] beanDefinitionNames = ac.getBeanDefinitionNames();
    
            for (String beanDefinitionName : beanDefinitionNames) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("beanDefinitionName = " + beanDefinitionName + " bean = " + bean);
            }
        }
    }

    실행을 하게 되면 실행 문제는 없으나, 아래와 같이 컴파일 과정에서 Override된 것이 명시된다. 그런데 이런 것은 사실 지양해야한다. 왜냐하면 어마어마하게 많은 양의 빈이 돌아가고 있고 협업 중인데, 이렇게 된다면 꼬이게 된다.

    이런 이유 때문에 최근에는 SpringBoot 메인으로 실행하게 될 경우, 시작하자마자 Error를 띄우도록 설정 해버렸다. 스프링부트 메인을 실행할 경우, 아래와 같이 Error가 바로 발생하는 것을 확인할 수 있다. 

     

    댓글

    Designed by JB FACTORY