개발자로 후회없는 삶 살기
spring PART.컴포넌트 스캔 본문
서론
※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.
https://www.inflearn.com/roadmaps/373
본론
- 컴포넌트 스캔과 의존관계 자동 주입
지금까지 설정 정보에 @를 붙여서 설정을 나열했다. 예제에서는 4개만 등록했으니 괜찮은데 실무에서는 너무 많아서 누락하기 쉽다. 그래서 스프링이 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다. 또 의존관계 자동 주입이라는 @Autowired도 제공한다. 이것들을 사용하면 굉장히 많이 편리해진다.
-> 사용해보기
@Configuration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class);
)
public class AutoAppConfig {
}
기존의 appconf 놔두고 새로 하나 만들자 Auto App Conf이다. 컴포넌트 스캔이란게 뭐냐면 스프링 빈을 쫙 긁어서 자동으로 스프링 빈에 등록을 해야한다. @ComponentScan을 하고 한가지 더로 configuration 어노가 붙은 애는 긁는 거에서 뺀다. 이 어노가 붙은게 원래 Appcof로 얘를뺀다.
> 이러면 완성인데 기존 appcof와 다르게 @Bean이 붙은 메서드가 하나도 없다. 즉 여기에 있지 않은 다른 것들을 다 긁는 다는 것이고 appcof도 스프링 빈에 등록이 되니 그 밑에 @Bean 붙은 애들도 다 등록이 될 것이다. 그래서 일단 다 등록되는 것을 막기 위해 필터링한다.( 보통은 제외하지 않고 지금은 교육용으로 기존 예제 코드를 사용하지 않기 위해 필터링하는 것이다. )/
> Component 스캔 어노테이션은 이름 그대로 @Component 어노가 붙은 클래스를 스캔해서 스프링 빈으로 자동 등록해준다.(Configuration이 붙은 애들도 Component가 자동으로 붙기에 Appconf를 빼준 것이다.)
-> @ 붙이기
메모리 멤버 구현체에 붙인다. 구현체들에 다 붙인다. fix와 rate 중에 쓸 것에만 붙인다.(이게 컴포넌트 스캔 쓰기 전에 역할과 구현을 분리하고 new로 구현을 선택해서 끼워 넣은 것과 같은 선택하는 것이다.) 주문, rate, 멤버 서비스, 멤버 레포에 다 @Compo를 붙인다.
> 근데 한가지를 더해줘야한다. 이렇게 하면 내가 스프링 빈을 등록하는게 아니고 자동으로 @Component만 보고 스프링 빈에 등록이 되는 것이다. 그러면 의존관계 주입을 어떻게 할까?
@Component
public class MemberServiceImpl implements MemberService{
MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
기존에는 AppConfig에서 멤버 서비스는 멤버 레포를 의존할 거라고 의존관계 주입을 할 수 있었다. 근데 AutoAppCOnf를 보면 내부 코드가 아무것도 없고 그냥 @Component가 붙은 애들이 스프링 빈으로 등록이 되어 버리는 것이다. 의존 관계 주입을 할 방법이 필요하고 자동 의존 관계 주입이 필요하다.
Autowired라는 기능을 생성자에 붙여주면 멤버 레포 타입에 맞는 애를(지금 메모리 멤버 레포) 찾아와서 의존 관계 주입을 자동으로 연결해 주입해준다. 따라서 @ComponentScan을 쓰면 무조건 AutoWired를 쓰게 된다. 왜냐하면 내 Bean이 자동으로 등록되니 의존관계를 설정할 방법이 없기 때문이다.
-> 정리해보면 Component라고 붙이면 스프링 빈에 등록이 되고 Autowired를 붙이면 스프링이 Component가 붙은 애를 생성할 때 자동 주입을 해준다.
-> 테스트
public class AutoAppConfigTest {
@Test
void basicScan() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService bean = applicationContext.getBean(MemberService.class);
Assertions.assertThat(bean).isInstanceOf(MemberService.class);
}
}
얘도 컨테이너에 등록된 객체들을 불러오는 게 똑같은 것이므로 new 어노테이션 어플리케이션 컨텍스트를 하는건데 차이가 컴포넌트 스캔하여 등록된 객체를 가져올 것이니 AutoAppConfig.class를 지정한다.
★ 원래는 지정한 class에 등록할 클래스가 메서드로 다 나와있어야하는데 하나도 없고 ComponentScan으로 실제 구현 클래스에 Component 어노가 붙은 애들을 스프링 빈에 등록한다. 사용법은 똑같이 getBean으로 멤버 서비스 조회해보면 isInstanceOf로 성공한다.
-> 로그 파악
로그를 보면 컴포넌트 스캔이 잘 된걸 확인할 수 있다. "뭔가 식별했다!" "그게 바로 이거다!" 라고 말해준다.
싱글톤 정보도 나오고 Autowired 정보도 나온다. orderServiceImpl를 만들때 메모리 멤버와 rate가 쓰였다는 것이다.
-> 그림 설명
@컴포넌트 스캔이 되어있으면 스프링 컨테이너가 싹다 뒤져서 @Component가 붙은 애들을 자동으로 등록을 한다. 싱글톤으로 등록이 되어서 하나만 생기고 이미 있으면 만들었던 것을 사용한다.
> 빈 이름은 클래스 명에 맨 앞만 소문자로 memberServiceImpl로 정해진다.
> Autowired는 멤버 서비스impl의 생성자에 적어놨으니 생성이 될 때 스프링이 스프링 컨테이너에 있는 멤버 레포지토리를 뒤진다.(왜 있지? 멤버 레포도 @component가 붙어서 스프링 빈에 등록이 되기 때문이다. 뒤져서 AutoWired 생성자에 있는 타입으로 조회를 해서 멤버 레포와 같은 타입의 메모리 멤버 레포를 찾아서 주입을 해준다.(역시 같은 타입이 여러개면 충돌이 난다.)
> 기본 주입 전략은 타입이 같은 빈을 주입하고 타입이 안 맞으면 더 복잡한 메커니즘으로 한다.
- 스캔 탐색 위치와 기본 스캔 대상
탐색할 위치를 basePackage로 hello.core.member로 지정하면 여기서부터 찾아서 탐색하게 된다. 이렇게 하면 멤버 패키지만 컴포넌트 스캔의 대상이 된다. basicScan 테스트를 돌려보면 member 밑에 있는 객체만 등록이 되고 나머지는 등록이 안됐다.
-> 지정하는 이유
이게 없으면 모든 자바 코드를 다 뒤져서 엄청 오래걸린다. 또는 선택적으로 스캔할 때 쓰인다./
-> defualt로 지정 안하면
componentscan이 붙은 패키지부터 시작해서 하위를 다 뒤진다. 여기서는 AutoAppConfig가 hello.core에 있으니 다 뒤진다.
※ 권장하는 방법
김영한님이 하는 방법은 패키지 위치를 지정하지 않고 설정 정보 클래스(Appconfig)를 프로젝트 최상단에 두는 것이다.
ex) 프로젝트가 저런 구조로 되어있으면 com.hello가 프로젝트 루트이므로 여기에 Appconfig같은 설정 정보 클래스를 두고 @ComponentScan을 붙이고 basePackege를 생략한다. 사실 스프링 부트를 쓰면 @ComponentScan을 할 필요가 없다. SpringBootApplication에 @ComponentScan이 붙어있다.
- 컴포넌트 스캔 기본 대상
컴포넌트 스캔은 @Component 뿐만 아니라 다음과 같은 내용도 추가로 대상에 포함한다. controller, service, repository, configuration으로 MVC에서 쓰는 어노테이션으로 얘들을 붙여놓으면 자동으로 컴포넌트 스캔의 대상이 된다.(맨 처음 예제에서 서비스는 공유해야하니 DI를 했다. 그게 @service를 붙여서 컨테이너에 등록을 한 거였다.)
> 그 이유는 이 어노테이션의 인터페이스에 @component가 붙어있기 때문이다. 근데 이건 자바가 해주는게 아니고 스프링이 해주는 기능이다.
- 필터
컴포넌트 스캔에 대상을 추가할, 제외할 수 있다. filter 패키지를 만들고 @을 만든다. 그러면 @interface가 생긴다. @Component에 있는 Target 등등의 어노테이션을 위에 붙인다. 지금 내가 내 커스텀 어노테이션을 만든 거고 얘가 붙은 애는 컴포넌트 스캔에 추가된다.
또 하나를 Exclude로 만든다. 얘가 붙은 애는 컴포넌트 스캔에 제외한다. 빈을 만들어서 A는 스캔에 추가하고 B는 제외하도록 해보자
@Configuration
@ComponentScan(
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentScanAppConfig {
}
> 이제 테스트를 만든다. 컴포넌트 스캔으로 스프링 빈에 등록할 때 추가하고 제외되는지 보는 테스트 일것이므로 컴포넌트 스캔이 붙은 Config를 만들어야할 것이고 아까 만든 빈에 Component를 붙여야 할 것이다.(아까 빈에 어노테이션을 붙여놨으니 안 붙여도 된다.) filter로 MyIncludeComponent가 붙은 애는 추가한다고 하고 제외할 거 어노테이션을 제외한다.
@Test
void filterScan() {
ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentScanAppConfig.class);
BeanA beanA = ac.getBean(BeanA.class);
Assertions.assertThat(beanA).isNotNull();
assertThrows(NoSuchBeanDefinitionException.class,
() -> ac.getBean(BeanB.class));
}
> 이제 마무리로 컨테이너에 config를 지정하고 빈을 꺼내본다. 그러면 추가한 빈 A는 NotNull이어야하고 B는 Null이어야한다. B는 조회한 순간 exception이 터질 것이라서 assertThrows는 한다.
-> 정리
이렇게 컴포넌트 스캔 대상에서 추가하고 제외해봤다. 필터링을 할 때 FilterType.을 Annotation으로 줘서 했는데 알아보자
- FilterType 옵션
@Configuration
@ComponentScan(
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = BeanA.class)
}
)
static class ComponentScanAppConfig {
}
ANNOTATION : 어노테이션을 인식해서 동작
ASSIGNABLE_TYPE : 지정한 타입과 자식 타입을 인식해서 동작한다. 클래스를 직접 지정할 수 있다.
등등 5가지 옵션이 있다.
- 중복 등록과 충돌
컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까? 두가지 상황이 있다.
1. 자동 빈 등록 한 것이 두 개 있는 경우
2. 수동 빈 등록 vs 자동 빈 등록한 경우로 2번의 경우가 많이 생긴다. 수동 빈 등록해놨는데 자동으로 한 것과 잘못해서 겹치는 경우이다.
-> 자동 빈 등록 vs 자동 빈 등록
예외가 발생한다. 멤버 서비스 impl과 주문 서비스 impl을 같은 이름으로 넣으면 컴포넌트 스캔 과정에서 충돌이 난다. 근데 이 경우는 별로 없음
-> 수동 빈 등록 vs 자동
@Configuration
@ComponentScan(
basePackages = "hello.core.member",
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
@Bean(name = "memoryMemberRepository")
MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
수동으로 빈을 등록하고 자동과 충돌나게 하는 방법은 자동으로 @Component로 등록한 메모리 멤버 레포와 같은 이름으로 빈 공간이었던 AutoAppConfig에 AppConfig에 수동으로 등록했던 것처럼 등록한다.
실행해보면 실행이 된다. 이 경우 수동 빈이 우선권을 가진다. 수동 빈이 자동 빈을 오버라이딩해준다. 스프링이 정말 친절하게 로그를 남겨준다. 근데 개발자가 의도적으로 수동을 우선하게 하면 좋은 거다. 하지만 현실은 개발자 의도로 이런 결과가 만들어지기 보다는 여러 설정이 꼬여서 이런 결과가 만들어지는 경우가 대부분이다. 여러명이서 개발하다가 빈이 이름이 같아 지는 경우가 있다!
> 그러면 정말 잡기 어려운 버그가 된다. 그래서 최근 스프링 부트는 수동 빈 등록과 자동 빈 등록이 충돌이 나면 오류로 꺼지게 만들었다. CoreApplication을 돌려보면 error가 뜨면서 이런 메세지를 남긴다.
기본 값은 error가 나게 했는데 오버라이딩을 true로 하고 싶으면 properties에 true로 하도록 만들어줬다. 이걸 스프링 부트가 결정한 것이다. 개발은 천재 한명이 아니고 여러명이 개발을 하니 아예 애매한 상황을 막아 버린 것이다!
'[백엔드] > [spring | 학습기록]' 카테고리의 다른 글
spring PART.빈 스코프 (0) | 2023.04.02 |
---|---|
spring PART.의존관계 자동 주입 (0) | 2023.03.31 |
spring PART.스프링 컨테이너 계층, 싱글톤 컨테이너 (0) | 2023.03.29 |
spring PART.중간점검 1 (0) | 2023.03.25 |
spring PART.순수 자바코드를 스프링으로 전환 (0) | 2023.03.25 |