728x90
1. 컴포넌트 스캔과 의존관계 자동 주입
- 컴포넌트 스캔 필요성
- 등록해야 할 spring bean이 수백개가 되면 일일이 등록하기 힘들고, 설정 정보도 커지고, 누락하는 문제도 발생
- bean 등록을 해주는 컴포넌트 스캔과 더불어 의존관계를 주입 해주는 @Autowired도 제공
- @Bean 등록 Overiding, 예외 처리 조건
- SpringBootApplication 실행 시 : 동일 @Bean 명일 경우 NoUniqueBeanDefinitionException 발생
- SpringApplication 실행 시 : 동일 @Bean 명이여도 overriding 해버림 - 위험
- 단위 Test 실행 시 : 동일 @Bean 명이여도 overriding 해버림 - 위험
- Spring Test 실행 시 : 동일 @Bean 명이여도 overriding 해버림 - 아직 안해봄
- @ComponentScan : @Component 등록된 것들을 @Bean으로 자동 등록, @ComponentScan을 탐색하진 않는다.
- java에서 수행한 @ComponentScan
- test 경로를 제외한 java에서 @ComponentScan이 시작되는 동일, 하위 경로 탐색
- 경로 상관없이 java에서 static으로 설정된 @Configuratoin 파일인 경우
- test에서 수행한 @ComponentScan
- test 경로를 포함한 java에서 @ComponentScan이 시작되는 동일, 하위 경로 탐색
- 경로 상관없이 test에서 static으로 설정된 @Configuratoin 파일인 경우
- java에서 수행한 @ComponentScan
- @Configuration : 내부에 @Component 존재
- 정리
- java project가 run 할 때는 시발점이 @SpringBootApplication 뿐이다. 해당 파일의 조건에 맞는 @Configuration과 @Component만 bean으로 등록
cf> @ComponentScan 파일을 만들었다고 무조건 작동한다고 착각하지 말 것 - 단위 test의 경우 내가 등록한 applicationContext의 매개변수로부터 @Bean 등록
cf> 이 때는수동 등록 파일(@AppConfig)인지 자동 등록 파일(@AutoAppConfig)인지 잘 인지해서 사용
- java project가 run 할 때는 시발점이 @SpringBootApplication 뿐이다. 해당 파일의 조건에 맞는 @Configuration과 @Component만 bean으로 등록
- 표
@Bean 등록 환경 | 중복 @Bean 발생 시, 동작 방식 |
SpringBootApplication | NoUniqueBeanDefinitionException |
SpringApplication | Overriding 과 warnning |
단위 Test | Overriding 과 warnning |
SpringBootTest | NoUniqueBeanDefinitionException |
등록 방식 | 실행 경로 | 적용 |
@ComponentScan(자동) | java package | java package의 @ComponentScan 시작 경로부터 하위 경로 |
java package의 경로 상관없이 @Configuration static Class {} | ||
@ComponentScan(자동) | test package (단위 test) | java package의 @ComponentScan 시작 경로부터 하위 경로 |
java package의 경로 상관없이 @Configuration static Class {} | ||
test package의 경로 상관없이 @Configuration static Class {} |
2. 실습 코드
- 주의점
- @SpringApplicationContext에서 수행되는 @ComponentScan은 수동등록인 @Configuration과 중복되어도 정상 동작
- 정상동작한 이유는 등록된 bean은 내용상 같지만 bean의 이름이 다르기 때문에 정상적으로 동작한 것 (헷갈리지 말 것)
- 사실상, 중복 Bean 이기 때문에 수동 등록 혹은 자동 등록 2개 중 하나만 할 것
- 만약 자동등록이나 수동등록 파일의 bean이름을 동일하게 지정시, 에러 발생
* 수동 bean 명 : method명
* 자동 bean 명 : class명
- @SpringApplicationContext에서 수행되는 @ComponentScan은 수동등록인 @Configuration과 중복되어도 정상 동작
2.1. 컴포넌트 스캔 실습
2.1.1. 컴포넌트 스캔 공통
- java 실행 파일
@SpringBootApplication
public class CoreApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(CoreApplication.class, args);
for (String beanDefinitionName : context.getBeanDefinitionNames()) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}
}
}
- test 실행 파일
public class AutoAppConfigTest {
@Test
void basicScan() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
for (String beanDefinitionName : ac.getBeanDefinitionNames()) {
System.out.println("name = " + beanDefinitionName);
}
}
}
- 수동 설정 파일
package hello.core;
@Configuration // 내부에 @Component 존재
public class AppConfig {
// select object
@Bean // spring container 에 넣기
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
- 자동 설정 파일
package hello.core;
@Configuration
@ComponentScan(
basePackages = "hello.core"
// excludeFilters = @ComponentScan.Filter(
// type = FilterType.ANNOTATION, // 타입에 어노테이션
// classes = Configuration.class) // @Configuration public class Class명 {}
)
public class AutoAppConfig {
}
2.1.2. java 내에서 예외 처리 되는 과정
- ComponentScan 파일
package hello.core.member;
@Component
public class MemoryMemberRepository implements MemberRepository {
// 임시 저장소 (db와 동일한 역할): store
private static Map<Long, Member> store = new HashMap<>(); // 동시성 이슈 때문에 concurrentHashMap을 실무에서는 사용한다.
@Override
public void save(Member member) { store.put(member.getId(), member); }
@Override
public Member findById(Long memberId) { return store.get(memberId); }
}
- 실행 결과 : NoUniqueBeanDefinitionException 발생
- 자동 설정 파일의 @Configuration 때문에 중복 @Bean 설정 됨
- 만약 자동 설정 파일 @Configuration이 없으면 수동과 자동 등록만 존재하므로 정상 동작 (헷갈림 주의)
2.1.3. test 파일에서는 overriding 되는 과정
- 실행 결과 : Overriding 예외만 발생 및 test의 static @Configuration Bean도 등록
- 이렇게 되는 상황이 위험한 경우 이기 때문에 test 정확도를 높이기 위해서 필요없는 @bean은 등록하지 않기 위해서 excludeFilters 조건을 건다.
2.2. 컴포넌트 스캔 적용
- spring container 설정 과정
- 모든 @Component 찾아서 등록 (의존관계 주입을 바로 수행하지 않는다.)
- 의존 관계 주입을 수행 (@Component가 다 등록 된 이후에 수행)
- 하지만 예외적으로 생성자 주입이나 수동 bean 설정 같은 경우에는 java code 특성상 1,2, 단계가 동시에 진행
2.2.1. spring bean 등록
- @ComponentScan - @Component
- @ComponentScan : @Component 붙은 클래스를 스캔해서 스프링 빈으로 등록
- @Component: springContainer에 bean으로 등록
- @Bean 이름 : class명 소문자로 작성 - default
- @Component("직접지정하는 Bean 이름")
- @Configuration : @Component 가 존재하므로 스프링빈으로 등록
- 심화
- @ComponentScan이 적용된 AppConfig는 실제로 @Configuration이 필요가 없다.
하지만 관례상 작성을 한다. - @Component 로 spring Contaioner에 등록 된 객체는 proxy와 CGLIB를 통해서 싱글톤이 유지된다.
- @ComponentScan이 적용된 AppConfig는 실제로 @Configuration이 필요가 없다.
2.2.2. spring bean 의존 관계 주입
- @Autowired
- 이전 처럼 생성자 주입 java code 작성을 통한 의존관계 주입이 불가
- 실제 @Component class의 생성자에 @Autowired를 작성하여 DI를 수행
- DI 과정 및 문제
- 먼저 springContainer 내에 동일 type의 bean 찾고 DI 수행
- ex) MemberRepository type 이면 bean type이 MemberRepository부터 하위 타입 모두 찾는다.
- getBean(매개변수의 type.class)로 찾는다. ex) getBean(MemberRepository.class)
- 문제 상황
- 빈 없으면 NoSuchBeaDefinitionException 발생
- getBean할 때 동일 type bean 많으면 NounqieException 발생
- 먼저 springContainer 내에 동일 type의 bean 찾고 DI 수행
@Component
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) { // type: MemberRepository
this.memberRepository = memberRepository;
}
}
- AppConfig
// CompenentScan 적용
@Configuration // 관례상 작성, ComponentScan으로 다 해결 가능
@ComponentScan
public class AppConfig{}
// ComponentScan 하기 전
@Configuration
public class AppConfig{
@Bean
public MemberService memberService(){ return new MemberServiceImpl(memberRepository()); }
@Bean
public Orderservice orderService(){ return new OrderServiceImpl(memberRepository(), discoutnPolicy()) }
@Bean
public MemberRepository memberRepository(){ return new MemoryMemberRepository(); }
@Bean
public DiscountPolicy discoutnPolicy(){ return new FixDiscountPolicy(); }
}
- @Component, @Autowired code
// MemoryMemberRepository
@Component
public class MemoryMemberRepository implements MemberReposotory{
''' 생략 '''
}
// RateDiscountPolicy
@Component
public class RateDiscountPolicy implements DiscountPlicy{
''' 생략 '''
}
// MemberServiceImpl
@Component
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemoryMemberRepository memberRepository){
this.memberRepository = memberRepository;
}
''' 생략 '''
}
// OrderServiceImpl
@Component
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
pirvate final DiscountPolicy discountPolicy;
@Autowired
OrderServiceImpl(MemberRepository memberRepository, DiscountPolciy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
더보기
AppConfig의 방식을 생각
1. Spring bean으로 등록 되야 하는 부분을 @component로 등록 - 구체화가 되어진 class에 붙인다. bean에는 객체가 들어가져야 한다.
2. 의존 관계 주입은 ServiceImpl에 생성자를 보면 이 부분이 DI를 하는 부분이므로 @Autowired를 넣어준다.
그러면 스프링 컨테이너가 @Autowired 된 생성자의 paramsTYPE과 같은 bean을 찾는다. 존재하면 의존관계를 주입한다.
생성자에 params가 아무리 많아도 다 찾아서 자동 주입한다.
- 등록 과정 그림
3. 탐색 위치와 기본 스캔 대상
3.1. 탐색 범위
- 탐색 범위를 지정하는 이유는 모든 자바 클래스를 다 스캔하려면 시간이 오래 걸리기 때문
- 탐색 위치
- default: @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치
- basePackages 존재시, 해당 경로가 시작 위치
package hello.core; // basPackages 지정안할 경우 default scanning 시작 위치
@Configuration
@ComponentScan(
basePackages = "hello.core.member",
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
}
- 탐색 위치 지정
- 자동
- 설정 정보 클래스의 위치를 프로젝트 최상단에 두기 (권장)
- 최근 스프링 부트도 이 방법을 기본으로 제공
- springboot: @SpringBootApplication 프로젝트 시작 루트 위치에 두는 것이 관례
- 수동
- @ComponentScan( basePackages = "hello.core")
- basePackages: 탐색할 패키지의 시작 위치 지정. 해당 패키지 부터 하위 패키지 까지
- 자동
3.2. 탐색 대상
- ComponentScan 기본 대상 - 아래 어노테이션들이 @Component를 포함
- @Component : 컴포넌트 스캔에서 사용
- @Controller : 스프링 MVC 컨트롤러에서 사용
- @Service : 스프링 비즈니스 로직에서 사용
- @Repository : 스프링 데이터 접근 계층에서 사용
- @Configuration : 스프링 설정 정보에서 사용
- @ControllerAdvice : 전역에 존재하는 예외를 잡아주는 역할
- 참고
- ComponentScan은 useDefaultFilters 옵션은 기본으로 켜져있는데, 이 옵션을 끄면 기본 스캔 대상들이 제외
- 사실 애노테이션에는 상속관계라는 것이 없다.
- 애노테이션이 특정 애노테이션을 들고 있는 것을 인식할 수 있는 것은 자바 언어가 지원하는 기능은 아니고, 스프링이 지원하는 기능
- 스프링 지원 기능
- @Component 기능을 포함하고 부가 가능을 수행 가능
- @Controller : 스프링 MVC 컨트롤러로 인식
- @Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환
- @Configuration : 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가처리
- @Service : @Service는 특별한 처리안한다. 개발자들이 핵심 비즈니스 로직 계층을 인식하는데 도움
- @Component 기능을 포함하고 부가 가능을 수행 가능
4. 필터
- 사전 주의
- @Component면 충분하기 때문에 includeFilters(거의 안씀), excludeFilter(간혹 씀) 로 옵션 변경해가면서 사용하지 말 것
- spring의 기본 설정에 최대한 맞추어 사용하는 것을 권장
- includeFilters: @ComponentScan 대상을 추가로 지정
- excludeFilters: @ComponentScan에서 제외할 대상을 지정
4.1. code
- include interface & class
package hello.core.scan.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {}
package hello.core.scan.filter;
@MyIncludeComponent
public class BeanA {}
- exclude interface & class
package hello.core.scan.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {}
package hello.core.scan.filter;
@MyExcludeComponent
public class BeanB {}
package hello.core.scan.filter;
public class ComponentFilterApaConfigTest {
@Test
void filterScan(){
ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
assertThat(beanA).isNotNull();
assertThrows(
NoSuchBeanDefinitionException.class,
() -> ac.getBean("beanB", BeanB.class)
);
}
@Configuration
@ComponentScan(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class))
static class ComponentFilterAppConfig {}
}
4.2. FilterType 옵션
- ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다. - 이것만 거의 씀. 나머지는 그냥 있다 정도
- ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다. - 어쩌다가 씀
- ASPECTJ: AspectJ 패턴 사용
- REGEX: 정규 표현식
- CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리
5. 중복과 충돌 (다른 책으로 한번 더 찾아보기 - 애매함)
- case
- 자동 vs 자동
- 수동 vs 자동
5.1. 자동 빈 등록 vs 자동 빈 등록
- 자동 빈 등록 vs 자동 빈 등록 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 이름이 같은 경우 스프링은 오류를 발생시킨다.
- ConflictingBeanDefinitionException 예외 발생
5.2. 수동 빈 등록 vs 자동 빈 등록
// 수동 bean 등록
public class AutoAppConfig {
@Bean(name = "memoryMemberRepository")
MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
- 스프링
- 수동 빈 등록이 우선권을 가진다.
- 수동 빈이 자동 빈을 오버라이딩 해버린다.
- 그래도 주의 log는 날려준다.
log: Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing
- 스프링 부트 -> 충돌이 안나는 케이스도 있고 너무 애매함. 다른 문서도 찾아봐서 확실히 할 필요가 있다.
- 추측
- 확실한 것은 수동x수동으로 빈이름이 중복날 가능성이 낮기 때문에 이부분들이 되게 애매하게 예외처리가 되고
- 중복 발생 가능성이 높은 자동x수동, 자동x자동의 경우는 무조건 예외가 터지도록 되어있다.
- 만약 잡기 어려운 bean 중복 에러가 발생 혹은 수동 bean 등록을 할 경우에는 더 조심해야하고 이부분에 대해선 tdd를 꼭 수행하도록 하자. 명확하지 않기 때문
- 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값이 설정 되었다.
- @SpringBootApplication 으로 동작할 때 에러 발생
- 만약 spring 처럼 overriding 하고 싶은 경우 설정 변경 방법
- 추측
# application.properties
# spring 처럼 자동, 수동 bean 동일 이름일 때 overriding 하기 위한 설정
spring.main.allow-bean-definition-overriding=true; # default가 false
이전 발행글 : 스프링 핵심 원리 이해 4 - 싱글톤 컨테이너
다음 발행글 : 스프링 핵심 원리 이해 6 - 의존관계 자동 주입
출처: 인프런 스프링 핵심 원리 - 기본편