컴포넌트 스캔

HeeSeong·2021년 7월 28일
0
post-thumbnail

컴포넌트 스캔


자동 주입과 함께 사용하는 추가 기능이 컴포넌트 스캔이다. 컴포넌트 스캔은 스프링이 직접 클래스를 검색해서 빈으로 등록해주는 기능이다. 설정 클래스에 빈으로 등록하지 않아도 원하는 클래스를 빈으로 등록할 수 있으므로 컴포넌트 스캔 기능을 사용하면 설정 코드가 크게 줄어든다.


@Component 애노테이션 스캔 대상 지정


  • MemberDao.java
@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 애노테이션을 붙여야 한다. @Component 애노테이션은 해당 클래스를 스캔 대상으로 표시한다.

@Component 애노테이션에 값을 주었는지에 따라 빈으로 등록할 때 사용할 이름이 결정된다. 값을 주지 않으면 클래스 이름의 첫 글자를 소문자로 바꾼 이름을 빈 이름으로 사용한다. 예를 들어 "MemberDao"면 빈 이름으로 "memberDao"를 사용한다.

@Component 애노테이션에 값을 주면 그 값을 빈 이름으로 사용한다. 아래의 코드에서 클래스의 이름은 "MemberInfoPrinter"이지만 빈 이름으로 "infoPrinter"를 사용한다.

@Component("listPrinter")
public class MemberListPrinter {
	private MemberDao memberDao;
}

@ComponentScan 애노테이션 스캔 설정


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


  • AppCtx.java
@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 애노테이션을 붙인 클래스를 검색해서 빈으로 등록해주기 때문에, 설정 코드가 줄어든 것을 알 수 있다.

basePackages 속성은 스캔 대상 패키지 목록을 지정한다. 예제에서는 spring 이름의 패키지와 그 하위 패키지에 속한 클래스를 스캔 대상으로 설정한다. 스캔 대상 클래스 중에서 @Component 애노테이션이 붙은 클래스의 객체를 생성해서 빈으로 등록한다.


  • MainForSpring.java
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) {
		if (arg.length != 5) {
			printHelp();
			return;
		}
        	// 빈 이름 수정 필요
		MemberRegisterService regSvc = 
				ctx.getBean("memberRegSer", MemberRegisterService.class);
		RegisterRequest req = new RegisterRequest();
		req.setEmail(arg[1]);
		req.setName(arg[2]);
		req.setPassword(arg[3]);
		req.setConfirmPassword(arg[4]);
		
		if (!req.isPasswordEqualToConfirmPassword()) {
			System.out.println("암호와 확인이 일치하지 않습니다.\n");
			return;
		}
		try {
			regSvc.regist(req);
			System.out.println("등록했습니다.\n");
		} catch (DuplicateMemberException e) {
			System.out.println("이미 존재하는 이메일입니다.\n");
		}
	}

만약에 MemberRegisterService 클래스에 @Component 애노테이션을 붙여줬다면, 위의 클래스에서 getBean() 메서드의 "memberRegSer" 이름을 지우고 MemberRegisterService.class만 남겨야 한다. 애노테이션으로 인해 MemberRegisterService의 빈 객체 이름이 "memberRegisterService"로 등록되었을 것이기 때문이다.


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


  • AppCtx.java
@Configuration
@ComponentScan(basePackages = {"spring", "spring2" }, 
	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();
	}
}

이 코드는 @Filter 애노테이션의 typ 속성 값으로 REGEX를 주었다. 이는 정규표현식을 사용해서 제외 대상을 지정한다는 것을 의미한다. pattern 속성은 FilterType에 적용할 값을 설정한다. 위 설정에서는 "spring"으로 시작하고 Dao로 끝나는 정규표현식을 지정했으므로 spring.MemberDao 클래스를 컴포넌트 스캔 대상에서 제외한다.

특정 애노테이션을 붙인 타입을 컴포넌트 대상에서 제외할 수도 있다. 다음의 @ManualBean 애노테이션을 붙인 클래스는 컴포넌트 스캔 대상에서 제외한다.


  • ManualBean.java
@Retention(RUNTIME)
@Target(TYPE)
public @interface ManualBean {
}

애노테이션을 붙인 클래스를 컴포넌트 스캔 대상에서 제외하려면 excludeFilters 속성을 설정한다.

@Configuration
@ComponentScan(basePackages = {"spring", "spring2" }, 
	excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = ManualBean.class))
    
public class AppCtxWithExclude {
	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}
	
	@Bean
	@Qualifier("printer")
	public MemberPrinter memberPrinter1() {
		return new MemberPrinter();
	}
}

  • MemberDao.java
@ManualBean
@Component
public class MemberDao {
}

type 속성값으로 ANNOTATION을 사용하면 classes 속성에 필터로 사용할 애노테이션 타입을 값으로 준다. 이 코드는 @ManualBean 애노테이션을 제외 대상에 추가했으므로 다음 클래스를 컴포넌트 스캔 대상에서 제외한다.

이외에도 AspectJ 패턴을 사용해서 대상을 지정하는 FilterType.ASPECTJ 와 특정 타입이나 그 하위 타입을 컴포넌트 스캔 대상에서 제외하는 FilterType.ASSIGNABLE_TYPE 도 있다.


기본 스캔 대상


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

  • @Component

  • @Controller

  • @Service

  • @Repository

  • @Aspect

  • @Configuration


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


빈 이름 충돌


spring 패키지와 spring2 패키지에 MemberRegisterService 클래스가 존재하고 두 클래스 모두 @Component 애노테이션을 붙이면 에러가 발생한다.

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

이렇게 컴포넌트 스캔 과정에서 서로 다른 타입인데 같은 빈 이름을 사용하는 경우가 있다면 둘 중 하나에 명시적으로 빈 이름을 지정해서 이름 충돌을 피해야 한다.


수동 등록한 빈과 충돌


  • MemberDao.java
@Component
public class MemberDao {
}

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

	@Bean
    	public MemberDao memberDao() {
        	MemberDao memberDao = new MemberDao();
            return memberDao;
        }
}

MemberDao 클래스는 컴포넌트 스캔 대상이다. 그런데 다음과 같이 설정 클래스에 직접 MemberDao 클래스를 "memberDao" 라는 이름의 빈으로 등록하면 어떨까?

스캔할 때 사용하는 빈 이름과 수동 등록한 빈 이름이 같은 경우 수동 등록한 빈이 우선한다. 즉 MemberDao 타입 빈은 AppCtx에서 정의한 한 개만 존재한다.


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

	@Bean
    	public MemberDao memberDao2() {
        	MemberDao memberDao = new MemberDao();
            return memberDao;
        }
}

다음같이 다른 이름을 사용하면 자동 등록한 "memberDao" 빈과 수동 등록한 "memberDao2" 빈이 모두 존재한다. MemberDao 타입의 빈이 두 개가 생성되므로 자동 주입하는 코드는 @Qualifier 애노테이션을 사용해서 알맞은 빈을 선택해야 한다.

profile
끊임없이 성장하고 싶은 개발자

0개의 댓글