[ 스프링 공부 ] 컴포넌트 스캔

2022. 1. 29. 00:38백엔드

 

지난번 글에서는 싱글톤 컨테이너에 대해서 정리해보았다. 싱글톤 디자인이란 여러 클라이언트가 서비스를 호출할때, 여러개의 인스턴스가 생성되는 것이 아니라, 오로지 단일 인스턴스를 활용하여 모든 서비스를 실행할 수 있도록 해주는 것이다.

 

이렇게 디자인을 할 경우 메모리 낭비를 막을 수 있고, 모든 서비스가 한 Reference를 공유하므로, 내용이 버그 수정에 용이하다는 장점이 있었다.

 

그에 반해 무상태로 코드를 작성해야하고, 특정 자바 코드를 싱글톤으로 만들기 위해서는 너무 많은 코드를 수정해야 한다는 단점이 있었다.

 

하지만, 스프링은 이러한 것들을 모두 해결하여 스프링 빈을 싱글톤으로 디자인하였고, 이것들을 담고 있는 스프링 컨테이너 마저도 싱글톤으로 디자인하였다. 

 

크게 고민할 필요없이 스프링 설정 정보에는 항상 @Configuration 어노테이션을 사용하도록 하자고 했다.

 

이번글에서는 컴포넌트 스캔에 대해서 공부해볼 예정이다.

 

스프링 빈을 스캔하고 각 스프링 빈간에 의존관계를 주입해볼 예정이다. 그리고 각 컴포넌트들을 필터링하여 스캔하는 방법에 대해서 알아볼 것이다.


컴포넌트 스캔이란 스프링 설정 정보가 없어도 자동으로 스프링 빈을 등록해주는 기능이다.

 

그리고 의존관계도 자동으로 주입이 가능한데, 바로 @Autowired 어노테이션을 활용하는 것이다.

 

코드로 컴포넌트 스캔에 대해서 알아보도록 하자!

 

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

 

AppConfig 클래스가 상당히 짧아진것을 볼 수 있다. 동일하게 @Configuration 어노테이션을 활용하였지만, 전혀 클래스 내부에는 내용이 없고 @ComponentScan 어노테이션이 활용된 것을 볼 수 있다.

 

@ComponentScan 어노테이션 코드의 내용에 대해서 설명해보도록 하겠다. excludeFilters 를 활용하여 제외할 컴포넌트를 설정하였다. 그리고 정의하길, Configuration.class 어노테이션이 포함되어 있는 컴포넌트들을 제외하겠다는 뜻이다.

 

@Bean 어노테이션을 활용하여 등록된 클래스가 하나도 없다는 것을 볼 수 있다.

 

위 코드에서 @Configuration을 제외한 이유는 기존 예제 코드를 최대한 유지하기 위함이다. 이제 각 클래스가 컴포넌트 스캔의 대상이 되도록 부분 수정을 하여야 한다.

 

@Component
public class MemoryMemberRepository implements MemberRepository {}

@Component
public class RateDiscountPolicy implements DiscountPolicy{}

@Component
public class MemberServiceImpl implements MemberService{}

@Component
public class OrderServiceImpl implements OrderService{}

위와 같이 우리가 스프링 빈으로 등록하여 사용할 클래스 위에 @Component 어노테이션을 달아주는 것이 전부이다.

 

추가로 MemberServiceImpl 클래스와 OrderServiceImpl 클래스의 생성자 부분에 @Autowired 어노테이션을 달아주면 자동으로 의존관계가 주입된다.

 

참고로 @Autowired 어노테이션을 활용하면 생성자에서 여러 컴포넌트가 동시에 의존관계가 주입될 수 있다!

 

제대로 동작하는지 확인해보자!

 

package hello.core.scan;

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

public class AutoAppConfigTest {

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

}

 

코드를 해석해보도록 하자. 동일하게 AutoAppConfig.class를 Parameter로써 넘겨 스프링 컨테이너를 생성한다.

 

그리고 스프링 컨테이너에서 MemberService 클래스로 정의된 스프링 빈을 호출하고, 그것이 MemberService 클래스로 생성된 인스턴스인지 확인하는 코드이다.

 

실행해보면 초록색으로 테스트 성공 결과를 확인할 수 있다.

 

추가로 로그를 살펴보면 컴포넌트 스캔이 잘 작동하여 RateDiscountPolicy와 MemberServiceImpl, MemoryMemberRepository, OrderServiceImpl 클래스가 컴포넌트로 올라가 있는 것을 확인할 수 있다.

 

@ComponentScan 어노테이션은 @Component 어노테이션이 붙은 모든 클래스를 스프링 빈으로 등록한다.

 

빈 이름에 대한 기본 전략은 첫글자만 소문자로 바꾸어 스프링 빈으로 등록된다. (물론 ()를 이용하여 직접 이를을 지정할 수 있다)

 

생성자에 @Autowired 어노테이션을 지정하면, 스프링 컨테이너가 자동으로 해당되는 스프링 빈을 주입한다.

 


 

컴포넌트 스캔을 모든 자바 클래스에 대해서 실시하게 되면 시간도 오래 걸리고 메모리 낭비가 심할 것이다. 그래서 필요한 위치에서 탐색하도록 설정할 수 있다.

 

@ComponentScan(
	basePackages = "hello.core"
)

 

위와 같은 코드를 추가함으로써 컴포넌트 스캔 위치를 지정해줄 수 있다. 해당 패키지 이하의 클래스만 스캔한다는 것이다.

 

만약에 지정하지 않을 경우 @ComponentScan 어노테이션이 붙은 설정 정보 클래스의 패키지가 스캔 시작 위치가 된다.

 

그러므로 설정 정보를 담고 있는 클래스는 프로젝트의 최상단에 두는 것을 권장한다.

 

컴포넌트 스캔의 기본 대상은 @Component 뿐만 아니라, @Controller, @Service, @Repository, @Configuration 어노테이션도 포함이 되는데, 이것들의 소스 코드를 보면 모두 @Component가 포함되어 있다.

 


 

이번에는 필터를 사용하여 컴포넌트 스캔 대상을 지정해보도록 하자.

 

package hello.core.scan.filter;

import java.lang.annotation.*;

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

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}

 

위 두개의 어노테이션을 직접 생성하자! 이름은 직관적으로 포함시킬 컴포넌트와 제외시킬 컴포넌트로 정의하자.

 

@MyIncludeComponent
public class BeanA {
}

@MyExcludeComponent
public class BeanB {
}

두가지 클래스를 추가로 정의하여 각각 @MyIncludeComponent와 @MyExcludeComponet 어노테이션을 달아 놓자.

 

package hello.core.scan.filter;

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

public class ComponentFilterAppConfigTest {
    @Test
    void filterScan(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA",BeanA.class);
        Assertions.assertThat(beanA).isNotNull(); //beanA는 필터에서 포함하기로 했으니 존재해야한다.

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

 

내가 만든 어노테이션이 잘 작동하는지 확인하는 테스트 코드이다. static class를 하나 생성하고 @Configuration 어노테이션과 @ComponentScan 어노테이션을 사용하는데, 내용은 @MyIncludeComponent는 컴포넌트로 포함시키고, @MyExcludeComponet는 컴포넌트에서 제외시킨다는 것이다.

 

@Test 어노테이션이 붙어있는 메서드의 내용을 살펴보자

 

아까 static으로 생성한 클래스를 기준으로 스프링 컨테이너를 생성하였다. 그리고 BeanA 클래스로 저장된 스프링 빈을 호출하였을때는 존재하는지를 확인하고 BeanB 클래스로 저장된 스프링 빈을 호출하였을때는

NoSuchBeanDefinitionException

위와 같은 Exception이 발생하는지 확인하는 코드이다.

 

당연히 결과는 테스트 성공이다 ㅎㅎ

 

사실 FilterType 옵션에는 어노테이션만 있는 것이 아니다.

 

출처 : 인프런 강의자료

 


 

그렇다면 컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까?

 

우선 자동으로 등록된 빈끼리 중복이 됐을 경우에는 ConflictingBeanDefinitionException 예외가 발생한다.

 

두번째로 수동으로 등록된 빈과 자동으로 등록된 빈이 충돌할 경우이다. 기본적으로는 수동 등록 빈이 우선권을 갖는다. 다르게 말하자면 수동 빈이 자동 빈을 오버라이딩 해버린다는 것이다.

 

당연한 것이다. 수동 빈은 개발자가 딱 짚어서 디자인한 것이고 자동 빈은 스프링에서 설정해준 것이기 때문이다.

 

하지만,, 스프링 부트를 활용할 경우 두번째 경우에도 오류가 발생한다...

 

명확하게 규명되지 않은 경우 혹은 애매하게 디자인된 코드는 개발하지 않는 것이 낫다는 것이다.. 스프링 부트 딴에서 다시금 확인하라고 기회를 주는 것이다..

 


 

이로써 컴포넌트 스캔에 대해서 살펴보았다.

 

요약하자면

 

1. 컴포넌트 스캔을 활용하면 스프링 설정 클래스에 직접 @Bean 으로 등록하는 번거로움이 사라진다.

 

2. 컴포넌트 스캔을 할때, 필터링을 통해 스캔하고자 하는 빈과 스캔에서 제외하고자 하는 빈을 구분할 수 있다.

 

3. 컴포넌트 스캔은 시작 위치를 설정할 수 있는데 기본적으로는 설정 클래스가 위치한 패키지를 기준으로 스캔을 시작한다.

그리고 이러한 디자인 패턴을 권장한다.

 

이번 수업을 통해서 컴포넌트 스캔에 대해서 깊게 알아보았다. @Configuration 어노테이션을 붙일 스프링 설정 클래스를 구성할 때 요긴하게 사용될 수 있을 것 같다! 끝! ㅎㅎ