본문 바로가기
Study/Java Spring Boot

[Spring] 핵심 원리 6 컴포넌트 스캔 (@Component)

본 글은 김영한님의 강의 내용을 바탕으로 정리한 글입니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 학습 페이지

지식을 나누면 반드시 나에게 돌아옵니다. 인프런을 통해 나의 지식에 가치를 부여하세요....

www.inflearn.com

 

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

Spring에서는 어노테이션을 활용하여 클래스에 @Component를 명시하여 @Bean을 등록해 줄 수 있고,

@AutoWired라는 어노테이션을 활용해 등록된 Bean을 자동으로 연결시켜줄수 있다.  

 

지금까지 우리는 @Bean을 명시해주는 @Configuration 이 명시된 AppConfig에 쭉 나열해주었었다.

하지만, 그렇게 작업하기에는 추후 수십~수백개의 컴포넌트들을 전부 넣기에는 너무나도 많은 작업이 된다.

스프링은 자동으로 빈을 등록하고 DI을 진행하는 또 다른 방법이 있다.

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService(){
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository(){
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository(); //메모리 맴버리포지토리로 생성
    }
    @Bean
    public OrderService orderService(){
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy(){
        System.out.println("call AppConfig.discountPolicy");
        return new FixDiscountPolicy(); // 고정 할인 정책으로 생성
    }

}

 

위 코드는 기존에 AppConfig 를 통한 Bean 등록과 의존관계 주입코드이다.

@Configuration을 명시한 DI컨테이너에서 생성자를 호출하는 방식으로 의존된 컴포넌트들을 넣어줬다면,

 

AppConfig의 작성 없이 코드 안에서 @Bean을 등록하고, 의존관계를 주입받아보자.

 

현재 개발한 도메인에서 우리가 사용하는 빈은  아래와 같이 4가지 이다.

각각의 빈을 AppConfig가 아닌, 자체적으로 클래스안에서 @Component 를 명시하여 등록하고,

그 안에서 의존성을 가지는 컴포넌트는 @AutoWired를 명시하여 자동의존성 주입을 해주도록 바꾼다.

 

 

Member 도메인 -> MemberRepository ->MemoryMemberRepository 를 빈으로 등록사용

@Component
public class MemoryMemberRepository implements MemberRepository {

Member 도메인 -> MemberService ->MemberServiceImpl 를 빈으로 등록사용

@Component
public class MemberServiceImpl implements MemberService{
	...
    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository){
    	...

discount 도메인 -> DiscountPolicy -> RateDiscountPolicy 를 빈으로 등록사용

@Component
public class RateDiscountPolicy implements DiscountPolicy{

order 도메인 -> OrderService -> OrderServiceImpl 를 빈으로 등록사용

@Component
public class OrderServiceImpl implements OrderService{
	...
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository,DiscountPolicy discountPolicy){

 

이렇게 변경해주었다면, 이제 AppConfig 없이도 빈 등록과, 의존성이 전부주입된 싱글톤 패턴의 서비스가 수행이 된다.Test를 진행해보자.

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을 명시한 설정클래스는 무시되도록 하나의 클래스를 만들어준다.

이 클래스를 통해 등록된 정말로 자동으로 빈이 등록되고 연결되었는제 조회해보자.  

package hello.core.scan;

import hello.core.AutoAppConfig;
import hello.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);
    }
}

하나의 싱글톤 인스턴스를 공유하고 있음을 로그를 통해 볼 수 있다. 

이러한 방식으로 사용할 Bean에 @Component를 명시하여 자동등록 및 의존성 주입을 실현할 수 있다.

@Component를 명시하면 아래와 같이 빈 객체의 클래스명이 소문자형식으로 네이밍 규칙에 따라 등록되는데,

@Component("memberService2") 와 같이 이름을 지정하여 등록할 수 도 있다. 

 

@AutoWired를 명시하면 등록된 스프링 컨테이너에서 빈객체를 찾아서 주입해준다.

동일 객체 타입이 등록되면, 충돌이 발생할 수 있기때문에, 등록되 연결이름을 신경써주어야 한다.

 

 

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

@ComponentScan을 통해 자동 스캔을 할 때, 다양한 속성을 지정해 줄 수 있다.

1) basePackage 탐색패키지

명시해주게 되면 해당 패키지에 하위에 있는 컴포넌트만 탐색하여 등록한다.

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

meber밑에 있는 두가지 빈만 등록되는것을 볼수 있다.

basePackage를 명시해주지 않게된다면,

해당 클래스가 위치한 패키지를 시작 위치로하여 하위 패키지들을 모두 탐색한다.

통상적으로 AppConfig는 프로젝트를 대표하는 구성정보이기 때문에, 프로젝트에 시작루트 패키지에 위치시킨다.

 

+ 참고 : Spring 프로젝트 생성시 만들어지는 coreApplication에 @SpringBootApplication에

이미 @ComponentScan이 내장되어 있다,

AutoAppConfig를 만들었던 내용은 필요없는 과정이다 (우리의 내용은 테스트와 검증을 위해 만든것임)

package hello.core;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CoreApplication {

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

}

 

컴포넌트 스캔 대상

  • @Component: 컴포넌트 스캔에서 사용
  • @Controlller: 스프링 MVC 컨트롤러에서 사용
  • @Service: 스프링 비즈니스로직에서 사용 (가시성, 명시성)
  • @Repository: 스프링 데이터 접근 계층에서 사용
  • @Configuration: 스프링 설정 정보에서 사용

 

 

3. 필터 

컴포넌트 스캔에서 추가 / 제외 대상을 지정할 수 있다.

똑같은 어노테이션 2개와 Bean클래스 2개를 각각 다른이름으로 만들어서 추가 / 제외 설정을 해보겠다. 

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 {
}
package hello.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}

BeanA는  @MyIncludeComponent를

BeanB는 @MyExcludeComponent를 적용시키고,

 

Test를 통해 @ComponentScan 속성중에

includeFilters 에 @MyIncludeComponent로 무조건 포함되게 지정,

excludeFilters 에 @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;

@Configuration
@ComponentScan(
        includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
public class ComponentFilterAppConfigTest {
    @Test
    void filterScan(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfigTest.class);
        BeanA beanA = ac.getBean("beanA",BeanA.class);
        assertThat(beanA).isNotNull();
        Assertions.assertThrows(NoSuchBeanDefinitionException.class, ()-> ac.getBean("beanB", BeanB.class));
    }


}

결과는 BeanA는 생성되고,

BeanB는 생성되지않아 NoSuchBeanDefinition익셉션이 발생됨

Filters 속성에 Type 5가지 옵션이 있다.

  • 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

 

예시코드) excludeFilter에서 ASSIGNABLE_TYPE 을 통해 BeanA를 제외시키는 코드 ↓ 

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

 

 

 

4. 중복 등록과 충돌

컴포넌트 스캔시 주의해야 하는 부분중 하나가 이름이다.

이름의 충돌이 발생하면, ConflictingBeanDefinitionException 익셉션이 발생한다.

 

웹 CSS도 인라인 CSS > HEAD CSS > 외부 CSS 순서로 우선권을 가지는 것처럼

수동 빈 등록 (AppConfig - @Confiuration, @Bean) > 자동 빈 등록 (@AutoWired, @Component) 이다

따라서 , 수동과 자동 모두 동일이름이 등록된 경우, 수동 빈 등록이 우선권을 가지므로 자동빈을 덮어쓴다.

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

 

수동등록, 자동등록 오류시 Spring boot 에러

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

스프링부트인 CoreApplication실행 해보면 오류를 있다.

 

소스코드 ↓