AppConfig라는 구성설정 파일을 만들어서 스프링빈을 등록했고 등록하는 과정에서 자동으로 의존관계도 주입되었다.
하지만 등록해야하는 스프링빈이 많아지면 설정 정보도 커지고 누락하는 문제가 발생한다. ( + 귀찮음)
그래서 스프링에서는 설정정보 없이 자동으로 스프링 빈을 등록하는 컴포넌트 스캔을 제공한다.
의존관계도 자동으로 주입하는 @Autowired라는 기능도 제공한다.
공부를 위해 원래있던 AppConfig는 냅두고 새로 AutoAppConfig를 생성한다.
@ComponentScan을 사용하면 @Component 어노테이션이 붙은 모든 클래스를 찾아서 자동으로 스프링빈으로 등록해준다.
AutoAppConfig.java
@Configuration // 설정정보 파일이니까 @Configuration 어노테이션을 필수다.
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Configuration.class)
) // 컴포넌트 스캔을 이용하여 @Component 어노테이션이 붙은 클래스를 전부 스프링빈으로 등록해줄것이다.
//그전에 필터를 통해서 @Configuration 어노테이션이 붙은 클래스는 등록하지않는다 -> 기존에있는 AppConfig가 등록되면 안되니까.
//컴포넌트스캔에 걸리는 이유는 @Configuration안에는 @Component이 있기떄문이다.
public class AutoAppConfig {
//내용이 없다.
}
AppConfig.java와 다르게 @Bean~ 이런 빈등록하는 코드가 없다.
@Component가 붙은 클래스를 자동으로 등록해주기 때문이다.
그렇다면 스프링빈으로 등록될수 있게끔 클래스들에 @Component를 추가해준다.
사용할 구현체클래스에 가서 @Component를 붙여준다.
@Component
public class RateDiscountPolicy implements DiscountPolicy {
AppConfing에서
@Bean
public DiscountPolicy DiscountPolicy() {
return new RateDiscountPolicy();
}
이런식으로 등록을 했는데 이때 메소드명이 빈 이름이고, 빈 타입은 반환값이였다.
즉 구현체를 스프링빈으로 등록하는것이니까 구현체 클래스에 가서 @Component를 붙여준다.
@Component
public class MemoryMemberRepository implements MemberRepository {
메모리멤버리포지토리에도 붙여주고
@Component
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
멤버서비스임플에도 붙여줬는데 멤버서비스구현체 안에는
원래 스프링빈에 등록된 멤버리포지토리를 주입받아 필드로 사용중이였다.
@Component만 쓰면 AutoAppConfig의 @ComponentScan으로 인해 클래스를 빈으로 등록해줄뿐
멤버서비스임플이 멤버리포지토리를 의존하는 의존관계 설정은 해주지 못한다.
이럴때 의존관계 설정을 위해 (의존관계 주입) @Autowired를 사용한다.
@Component
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
다음과 같이 의존관계를 주입받기위해 있는 생성자위에다가 @Autowired를 붙여준다.
@Autowired가 붙어있으면 전달인자인 MemberRepository를 보고 스프링컨테이너에 에서 알맞은 빈을 꺼내다 주입해준다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
오더서비스임플도 어노테이션 붙여준다.
빈등록이 잘됬는지 테스트
public class AutoAppConfigTest {
@Test
void basicScan() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(
AutoAppConfig.class);
String[] beanDefinitionNames = ac.getBeanDefinitionNames(); //빈 메타정보들 이름을 가져온다. = 빈 이름을 가져온다.
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName); //빈 메타정보 이름을 이용해서 해당 빈 메타정보를 가져온다.
// 빈 이름을 이용해서 빈 메타정보를 가져온다. getBeanDefinition메소드를 쓰기위해 AnnotationConfigApplicationContext객체에 담아 사용한다.
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) { //빈 메타정보안에 Role정보를 이용해서, 내가 등록한 빈이라면 (스프링 자체에서 올린게아니고)
System.out.println("beanDefinitionName = " + beanDefinitionName); //빈 이름을 출력해본다.
}
}
MemberService memberService = ac.getBean(MemberService.class); //검증을 위해 스프링빈에 등록된 멤버서비스 구현체를 꺼내온다.
assertThat(memberService).isInstanceOf(MemberService.class); //멤버서비스의 구현체인지 검증
}
}
@Component를 붙여준 구현체클래스가 잘 출력되는것을 확인할 수 있다.
@ComponentScan 동작방식
@Component가 붙은 클래스명이 빈이름이 되고(맨앞글자는 소문자로 바뀌어서) 그 클래스의 객체가 빈 객체가 된다.
컴포넌트스캔을 안쓰고 AppConfig에서 직접 @Bean을 이용해서 등록해줄때는 메소드명이 빈이름이 됬고
반환되는값(구현체)가 빈객체가 됬었다.
@Autowired 동작방식
탐색위치와 기본 스캔 대상
탐색할 패키지의 시작 위치 지정
모든 자바 클래스를 컴포넌트 스캔하면 시간이 오래걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작위치를 지정할 수 있다.
@Configuration // 설정정보 파일이니까 @Configuration 어노테이션을 필수다.
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class),
basePackages = "com.example.demo"
)
public class AutoAppConfig {
}
basePackages = "찾기 시작할 시작위치"로 지정가능하다.
만약
basePackages = "com.example.demo.member"
로 수정했다면 member 패키지 아래있는것들만 스캔이 되서 등록이 될것이다.
테스트해보자.
이렇게 탐색위치를 지정해주지않으면 모든 클래스파일을 찾아볼것이다 (라이브러리까지도) , 그런 작업은 시간이 오래 소요된다.
탐색할 패키지의 시작위치를 지정하지않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작위치가 된다.
그러므로 설정정보(구성정보)클래스 ex) AppConfig.java AutoAppConfig.java
를 프로젝트 최상단에 위치시키는것이다.
package com.example.demo; // AutoAppConfig가 속한 패키지 위치
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
@Configuration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class),
basePackages = "com.example.demo.member"
)
public class AutoAppConfig {
}
프로젝트 최상위위치에 ~Application이라고 있을것이다.(자동으로 거기 생성됨) 이름은 내가 지정한 이름이 앞에 붙는다.
(== 스프링부트를 실행시키는 클래스)
내용을 보면 @SpringBootApplication이 있고 @SpringBootApplication를 클릭해서 들어가보면
컴포넌트 스캔이 존재한다.
즉 스프링 부트를 쓰면 내 프로젝트 위치부터 다 컴포넌트스캔을 하겠다는것이다.
(스프링부트를 쓰면 사실 @ComponentScan을 쓸 필요가없다고한다. ~Application 클래스에서 스프링을 부팅하면서 다 스캔을 해주니까)
컴포넌트 스캔 기본대상
컴포넌트 스캔 기본대상 설명
필터
테스트 해보기전에 임의로 어노테이션을 만든다.
package com.example.demo.scan.filter;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
//위의 3가지 어노테이션은 @Component 내부의 어노테이션을 복사붙여넣었다.
public @interface MyIncludeComponent { //임의로 만든 어노테이션,이 어노테이션이 붙은 애들만 컴포넌트스캔할것이다.
}
@MyIncludeComponent 해당 어노테이션이 있으면 컴포넌트 스캔을한다.
package com.example.demo.scan.filter;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyexcludeComponent { //임의로 만든 어노테이션,이 어노테이션이 붙은 애들은 스캔에 제외한다.
}
@MyexcludeComponent 해당 어노테이션이 있으면 스캔에서 제외한다.
빈등록할 임의의 클래스를 만든다.
BeanA.class
package com.example.demo.scan.filter;
@MyIncludeComponent
public class BeanA {
}
@MyIncludeComponent이 있으므로 스캔이 되야한다.
package com.example.demo.scan.filter;
@MyexcludeComponent
public class BeanB {
}
@MyexcludeComponent이 있으므로 스캔이 되면 안된다.
테스트코드
public class ComponentFilterAppConfigTest {
@Test
void filterScan(){
ApplicationContext ac = new AnnotationConfigApplicationContext(
ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class); // 클래스명이 빈 이름인데 , 첫글자는 소문자가 된다. 그래서 빈 이름은 beanA
assertThat(beanA).isNotNull(); // beanA는 스캔대상이므로 null이면 안된다.
assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("beanB", BeanB.class)); //beanB를 가져오려고하면 예외가 발생한다.
}
@Configuration
@ComponentScan(
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class), //스캔에 포함 설정
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyexcludeComponent.class) // 스캔 제외 설정
)
static class ComponentFilterAppConfig { //테스트용 구성설정 클래스이다, 구성설정 클래스이므로 @Configuration붙여줘야하고
//스캔을 테스트할거니까 @ComponentScan 붙여준다.
}
}
type = FilterType.ANNOTATION,
부분은 기본값이라 생략해도 돌아간다.
중복 등록과 충돌
컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게될까?
두가지 경우가있다.
1. 수동 빈등록 vs 수동 빈등록 서로 충돌
2. 자동 빈등록 vs 수동 빈등록 서로 충돌
수동 빈 등록 -> @Bean을 이용해서 빈 등록 메소드를 만든다 (수동으로 빈 생성,의존관계 생성[전달인자 적어야하니까])
public class MemoryMemberRepository implements MemberRepository {}
public class AutoAppConfig {
@Bean(name = "memoryMemberRepository")
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
충돌을 만들기위해 AutoAppConfig에 빈등록을 해준다. 이름은 memory~로 해준다.
컴포넌트스캔으로 만들어지는 빈이름은 클래스이름인데 맨앞글자가 소문자이기 때문이다.
그렇게 해놓고 테스트를 해보면 오류없이 정상동작한다. 로그에서는 같은 이름이 빈이 등록되어서 오버라이드 됬다고 나온다.
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
스프링부트 실행 클래스를 실행시 등록된 빈중 중복된 이름을 가진 빈이 있다면 아예 오류메시지를 띄우면서 스프링이 안올라간다.
application.properties에 오버라이딩 설정 코드를 추가해주면 수동빈으로 오버라이딩 시켜줘서 스프링이 잘 올라간다.
댓글