Spring 핵심 원리 - 컴포넌트 스캔

김태훈·2023년 3월 1일
0

Spring 핵심 원리

목록 보기
13/15

지금까지, 스프링 빈을 등록할 때 @Bean 과 같은 어노테이션을 통해서 스프링 빈으로 등록했다. 하지만 등록,관리해야할 스프링 빈이 많아지게 된다면, 꽤나 귀찮아질 것이다.
그래서 스프링에서는 설정 정보가 따로 없어도 자동으로 스프링 빈으로 등록하는
컴포넌트 스캔 이라는 기능을 제공한다.
또한, 의존 관계도 자동으로 주입하는 @Autowired 기능도 제공한다.

1. AutoAppConfig 클래스 코드 작성

1. @Component와 @Autowired

package Goat.core;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import static org.springframework.context.annotation.ComponentScan.*;
@Configuration
@ComponentScan(
        excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}
  • @ComponentScan
    해당 어노테이션은, @Component 어노테이션이 붙은 클래스를 찾아서 알아서 스프링 빈으로 등록해준다.

    이 때, 기본적으로 스프링 빈에 등록되는 이름은 클래스 이름을 그대로 사용하되, 앞글자만 소문자로 바꾸어 저장한다. (물론 임의로 이름을 바꾸는 설정 또한 가능하다)
    ex) MemberServiceImpl -> memberServiceImpl

  • exculdeFilters
    ComponentScan안에서 스프링 빈으로 등록하지 않을 클래스를 지정한다.
    여기서는 당연한것이, Configuration 클래스에는 수동으로 @Bean 어노테이션으로 등록한 클래스이기 때문에 해당 클래스를 제외한 것이다.
    (이 때, @Configuration 어노테이션이 붙은 클래스도 자동으로 등록이 되는 이유는 @Configuration안에 @Component 어노테이션이 포함되어있기 때문이다.

    Ctrl + Shift + F 누르면 전체 파일에서 문자열 검색 가능
    Shift + Shift 는 설정 전체에서 설정 클래스 검색 가능

  • AutoAppConfig가 텅텅 비어있어요!!

코드를 다 봤으면,
이제 @Component 를 스프링 빈으로 등록할 클래스 파일로 가서 다 붙여주자.
1. MemoryMemberRepository
2. RateDiscountPolicy
3. MemberServiceImpl
4. OrderServiceImpl
이렇게까지 하면, MemberServiceImpl가 의존하는 Repository를 구제할 방법이 없다.
기존의 AppConfig에서는 그냥 Bean으로 등록할때 의존성 주입을 해주었지만, @Component의 사기 어노테이션을 쓰는 순간 방도가 없는 것이다.
그래서 필요한것이 @Autowired 라는 자동 의존 관계 주입 어노테이션이다 !!
OrderServiceImpl도 같은 방식이다. (의존성 주입 필요한 클래스)

@Autowired
Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다. 이 때 '타입'을 보고 지정한다.
만약 MemberRepository의 의존성 주입이 필요하다면, memoryMemberRepository가 주입이 되겠다. 마치, getBean(MemberRepository.class)와 동일

@Component
public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    @Autowired
    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;
    }
}

2. 테스트 코드 작성

package Goat.core.scan;

import Goat.core.AutoAppConfig;
import Goat.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.*;
public class AutoAppConfigTest {
    @Test
    void basicScan() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

당연히, AnnotationConfigApplicationContext에서 인자를 AutoAppConfig.class로 넘겨주어야 겠다.

2. 컴포넌트 스캔의 탐색 위치와 기본 스캔 대상

1. 탐색 위치를 지정해주기

1. basePackages 로 찾기

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

다음과 같이 basePackages로 탐색 위치를 지정하면,
"member"패키지 안에 있는 싱글톤만 등록된다.

이렇게 두 개의 싱글톤만 bean으로 등록이 되었다.
만일, 패키지 탐색 위치를 여러개로 지정하고 싶다면,

basePackages = {"Goat.core","Goat.core2"}

로 코드를 작성하면 되겠다.

2. basePackagesClasses 로 찾기

basePackagesClasses = AutoAppConfig.class,

로 작성을 한다면, 해당 AutoAppConfig가 포함되어 있는 패키지에서 부터 탐색을 시작한다.

package Goat.core;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import static org.springframework.context.annotation.ComponentScan.*;
@Configuration
@ComponentScan(
        basePackageClasses = AutoAppConfig.class,
        excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}

여기에서는 Goat.core 패키지 안에 있는 모든 싱글톤을 탐색한다.

이렇게 패키지의 위치를 지정하지 않는다면, 모든 패키지들을 탐색해야 하므로 시간이 매우 오래걸릴 것이다.

3. 만약 아무것도 지정하지 않는다면?

ComponentScan 어노테이션을 가지고 있는 클래스에서부터 탐색하도록 default로 설정이 되어있다.
따라서, 권장하는 방법은, AppConfig같은 파일을 프로젝트 최상단에 위치하도록 옮기고, basePackages와 같은 정보들을 생략해서, 전체 패키지를 탐색할 수 있도록 하는 것이다.

2. ComponentScan의 기본 대상

컴포넌트 스캔은 "@Component"가 붙은 클래스 뿐 아니라 다음과 같은 어노테이션이 등록된 클래스도 컴포넌트 스캔의 대상이 된다. (@Component의 어노테이션도 가지고 있음)

  • @Controller
  • @Service
  • @Repository
  • @Configuration

이 때 놓칠 수 있는 것이 있는데,
어노테이션은 상속관계가 없다.
단지, 스프링에서 상속관계인것 처럼 기능을 지원하는 것이다.

3. 필터

필터는 컴포넌트 스캔 대상으로 포함/미포함 하는 여부를 결정하는 것이다.

scan 패키지에 filter 패키지를 추가하여 Annotation파일을 만들자.
하나는 MyIncludeComponent
다른 하나는 MyExcludeComponent로 만든다.

  • MyExcludeComponent
package Goat.core.scan.filter;

import java.lang.annotation.*;

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

import java.lang.annotation.*;

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

그 후, BeanA와 BeanB 클래스를 만들어 각각 해당 어노테이션(MyIncludeComponent 혹은 MyExcludeComponent)를 붙인다.

package Goat.core.scan.filter;

@MyIncludeComponent
public class BeanA {
}
package Goat.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}

그리고 테스트 클래스 ComponentFilterAppConfigTest 클래스를 만든다.

package Goat.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;

public class ComponentFilterAppConfigTest {
    @Test
    void filterScan(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA",BeanA.class);
        Assertions.assertThat(beanA).isNotNull();
        
        org.junit.jupiter.api.Assertions.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 {
    }
}
  • ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class)
    이 코드는 Annotation과 관련된 필터를 만드는 것이다. 그리고 MyIncludeComponent 클래스를 받아옴.

이렇게 하면 BeanA는 등록, BeanB는 등록되지 않는다 !

FilterType의 다섯가지 종류

  • Annotation : 기본값, 어노테이션을 인식해서 동작
  • ASSIGNABLE_TYPE : 지정한 타입과 자식 타입을 인식해서 동작
  • ASPECTJ : AspectJ 패턴 사용
  • REGEX : 정규표현식
  • CUSTOM : "TypeFilter" 라는 인터페이스를 구현해서 처리

4. 중복 등록과 충돌

컴포넌트 스캔에서 같은 빈 이름을 등록하면 충돌이 일어날 것이다.
그 경우의 수는 다음 두가지이다.

  • 자동 빈 등록 vs 자동 빈 등록
  • 수동 빈 등록 vs 자동 빈 등록

1. 자동 빈 등록과 자동 빈 등록의 충돌

해당 상황은 오류를 발생시킨다. (ConflictBeanDefinitionException)

확인 방법은 @Component("중복이름넣기")로
클래스마다 해당 어노테이션을 넣으면 "중복이름"이 동시에 Component Scan으로 스프링 빈에 자동으로 등록되면서 오류가 발생한다.

2. 수동 빈 등록과 자동 빈 등록의 충돌

다음을 확인하기 위해서

@Configuration
@ComponentScan(
        basePackageClasses = AutoAppConfig.class,
        excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
    @Bean(name = "memoryMemberRepository")
    MemberRepository memberRepository (){
        return new MemoryMemberRepository();
    }
}

이렇게 등록하면,
이미 자동으로 "MemoryMemberRepository"가 앞 글자는 소문자로 바뀐채 스프링 빈에 등록이 되어있는데
수동으로 다시 한 번 같은이름으로 등록이 되어서 오류가 발생할 것이다.
하지만 오류는 발생하지 않는다 !!
이유는 다음과 같다

이런 문구가 뜨는데, 비밀은 수동으로 등록된 빈이 우선순위를 갖는다는 점이다.

하지만.. 이렇게 수동/자동 우선순위를 따지게 된다면 협업 상황에서 버그를 잡기 어려운 상황이 올 수도 있다.

따라서 스프링부트에서는 수동/자동 따지지 않고, 같은 이름의 스프링빈이 중복해서 등록이 되면 Application을 실행할 때 오류가 발생하게 끔 하였다.

이 때, 중복된 수동/자동 빈 이름의 오버라이딩 (수동으로 등록된 빈이 우선수위를 갖게끔)을 허용하는 설정을 application.properties에 넣게 되면, 간단히 해결할 순 있다.

profile
기록하고, 공유합시다

0개의 댓글