스프링 핵심 원리 07] 컴포넌트 스캔

컴업·2022년 1월 12일
0

본 포스트는 Inflearn 김영한 선생님 강의를 정리한 것 입니다!

지난 포스트에서는 @Configuration이 붙은 AppConfig.class 구성정보를 상요해 스프링 컨테이너에 빈을 등록하는 과정에 대해 알아보았습니다.

AppConfig에 구성 정보만 기입하면 모든 빈들을 싱글톤으로 만들어 관리해주니 상당히 편리한 기능이었습니다.

그런데 만약 서비스가 커져 등록해야할 빈이 수십 수백개가 되면 어떻게 될까요?

일일이 등록하는 것 자체도 일이고, 설정 정보도 복잡해지고 추후에 누락의 위험도 분명 커지겠죠.

스프링에서는 귀찮게 AppConfig에 구성정보를 기입하지 않더라도 스프링 빈을 자동으로 등록해주는 편리한 기능을 제공합니다.

이번 포스트에서 알아보도록 하겠습니다.

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

컴포넌트 스캔은 설정 정보 없이 자동으로 스프링 빈을 등록하는 기능입니다.

이 기능을 사용하기위해서 먼저 core 패키지에 새로운 AppConfig를 만들겠습니다.

package hello.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을 설정 정보에 붙여주면 됩니다.

기존과 달리 @Bean으로 등록하지 않았죠??


참고
excludeFilters는 앞선 실습에서 만들었던 @Configuration이 붙은 AppConfig를 스캔 대상에서 제외하기 위한 코드입니다.


컴포넌트 스캔, 말 그대로 @Component가 붙은 모든 클래스를 스캔해 스프링 빈으로 등록하는 기능입니다.

빈으로 등록할 구현 객체들에 @Component 를 붙여줍니다.

@Component
public class MemoryMemberRepository implements MemberRepository {}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

...

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

...
}

이전 AppConfig에서는 @Bean으로 스프링 빈등록, 의존관계 주입을 한꺼번에 처리했지만, 이제는 이러한 설정 정보가 없기 때문에 의존관계 주입을 이 클래스 내부에서 끝내야합니다.

@Autowired는 이러한 의존관계 주입을 자동으로 해줍니다.

뒤에서 좀 더 자세하게 알아보겠습니다.

@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;


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

OrderServiceImpl또한 마찬가지로 @Autowired까지 붙여줍니다.

그럼 빈등록, 의존관계주입이 제대로 되었는지 확인해 보겠습니다.

package hello.core;

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;

class AutoAppConfigTest {

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

}

귀찮으니까 하나만 테스트 하고 대신 로그를 잘 보면

실행 로그

Creating shared instance of singleton bean 'rateDiscountPolicy'
Creating shared instance of singleton bean 'memberServiceImpl'
Creating shared instance of singleton bean 'memoryMemberRepository'
Autowiring by type from bean name 'memberServiceImpl' via constructor to bean named 'memoryMemberRepository'
Creating shared instance of singleton bean 'orderServiceImpl'
Autowiring by type from bean name 'orderServiceImpl' via constructor to bean named 'memoryMemberRepository'
Autowiring by type from bean name 'orderServiceImpl' via constructor to bean named 'rateDiscountPolicy'

우리가 @Component를 붙였던 모든 클래스들이 singleton bean으로 잘 만들어졌고, @Autowired를 통해 의존관계 주입도 끝냈다 라는 로그를 확인할 수 있습니다.

@ComponentScan

@ComponentScan은 @Component가 붙은 모든 클래스를 빈으로 등록합니다.

  • 빈 이름 기본 전략
    빈 이름은 클래스명을 사용하되 맨 앞글자를 소문자로 바꿔 사용합니다.
    ex) MemberRepository -> memberRepository

  • 빈 이름 직접 지정
    @Component("userRepository") 이렇게 직접 이름을 지정할 수 도 있습니다.

@Autowired (의존관계 자동주입)

생성자에 @Autowired를 붙여주면 스프링 컨테이너가 파라미터로 들어가는 해당 스프링 빈들을 찾아서 주입해줍니다.

이때 기본 조회 전략은 타입이 같은 빈을 찾는 것 입니다.

getBean(MemberRepository.class) 와 동일합니다.

때문에 동일 타입이 2개 이상 존재하면 물론 충돌이 발생합니다.

이에 대해서는 추후에 기술 하겠습니다.

2. 탐색 위치와 기본 스캔 대상

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

모든 자바 클래스를 대상으로 컴포넌트 스캔을하면 시간이 오래 걸릴 것 입니다.

그래서 꼭 필요한 위치부터 스캔하도록 위치를 지정할 수 있습니다.

@Configuration(
	basePackages = "hello.core"
)

basePackages: 지정한 패키지를 포함한 하위 패키지를 모두 스캔합니다.

  • basePackages = {"hello.core", "hello.service"} 이렇게 여러개를 지절할 수 도 있습니다.

basePackageClasses: 지정한 클래스가 속한 패키지를 시작 위치로 지정합니다.

만약 시작 위치를 지정하지 않으면 @ComponentScan이 붙은 클래스가 속한 패키지부터 스캔이 시작됩니다.

선생님이 권장하는 법
패키지 위치를 지정하지 않고 @ComponentScan 설정 정보 클래스를 프로젝트 최 상단에 두는 것을 권장합니다. 스프링 부트도 이 방법을 기본적으로 제공합니다.


참고
스프링 부트를 사용할 경우 스프링 부트 대표 시작정보인 @SpringBootApplication을 프로젝트 최상단에 두는 것이 관례입니다. 이 어노테이션안에 @ComponentScan이 포함되어있습니다.


컴포넌트 스캔 기본 대상

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

위 어노테이션들을 까보면 @Component가 포함되어있는 것을 확인할 수 있습니다.

덕분에 자주사용하는 위 어노테이션이 붙은 클래스에는 추가로 @Component를 붙여주지 않아도 자동으로 스캔 대상에 포함됩니다.


참고

애노테이션 간에는 상속관계라는 것이 없습니다. 그래서 애노테이션이 다른 애노테이션을 들고있는 것을 인식하는 건 자바가 아니라 스프링에서 제공하는 기술입니다.

위 애노테이션들은 추가적으로 부가 기능을 가지고있습니다.

  • @Controller : 스프링 MVC 컨트롤러로 인식
  • @Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
    (DB마다 발생하는 예외가 다르다. 그 예외들을 스프링 예외로 추상화해준다.)
  • @Configuration : 앞서 보았듯이 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다. (CGLIB)
  • @Service : 사실 @Service 는 특별한 처리를 하지 않는다. 대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나 라고 비즈니스 계층을 인식하는데 도움이 된다.

useDefaultFilters 옵션은 기본으로 켜져있는데, 이 옵션을 끄면 기본 스캔 대상들이 제외된다. 그냥 이런 옵션이 있구나 정도 알고 넘어가자.


3. 필터

아까 기존 AppConfig를 스캔 대상에서 제외시키기 위해 excludeFilters를 사용했던 것 기억나시나요?

@ComponentScan(excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))

이처럼 컴포넌트 스캔 대상에서 추가할 아이들, 제외할 아이들을 필터링 할 수 있습니다.

단, 선생님께서 말씀하시기를 스캔 대상 추가는 @Component 만으로 이미 충분하고, 제외도 사용할 일이 많지 않습니다.

특히 스프링 부트는 컴포넌트 스캔을 기본으로 제공하는데, 옵션을 변경하면서 사용하기 보다는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장한다고 하세요.

한마디로 쓰지 말란말.

그냥 넘어가세요

그래도 정리는 해두겠습니다!

@Filter를 이용해서 스캔 대상에 포함 혹은 제외 시킬 아이들을 고를 수 있습니다.

type을 지정해 애노테이션을 기준으로 할 지, 클래스 자체를 기준으로 할지 지정할 수 있습니다.

  • ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.
    ex) org.example.SomeAnnotation
  • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
    ex) org.example.SomeClass
  • ASPECTJ: AspectJ 패턴 사용
    ex) org.example..*Service+
  • REGEX: 정규 표현식
    ex) org.example.Default.*
  • CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리
    ex) org.example.MyTypeFilter

Annotation이 디폴트 이므로 실습에서는 이를 이용해보겠습니다.

먼저 core패키지에 scan패키지를 또 그안에 filter패키지를 만들고 애노테이션을 만들었습니다.

제외 시키는 어노테이션

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;

@MyIncludeComponent
public class BeanA {
}

BeanA에는 MyIncludeComponent를 붙이고

package hello.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}

BeanB에는 MyExcludeComponent를 붙였습니다.

테스트 케이스를 작성합니다

package hello.core.scan.filter;

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

public class ComponentAppConfigTest {
    
    @Test
    void filterScan() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        
        BeanA beanA = ac.getBean("beanA", BeanA.class);
        
        assertThat(beanA).isNotNull();
        
        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 {
    }
}

먼저 ComponentFilterAppConfig라는 설정 정보를 만들었습니다.

@Filter type을 ANNOTAION으로 설정하고, includeFilters에는 MyIncludeComponent를 excludeFilters에는 MyExcludeComponent를 지정했습니다.

이제 각 어노테이션이 붙은 클래스들은 이 설정대로 스캔 대상에 포함되거나 제외됩니다.

테스트인 filterScan을 보시면 BeanA는 정상적으로 빈으로 등록되었고, BeanB는 등록되지않아 NoSuchBeanDefinitionException이 발생했습니다.

마지막으로

BeanA만 다시 스캔 대상에서 제외 시켜볼까요??

@Filter type을 클래스로 하여 BeanA만 바꿔보겠습니다.

@ComponentScan(
        includeFilters = {
                @Filter(type = FilterType.ANNOTATION, classes =
                        MyIncludeComponent.class),
        },
        excludeFilters = {
                @Filter(type = FilterType.ANNOTATION, classes =
                        MyExcludeComponent.class),
                @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = BeanA.class)
        }
)

4. 중복 등록과 충돌

컴포넌트 스캔도중 같은 빈 이름으로 등록을 하려하면 어떻게 될까요?

두가지 경우를 생각해 볼 수 있습니다.

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

자동 빈 등록 vs 자동 빈 등록에서는 문제가 되지 않습니다.

스캔도중 같은 이름을 가진 또다른 클래스를 발견하면 ConflictingBeanDefinitionException예외가 발생하거든요.

그러나 수동 빈 등록 그리고 자동 빈 등록에서 충돌이 발생하면 어떻게 될까요?

실험을 해보겠습니다.

@Component
public class MemoryMemberRepository implements MemberRepository {}

MemoryMemberRepository에는 아까 @Component를 붙여두었죠?

@Configuration
@ComponentScan(excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {

    @Bean(name = "memoryMemberRepository")
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

AutoAppConfig에 @Bean을 이용해 수동으로 이름이 똑같은 MemoryMemberRepository를 등록시켜 보겠습니다.

로그
Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing

로그를 확인해보면 빈이 오버라이딩 되었다는 것을 확인할 수 있습니다.

수동 빈이 자동 빈을 오버라이딩 해버리는 것 입니다.

이 경우 정말 잡기 어려운 버그가 만들어집니다.

때문에 최근 스프링 부트에서는 수동 빈과 자동빈이 충돌이 나더라도 오류가 발생하도록 기본값을 바꾸었습니다.

profile
좋은 사람, 좋은 개발자 (되는중.. :D)

0개의 댓글