본문 바로가기
WEB/SPRING

[Spring] 컴포넌트 스캔

by snow_white 2022. 6. 3.

자동 주입과 함께 사용하는 추가 기능이 컴포넌트 스캔이다.

컴포넌트 스캔은 스프링이 직접 클래스를 검색해서 빈으로 등록해주는 기능이다.

설정 클래스에 빈으로 등록하지 않아도 원하는 클래스를 빈으로 등록할 수 있으므로 컴포넌트 스캔 기능을 사용하면 설정 코드가 크게 줄어든다.

 

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

스프링이 검색해서 빈으로 등록할 수 있으려면 클래스에 @Component 애노테이션을 붙여야 한다.

@Component 애노테이션은 해당 클래스를 스캔 대상으로 표시한다.

 

<@Component 애노테이션을 MemberDao 클래스에 붙인 예>

package spring;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.springframework.stereotype.Component;

@Component
public class MemberDao {

	private static long nextId = 0;

	private Map<String, Member> map = new HashMap<>();

	public Member selectByEmail(String email) {
		return map.get(email);
	}
	// 생략
}

<@Component 애노테이션에 속성값을 사용한 예>

package spring;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component("infoPrinter")
public class MemberInfoPrinter {

	private MemberDao memDao;
	private MemberPrinter printer;
    // 생략
}

@Component 애노테이션에 값을 주었는지에 따라 빈으로 등록할 때 사용할 이름이 결정된다.

위의 MemberDao 클래스에는 @Component 애노테이션에 값을 주지 않았다.

이 경우 클래스 이름의 첫 글자를 소문자로 바꾼 이름빈 이름으로 사용한다.

예를 들어 클래스 이름이 MemberDao이면 빈 이름으로 "memberDao"를 사용하고 클래스 이름이 MemberRegisterService이면 빈 이름으로 "memberRegisterService"를 사용한다.

 

@Component 애노테이션에 값을 주면 그 값을 빈 이름으로 사용한다.

위의 MemberInfoPrinter 클래스의 경우는 빈 이름을 "InfoPrinter"로 사용한다는 의미이다.

 

 

2. @Component 애노테이션으로 스캔 설정

@Component 애노테이션을 붙인 클래스를 스캔해서 스프링 빈으로 등록하려면 설정 클래스에 @ComponentScan 애노테이션을 적용해야 한다.

설정 클래스인 AppCtx에 @ComponentScan 애노테이션을 적용한 코드는 아래와 같다.

package config;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import spring.MemberPrinter;
import spring.MemberSummaryPrinter;
import spring.VersionPrinter;

@Configuration
@ComponentScan(basePackages = {"spring"})
public class AppCtx {

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

스프링 컨테이너가 @Component 애노테이션을 붙인 클래스를 검색해서 빈으로 등록해주기 때문에 설정 코드가 줄어든 것을 알 수 있다.

@ComponentScan 애노테이션의 basePackages 속성값은 {"spring"}이다.

이 속성은 스캔 대상 패키지 목록을 지정한다.

스캔 대상에 해당하는 클래스 중에서 @Component 애노테이션이 붙은 클래스의 객체를 생성해서 빈으로 등록한다.

 

 

3. 예제 실행

MainForSpring 클래스에서 빈을 검색하는 코드를 변경해주어야 한다.

public class MainForSpring {

	private static ApplicationContext ctx = null;
	
	public static void main(String[] args) throws IOException {
		ctx = new AnnotationConfigApplicationContext(AppCtx.class);
		
		BufferedReader reader = 
				new BufferedReader(new InputStreamReader(System.in));
		// 생략
	}

	private static void processNewCommand(String[] arg) {
		// 생략
		MemberRegisterService regSvc = 
				ctx.getBean("memberRegSvc", MemberRegisterService.class);
		// 생략
	}

	private static void processChangeCommand(String[] arg) {
		// 생략
		ChangePasswordService changePwdSvc =
				ctx.getBean("changePwdSvc", ChangePasswordService.class);
		// 생략
	}

	private static void processListCommand() {
		MemberListPrinter listPrinter = 
				ctx.getBean("listPrinter", MemberListPrinter.class);
	}

	private static void processInfoCommand(String[] arg) {
		// 생략
		MemberInfoPrinter infoPrinter = 
				ctx.getBean("infoPrinter", MemberInfoPrinter.class);
	}
	
	private static void processVersionCommand() {
		VersionPrinter versionPrinter = 
				ctx.getBean("versionPrinter", VersionPrinter.class);
	}

}

 

아래 코드에서 MemberRegisterService 타입 빈과 ChangePasswordService 타입의 빈은 이름이 달라졌다.

이 두 클래스 이름의 첫 글자를 소문자로 바꾼 이름을 빈 이름으로 사용한다.

따라서 MemberRegisterService 타입 빈 객체의 이름은 "memberRegisterService"가 되고 ChangePasswordService 타입 빈은 "changePasswordService"가 된다.

public class MainForSpring {

	private static ApplicationContext ctx = null;
	
	public static void main(String[] args) throws IOException {
		ctx = new AnnotationConfigApplicationContext(AppCtx.class);
		// 생략
	}

	private static void processNewCommand(String[] arg) {
		MemberRegisterService regSvc = 
				ctx.getBean(MemberRegisterService.class);.println("이미 존재하는 이메일입니다.\n");
	}

	private static void processChangeCommand(String[] arg) {
		ChangePasswordService changePwdSvc =
				ctx.getBean(ChangePasswordService.class);
	}
}

 

 

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

excludeFilters 속성을 사용하면 스캔할 때 특정 대상을 자동 등록 대상에서 제외할 수 있다.

다음 코드는 excludeFilters 속성의 사용 예를 보여준다.

@Filter 애노테이션의 type 속성값으로 FilterType.REGEX를 주었다.

이는 정규표현식을 사용해서 제외 대상을 지정한다는 것을 의미한다.

pattern 속성은 FilterType에 적용할 값을 설정한다.

"spring"으로 시작하고 Dao 로 끝나는클래스를 컴포넌트 스캔 대상에서 제외한다.

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;

@Configuration
@ComponentScan(basePackages = {"spring"}, 
	excludeFilters = { 
			@Filter(type = FilterType.REGEX, pattern = "spring\\..*Dao" ))
public class AppCtxWithExclude {
	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}
	
	@Bean
	@Qualifier("printer")
	public MemberPrinter memberPrinter1() {
		return new MemberPrinter();
	}
}

 FilterType.ASPECTJ를 필터 타입으로 설정할 수도 있다.

이 타입을 사용하면 정규표현식 대신 AspectJ 패턴을 사용해서 대상을 지정한다.

AspectJ 패턴은 정규표현식과 다른데 이 설정을 사용하면 spring 패키지에서 이름이 Dao로 끝나는 타입을 컴포넌트 스캔 대상에서 제외된다.

@Configuration
@ComponentScan(basePackages = {"spring"}, 
	excludeFilters = { 
			@Filter(type = FilterType.ASPECTJ, pattern = "spring.*Dao" ))
public class AppCtxWithExclude {
	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}
}

 

AspectJ 패턴이 동작하려면 의존 대상에 aspectjweaver 모듈을 추가해야 한다.

<dependencies>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-context</artifactId>
		<version>5.0.2.RELEASE</version>
	</dependency>

	<dependency>
		<groupId>org.aspectj</groupId>
		<artifactId>aspectjweaver</artifactId>
		<version>1.8.13</version>
	</dependency>
		
</dependencies>

 

 

4.1 기본 스캔 대상

@Component 애노테이션을 붙인 클래스만 컴포넌트 스캔 대상에 포함되는 것은 아니다.

다음 애노테이션을 붙인 클래스가 컴포넌트 스캔 대상에 포함된다.

  • @Component(org.springframework.stereotype 패키지)
  • @Controller(org.springframework.stereotype 패키지)
  • @Service(org.springframework.stereotype 패키지)
  • @Repository(org.springframework.stereotype 패키지)
  • @Aspect(org.aspect.lang.annotation 패키지)
  • @Configuration(org.springframework.context.annotation 패키지)

@Aspect 애노테이션을 제외한 나머지 애노테이션은 실제로는 @Component 애노테이션에 대한 특수 애노테이션이다.

예를 들어 @Controller 애노테이션은 다음과 같다.

@Targer({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller{
	@AliasFor(annotation = Component.class)
    String value() default "";
}

@Component 애노테이션이 붙어 있는데, 스프링은 @Controller 애노테이션을 @Component 애노테이션과 동일하게 컴포넌트 스캔 대상에 포함한다.

@Controller 나 @Repository 애노테이션 등은 컴포넌트 스캔 대상이 될뿐만 아니라 스프링 프레임워크에서 특별한 기능과 연관되어 있다.

예를 들어 @Controller 애노테이션은 웹 MVC와 관련 있고 @Repository 애노테이션은 DB 연동과 관련 있다.

 

 

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

컴포넌트 스캔 기능을 사용해서 자동으로 빈을 등록할 때에는 충돌에 주의해야 한다.

크게 빈 이름 충돌과 수동 등록에 따른 충돌이 발생할 수 있다.

 

5.1 빈 이름 충돌

spring 패키지와 spring2 패키지에 MemberRegisterService 클래스가 존재하고 두 클래스 모두 @Component 애노테이션을 붙였다고 하자. 이 상태에서 다음 @ComponentScan 애노테이션을 사용하면 어떻게 될까?

@Configuration
@ComponentScan(basePackages = {"spring", "spring2" })
public class AppCtx {
	...
}

❌ 에러 발생!!

spring2.MemberRegisterService 클래스를 빈으로 등록할 때 사용한 빈 이름인 memberRegisterService가 타입이 일치하지 않는 spring.MemberRegisterService 타입의 빈 이름과 충돌난다는 것을 알 수 있다.

따라서 둘 중 하나에 명시적으로 빈 이름을 지정해서 이름 충돌을 피해야 한다.

 

5.2 수동 등록한 빈과 충돌

MemberDao 클래스에 @Component 애노테이션을 붙였었다.

따라서 MemberDao 클래스는 컴포넌트 스캔 대상이다.

자동 등록된 빈의 이름은 클래스 이름의 첫 글자를 소문자로 바꾼 "memberDao"이다.

그래서 다음과 같이 설정 클래스에 직접 MemberDao 클래스를 "memberDao"라는 이름의 빈으로 등록하면 어떻게 될까?

@Configuration
@ComponentScan(basePackages = {"spring"})
public class AppCtx {
	@Bean
    public MemberDao memberDao(){
    	MemberDao memberDao = new MemberDao();
        return memberDao;
    }
}

스캔할 때 사용하는 빈 이름과 수동 등록한 빈 이름이 같은 경우 수동 등록한 빈이 우선한다.

즉 MemberDao 타입 빈은 AppCtx에서 정의한 한 개만 존재한다.

 

다음과 같이 다른 이름을 사용한다면?

@Configuration
@ComponentScan(basePackages = {"spring"})
public class AppCtx {
	@Bean
    public MemberDao memberDao2(){
    	MemberDao memberDao = new MemberDao();
        return memberDao;
    }
}

이 경우 스캔을 통해 등록한 "memberDao" 빈과 수동 등록한 "memberDao2" 빈이 모두 존재한다.

MemberDao 타입의 빈이 두 개가 생성되므로 자동 주입하는 코드는 @Qualifier 애노테이션을 사용해서 알맞은 빈을 선택해야 한다.

 

@Qualifier 애노테이션을 이용한 의존 객체 선택

아래 코드와 같이 @Qualifier 애노테이션 값으로 일치하는 빈이 두 개 이상 존재할 때 한정자로 필드 이름을 지정하여 어떤 빈을 주입할지 결정할 때 사용한다.

@confifuration
public class AppCtx{
      @Bean
      public MemberPrinter printer(){
            return new MemberPrinter();
      }
      @Bean
      @Qualifier("mprinter")
      public MemberPrinter printer2(){
            return new  MemberPrinter();
      }
}

 

 

'WEB > SPRING' 카테고리의 다른 글

[Spring] AOP 프로그래밍  (0) 2022.06.03
[Spring] 빈(Bean) 🟢  (0) 2022.06.03
[Spring] @Configuration 설정 클래스의 @Bean 설정과 싱글톤  (0) 2022.06.03
[Spring] Inversion of Control  (0) 2022.06.03
[Spring] Dependency Injection  (0) 2022.06.02

댓글