자동 주입과 함께 사용하는 추가 기능이 컴포넌트 스캔이다. 컴포넌트 스캔은 스프링이 직접 클래스를 검색해서 빈으로 등록해주는 기능이다. 설정 클래스에 빈으로 등록하지 않아도 원하는 클래스를 빈으로 등록할 수 있으므로 컴포넌트 스캔 기능을 사용하면 설정 코드가 크게 줄어든다.
@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;
}
@Component 애노테이션을 붙인 클래스를 스캔해서 스프링 빈으로 등록하려면 설정 클래스에 @ComponentScan 애노테이션을 적용해야 한다.
@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 애노테이션이 붙은 클래스의 객체를 생성해서 빈으로 등록한다.
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"로 등록되었을 것이기 때문이다.
@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 애노테이션을 붙인 클래스는 컴포넌트 스캔 대상에서 제외한다.
@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();
}
}
@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 타입의 빈 이름과 충돌난다는 것을 알 수 있다.
이렇게 컴포넌트 스캔 과정에서 서로 다른 타입인데 같은 빈 이름을 사용하는 경우가 있다면 둘 중 하나에 명시적으로 빈 이름을 지정해서 이름 충돌을 피해야 한다.
@Component
public class MemberDao {
}
@Configuration
@ComponentScan(basePackages = {"spring"})
public class AppCtx {
@Bean
public MemberDao memberDao() {
MemberDao memberDao = new MemberDao();
return memberDao;
}
}
MemberDao 클래스는 컴포넌트 스캔 대상이다. 그런데 다음과 같이 설정 클래스에 직접 MemberDao 클래스를 "memberDao" 라는 이름의 빈으로 등록하면 어떨까?
스캔할 때 사용하는 빈 이름과 수동 등록한 빈 이름이 같은 경우 수동 등록한 빈이 우선한다. 즉 MemberDao 타입 빈은 AppCtx에서 정의한 한 개만 존재한다.
@Configuration
@ComponentScan(basePackages = {"spring"})
public class AppCtx {
@Bean
public MemberDao memberDao2() {
MemberDao memberDao = new MemberDao();
return memberDao;
}
}
다음같이 다른 이름을 사용하면 자동 등록한 "memberDao" 빈과 수동 등록한 "memberDao2" 빈이 모두 존재한다. MemberDao 타입의 빈이 두 개가 생성되므로 자동 주입하는 코드는 @Qualifier 애노테이션을 사용해서 알맞은 빈을 선택해야 한다.