[Spring] 컴포넌트 스캔

첫걸음 - 5

Posted by owin2828 on 2019-12-30 11:20 · 8 mins read

1. About Component 스캔


1-1. 컴포넌트 스캔이란?

  • 자동 주입과 함께 사용되는 기능
  • 스프링이 직접 클래스를 검색해서 빈으로 등록해주는 기능
  • 설정 코드가 크게 줄어듦

1-2. @Component 어노테이션으로 스캔 대상 지정

  1. 기본 속성 값으로 해당 클래스를 스캔 대상으로 지정

     //MemberDao.java
     import org.springframework.stereotype.Component;
    
     @Component
     public class MemberDao {
         ...
     }
    
  2. 속성 값을 주어 스캔 대상으로 지정

    //MemberInfoPrinter.java
    // 빈의 이름을 지정
    @Component("infoPrinter")
    public class MemberInfoPrinter {
     ...
    }
    

컴포넌트 스캔시 Bean의 이름이 지정되는 방법은 위의 두 가지 방법에 따라 아래와 같이 결정됨

  1. @Component 어노테이션에 값을 주지 않을 경우, Bean으로 등록할 때 사용될 이름은 클래스 이름의 첫 글자를 소문자로 바꾼 이름
    즉, 클래스 이름이 MemberDao 라면 Bean 이름으로는 memberDao를 사용
  2. @Component 어노테이션에 값을 주었다면, 그 값을 Bean의 이름으로 사용

1-3. @ComponentScan 어노테이션으로 스캔 설정

// AppCtx.java
import org.springframework.context.annotation.ComponentScan;

@Configuration
// ComponentScan 어노테이션
@ComponentScan(basePackages = {"spring"})
public class AppCtx {
       /*
        * 다음과 같은 코드가 줄어들게 됨
        * public MemberDao memberDao(){
	*     ...
        * }
        * public MemberInfoPrinter memberInfoPrinter(){
	*     ...
        * }
        */

	@Bean
	@Qualifier("printer")
	public MemberPrinter memberPrinter1() {
		return new MemberPrinter();
	}
	
	@Bean
	@Qualifier("summaryPrinter")
	public MemberSummaryPrinter memberPrinter2() {
		return new MemberSummaryPrinter();
	}
	
	@Bean
	public VersionPrinter versionPrinter() {
		VersionPrinter versionPrinter = new VersionPrinter();
		versionPrinter.setMajorVersion(5);
		versionPrinter.setMinorVersion(0);
		return versionPrinter;
	}
}
  • @ComponentScan 어노테이션으로 인해, 4장의 AppCtx 코드와 비교하여 설정 코드가 줄어듦
  • @ComponentScan의 basePackage 속성값은 {“spring”} 인데,
    이는 spring 한 개만 존재하고 그 패키지와 하위 패키지에 속한 클래스를 스캔 대상으로 설정

1-4. 스캔 대상에서 제외하거나 포함시키기

excludeFilters 속성을 사용하면 스캔할 때 특정 대상을 자동 등록 대상에서 제외 가능하고, 아래와 같이 3가지 사용법이 존재

  1. 제외 대상을 직접 지정
  2. 특정 어노티에션을 붙인 타입을 컴포넌트 대상에서 제외
  3. 특정 타입이나 그 하위 타입을 컴포넌트 스캔 대상에서 제외

각 방법별 사용 예시는 아래와 같음

  1. 정규 표현식 / AspectJ 패턴을 사용한 대상 지정
     // AppCtxWithExclude.java
     import org.springframework.context.annotation.FilterType;
     import org.springframework.context.annotation.ComponentScan.Filter;
    
     // 동일 코드에 대하여 정규표현식 / AspectJ 패턴 사용 방식
     // 정규표현식을 이용한 1번째 방법
     @Configuration
     @ComponentScan(basePackages = {"spring"}, 
         excludeFilters = 
                 (@Filter(type = FilterType.REGEX, pattern = "spring\\..*Dao" )		
     )
    
     // AspectJ를 이용한 2번째 방법
     @Configuration
     @ComponentScan(basePackages = {"spring"}, 
         excludeFilters = 
                 (@Filter(type = FilterType.ASPECTJ, pattern = "spring.*Dao" )		
     )
    
     public class AppCtxWithExclude {
         ...
     }
    
    • 정규 표현식은 “spring”으로 시작하고 Dao로 끝나는 타입을 지정
    • AspectJ 패턴은 spring 패키지의 Dao로 끝나는 타입을 지정

  2. 특정 어노테이션을 제외
     // NoProduct.java
     @Retention(RUNTIME)
     @Target(TYPE)
     public @interface NoProduct {
    
     }
    
     // ManualBean.java
     @Retention(RUNTIME)
     @Target(TYPE)
     public @interface ManualBean {
    
     }
    

    위와 같은 특정 어노테이션을 붙인 타입을 텀포넌트 대상에서 제외하는 방법은 아래와 같이 존재

     // AppCtxWithExclude.java
     @Configuration
     @ComponentScan(basePackages = {"spring"}, 
         excludeFilters = { 
                 @Filter(type = FilterType.ANNOTATION, classes = ManualBean.class )			
     })
    
    • type 속성값으로 FilterType.ANNOTATION을 사용하면 class 속성에 필터로 사용할 어노테이션 타입을 값으로 줌
    • 이 코드는 @ManualBean 어노테이션을 제외 대상에 추가했으므로, 결국 다음 클래스를 컴포넌트 스캔 대상에서 제외
     // MemberDao.java
     @ManualBean
     @Component
     public class Member Dao{
         ...
     }
    

    @Retention, @Target 어노테이션?

    • 어노테이션을 Customizing 하는 방법
    • @Retention: 어느 시점까지 어노테이션을 남길 것인가? (파라미터는 아래와 같음)
      1. SOURCE: 컴파일시 사라짐
      2. CLASS: 컴파일러가 클래스를 참조할 때까지 유효
      3. RUNTIME: 컴파일 이후에도 VM을 통해 참조 가능
    • @Target: 어디에 우리가 만든 어노테이션을 적용할 것인가? (파라미터는 아래와 같음)
      1. TYPE: 클래스, 인터페이스
      2. FIELD: 필드
      3. METHOD: 매서드
      4. PARAMETER: 파라미터
      5. CONSTRUCTOR: 생성자
      6. LOCAL_VARIABLE: 지역변수
      7. ANNOTATION_TYPE: 어노테이션 타입
      8. PACKAGE: 패키지
  3. 특정 타입이나 그 하위 타입을 제외
    // AppCtxWithExclude.java
    @Configuration
    @ComponentScan(basePackages = {"spring"}, 
     excludeFilters = { 
             @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = MemberDao.class )			
    })
    
    • classes 속성에는 제외할 타입의 목록을 지정

    설정할 필터가 두 개 이상이면 @ComponentScan의 exludeFilters 속성에 배열을 사용해 @Filter 목록을 전달

     // AppCtxWithExclude.java
     @Configuration
     @ComponentScan(basePackages = {"spring"}, 
         excludeFilters = { 
                 @Filter(type = FilterType.ANNOTATION, classes = ManualBean.class ),
                 (@Filter(type = FilterType.REGEX, pattern = "spring\\..*Dao" )
     })
    


2. 컴포넌트 스캔에 따른 충돌 처리


컴포넌트 스캔 기능을 사용해서 Bean을 등록할 경우, 아래와 같은 두 가지 충돌이 발생 가능

  1. Bean 이름의 충돌
  2. 수동 등록에 따른 충돌

2-1. Bean 이름 충돌

  • 서로 다른 패키지에 같은 클래스의 이름이 존재하고, 두 클래스 모두 @Component 어노테이션이 붙게 된다면,
    Exception이 발생하게 됨
  • 이럴 경우, 둘 중 하나에 명시적으로 Bean 이름을 지정해서 이름 충돌을 피해야 함

2-2. 수동 등록에 따른 충돌

  1. 같은 클래스를 같은 이름으로 설정할 때:
    • @Component 어노테이션을 통해 이미 지정한 Bean의 클래스 이름이 존재하지만,
      클래스 설정에 직접 해당 클래스를 동일한 이름으로 정해 등록하는 경우 발생
    • 즉, 스캔할 때 사용하는 Bean 이름과 수동 등록한 이름이 같을 경우, 수동 등록한 Bean이 우선시 됨
  2. 같은 클래스를 다른 이름으로 설정할 때:
    • 만약, 스캔할 때 사용하는 Bean 이름과 동일한 클래스를 다른 이름으로 수동 등록하는 경우에는
      다른 이름의 두 개의 Bean이 만들어지게 됨
    • 이런 경우에는 @Qualifier 어노테이션을 통해 알맞은 Bean을 선택해야 함