BackEnd/Spring

[Spring] 컴포넌트 스캔

샤아이인 2022. 2. 4.

내돈내고 내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

 

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

지금까지의 AppConfig를 생각하면, @Bean으로 생성할 빈 객체에 대한 정보를 적어줬었다. 하지만 컴포넌트 스캔을 이용하면 빈을 자동 등록할 수 있다.

 

이전까지의 예제에서는 등록한 스프링 빈이 적어서 수동으로 가능했지, 30개만 되도 수동으로 @Bean을 적어 등록하기는 힘들어진다...

따라서 보통 컴포넌트 스캔을 이용한다.

 

바뀐 AutoAppConfig는 다음과 같다.

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

@ComponentScan을 추가해주면 설정 파일은 끝난다. 기존의 AppConfig와 다르게 @Bean으로 등록된 클래스가 하나도 없다!

 

위 코드에서 excludeFilters 를 사용하고 있는데, 이는 기존의 다른 설정 class에서 @Configuration을 사용하여 만든 AppConfig를 scan 하지 않기 위해서 이다.

 

만약 기존의 AppConfig를 제외 시키지 않을 경우, AppConfig 내부에 @Bean 으로 등록되어 있던 memberService, memberRepository, orderService, discountPolicy가 전부 수동으로 등록되기 때문이다.

이러면 이게 자동등록 된것 인지? 수동 등록 된것인지? 알수가없다.

 

따라서 기존의 AppConfig 등록을 막기 위해 excludeFilter로 @Configuration 이 붙은 기존 config 들의 등록을 막는다

 

(추가로 자기 자신의 Configuration까지 제외 시키면 문제되지 않는가? 란 의문이 들수도 있는데, 이는 선 후 관계를 잘 생각해보면

이 AutoAppConfig가 등록됬기 때문에 excludFilters가 적용됨을 알 수 있다.)

 

이후 컴포넌트 스캔을 할때 @Component가 붙은 클래스들을 스캔하여 빈으로 등록하게 된다.

(이전까지 @Configuration 또한 스프링 빈으로 등록 됬는데, @Configuration 소스코드를 열어보면 @Component이 붙어있기 때문이다.)

 

이제 각 클래스에 컴폰넌트 스캔의 대상이 되도록 수정해 보자!

출처 - 인프런 스프링 (김영한) 강의

전부 @Conponent가 추가되었다!

또한 이전까지는 의존성 주입을 위해 생성자의 인자로 수동으로 적어주던 부분이 바뀌었다.

생성자 위에 @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.annotation.AnnotationConfigApplicationContext;

public class AutoAppConfigTest {

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

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

결과는 다음과 같다.

싱글톤 빈이 정상적으로 생성되었으며, 의존성 주입을 자동으로 해주고 있다.

 

@Component로 빈을 등록할때 이름은 클래스의 이름에서 맨 앞글자만 소문자로 바꿔 등록한다.

예를 들어 MemberServiceImple 클래스를 등록하면 memberServiceImple로 빈이 등록되게 된다.

만약 이름을 직접 지정하고싶다면 @Component("mmService2") 와 같은 방식으로 지정할수 있다.

 

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

스캔의 시작 위치를 지정할 수 있다!

@ComponentScan(
          basePackages = "hello.core",
         // basePackages = {"hello.core", "hello.service"} 여러개 지정 가능
}
 

위와 같이 basePackages 를 사용하면 탐색할 패키지의 시작 위치를 지정할 수 있다. 이 패키지를 포함하여 하위 패키지를 모두 탐색한다.

또한 2번째 줄 처럼 여러개의 패키지를 지정할수도 있다.

 

basePackageClasses 를 사용하면 지정한 클래스가 있는 패키지를 시작 패키지로 설정한다.

 

만약 어떠한 지정도 하지 않는다면 default는 @ComponentScan이 붙은 설정정보 클래스가 있는 패키지가 시작 위치이다.

 

영한님이 권장하신 방법

개인적으로 즐겨 사용하는 방법은 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이라고 하셨다.

최근 스프링 부트도 이 방법을 기본으로 제공한다.

 

예를 들어 프로젝트의 구조가 다음과 같다고 해보자!

1) com.hello

2) com.hello.serivce

3) com.hello.repository

com.hello 가 프로젝트의 시작 루트이다. 여기에 AppConfig같은 설정정보 파일을 두고, @ComponentScan을 지정하면 된다.

 

이렇게 하면 com.hello 를 포함한 하위는 모두 자동으로 컴포넌트 스캔의 대상이 된다.

그리고 프로젝트 메인 설정 정보는 프로젝트를 대표하는 정보이기 때문에 프로젝트 시작 루트 위치에 두는 것이 좋다 생각한다.

 

컴포넌트 기본 스캔 대상에 대하여 알아보자!

 

컴포넌트 스캔은 @Component 뿐만 아니라 다음과 내용도 추가로 대상에 포함한다.

@Component : 컴포넌트 스캔에서 사용

@Controlller : 스프링 MVC 컨트롤러에서 사용

@Service : 스프링 비즈니스 로직에서 사용

@Repository : 스프링 데이터 접근 계층에서 사용

@Configuration : 스프링 설정 정보에서 사용

 

몇몇 클래스의 코드를 직접 들여다 보면 @Component를 포함하고 있음을 알 수 있다.

@Component
public @interface Controller {
}

@Component
public @interface Service {
}

@Component
public @interface Configuration {
}
 

사실 애노테이션에 상속관계는 자바의 문법으로 지원하는것이 아니다! 이는 스프링이 지원하는 기능이다!

 

3. 필터

컴포넌트 스캔 대상에 추가할 수 있는 애노테이션 @MyIncludeComponent 와 스캔 대상에서 제외할 수 있는 @MyExcludeComponent를 만들어 보자!

 

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


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

위에서 만든 애노테이션을 적용하여 컴포넌트 스캔 대상에서 추가할 클래스 BeanA와 컴포넌트 스캔 대상에서 제외할 BeanB

@MyIncludeComponent
public class BeanA {
}

@MyExcludeComponent
@Component
public class BeanB {
}
 

전체 테스트 코드는 다음과 같다!

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 = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class  ComponentFilterAppConfig{
    }
}
 

includeFilters 에 MyIncludeComponent 애노테이션을 추가해서 BeanA가 스프링 빈에 등록된다.

excludeFilters 에 MyExcludeComponent 애노테이션을 추가해서 BeanB는 스프링 빈에 등록되지 않는다.

 

추가적으로 필터 타입의 옵션은 5가지가 있다!

출처 - 인프런 스프링 (김영한) 강의

 

 

4. 중복 등록과 충돌

총 2가지 상황이 있을 수 있다.

 

1) 자동 빈 등록 vs 자동 빈 등록

이 경우에는 이름이 같은 빈이 있다면 ConflictingBeanDefinitionException 예외 발생

 

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

이 경우에는 수동빈 등록이 우선권을 갖는다. 수동빈이 자동빈을 오버라이딩 해버리게 된다!

 

따라서 로그를 확인해보면 다음과 같이 남아있다.

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

하지만 스프링 부트를 사용한다면 기본이 에러를 발생하게 만들어 준다.

 

댓글