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