WEB/Spring

김영한 (스프링 핵심원리 6) 컴포넌트 스캔

Tony Lim 2021. 2. 13. 00:50
728x90

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

스프링은 설정 정보가 없어도 자동으로 스프링 빈으로 등록하는 컴포넌트 스캔이라는 기능을 제공한다.

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와 다르게 아무것도 안에 없다. excludeFilters 는 예제를 유지하기 위함이다. 

package hello.core.member;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MemberServiceImpl implements MemberService
{
    private final MemberRepository memberRepository;

    @Autowired //ac.getBean(MemberRepository.class)
    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);
    }


    //테스트 용도
    public MemberRepository getMemberRepository()
    {
        return memberRepository;
    }
}

@Component를 쓰는것 만으로 생성자에 필요한 MemberRepository를 주입해줄 수 없다. Autowired를 통해서 injection 을 해준다.

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.annotation.AnnotationConfigApplicationContext;


public class AutoAppConfigTest
{
    @Test
    void basicScan()
    {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        MemberService memberService = ac.getBean(MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

 잘동작하는 지 테스트 한 결과이다. 로그를 살펴보겠다. 이때 AutoAppConfig가 스프링 컨테이너를 생성할 때 넘겨주는 클래스인데 이 순간 빈으로 자동으로 등록이 되는것이다. 

23:47:30.751 [main] DEBUG org.springframework.context.annotation.ClassPathBeanDefinitionScanner - Identified candidate component class: file [D:\Java things\core\core\out\production\classes\hello\core\discount\RateDiscountPolicy.class]
23:47:30.759 [main] DEBUG org.springframework.context.annotation.ClassPathBeanDefinitionScanner - Identified candidate component class: file [D:\Java things\core\core\out\production\classes\hello\core\member\MemberServiceImpl.class]
23:47:30.779 [main] DEBUG org.springframework.context.annotation.ClassPathBeanDefinitionScanner - Identified candidate component class: file [D:\Java things\core\core\out\production\classes\hello\core\member\MemoryMemberRepository.class]
23:47:31.281 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
23:47:31.290 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
23:47:31.293 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
23:47:31.305 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
23:47:31.398 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'autoAppConfig'
23:47:31.411 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'rateDiscountPolicy'
23:47:31.412 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberServiceImpl'
23:47:31.493 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memoryMemberRepository'
23:47:31.495 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Autowiring by type from bean name 'memberServiceImpl' via constructor to bean named 'memoryMemberRepository'

어떤 클래스들이 빈으로 만들어졌는지 마지막줄에는 어떤 Bean이 생성자의 인자로서 Autowired 되었는지 알려준다. 

@ComponetScan은 @Component 가 붙은 모든 클래스를 스프링 빈으로 등록한다. Bean의 기본이름은 클래스이름이다. 지정하고싶으면 @CompoentScan("name") 이런식으로 지정하면 된다.

@Autowired는 우선적으로 타입으로 넣어줄 Bean을 찾는다. 같은 타입이 여러개 일 경우에는 뒤에 설명한다. 기본적으로 getBean(MemberRepostiory.class) 와 같다. 

@Configuration
@ComponentScan(
        basePackages = "hello.core.member",
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)

basepackage 로 이 패키지를 포함에서 밑으로만 스캔해달라고 할 수있다. basePackages = {"hello.core", "hello.service"}  이런식으로 도 지정할 수 있다.

basePackageClasses = AutoAppConfig.class 지정한 클래스의 패키지를 탐색 시작위로 지정할 수 있다.

그럼 basePackages 설정이 없으면 어떻게 될까, @ComponetScan이 있는 패키지위치부터 밑으로 스캔한다.

참고로 스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication 를 이 프로젝트 시작 루트 위치에 두는 것이 관례이다. @ComponentScan이 포함되어 있기 때문이다.

또한 main 메소드가 실행되면서 run 메소드 인자로 CoreApplication.class 를 넣어주기에 따로 Bean으로 등록할 필요가 없다. 

@SpringBootApplication
public class CoreApplication {

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

}

애노태이션에는 상속관계라는 것이 없다. 그래서 이렇게 애노테이션이 특정 애노테이션을 들고 잇는 것을 인식 할 수 있는 것은 자바 언어가 지원하는 기능이 아니고, 스프링이 지원하는 기능이다.

@Controller = 스프링 MVC 컨트롤러로 인식

@Repository = 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 에외를 스프링 예외로 변환해준다.

@Configuration = 앞서 보았듯이 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.

@Service = 개발자로 하여금 핵심 비지니스 로지깅 여기에 있겠구나 라고 비즈니스 계층을 인식하는데 도움이 된다. 

 

필터

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
{
}
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 org.springframework.stereotype.Component;

import static org.assertj.core.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);
        assertThat(beanA).isNotNull();

        org.junit.jupiter.api.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
    {

    }
}

includeFilter 에 MyIncludeComponet 애노태이션을 추가해서 BeanA가 스프링 빈에 등록이된다. 반대로 excludeFilter에 MyExcludeComponent 애노테이션을 추가해서 BeanB는 스프링 빈에 등록되지 않는다.  

type = FilterType.ANNOTATION 이 default임으로 생략해도 잘 동작한다.

 

중복 등록과 충돌

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

ConflictingBeanDefinitionException 예외 발생시킨다.

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

수동빈이 우선권을 가지며 자동 빈을 오버라이드 해버린다. 

개발은 항상 명확한것만 해야한다.

728x90