6.1 컴포넌트 스캔과 의존관계 자동 주입 시작하기
@ComponentScan
- 지금까지 배운 내용과 다르게 빈이 많아진다면 하나씩 등록하기 귀찮고 누락하는 문제가 생긴다.
- 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔 기능을 제공한다.
- @Autowired: 의존관계를 자동으로 주입한다. 생성자에 붙이면 매개변수에 맞는 것을 찾아와서 의존관계를 주입
- @ComponentScan은 스프링 빈을 자동으로 끌어 올려준다
- @ComponentScan을 사용하면 @Configuration이 붙은 설정 정보도 자동으로 등록되므로 AppConfig, TestConfig 등 앞서 만들어두었던 설정 정보도 함께 등록되고 실행되어 버린다.
- @Configuration 소스코드를 열어보면 @Compomemt가 붙어있기 때문에 @Configuration은 컴포넌트스캔의 대상이 된다.
- @ComponentScan을 붙이면 @Component이 붙은 모든 클래스를 찾아서 스프링 빈으로 등록해준다.
Ex)
<hide/>
package hello.core;
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) // 제외할 것을 지정해준다.
)
public class AutoAppConfig {
}
- excludeFilters는 제외할 사항을 지정해준다. (실무에서는 굳이 제외하지 않는다.)
-> Configuration.class를 왜 뺄까?
-> AppConfig는 수동 등록 중이니까 자동으로 등록되면 안된다.
-> AppConfig에 붙은 인터페이스 @Configuration에는 이미 @Component가 붙어 있다. 따라서 자동으로 컴포넌트 스캔의 대상이 된다.
Note)
- MemoryMemberRepo, RateDiscountPolicy, MemberServiceImpl 에 @Component 붙인다. (스캔 대상되고 빈으로 등록)
- @Autowired를 생성자에 붙이면 스프링이 생성자의 매개변수의 타입에 맞는 것을 가져와서 자동으로 연결해서 의존관계를 주입해준다. (@Autowired 쓰면 생성자에 여러 의존관계를 한번에 주입 가능)
- @ComponentScan을 쓰면 매핑이 자동으로 되는데 의존관계 설정 방법이 없으니 자연스럽게 @Autowired를 쓰게 된다.
- 이전에 AppConfig에서는 @Bean 으로 직접 설정 정보를 작성했고 의존 관계도 직접 명시했다. 이제는 이런 설정 정보 자체가 없기 때문에 의존관계 주입도 이 클래스 안에서 해결해야한다.
Ex) AutoAppConfigTest
<hide/>
package hello.core.scan;
import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class AutoAppConfigTest {
@Test
void basicScan(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
- 테스트 클래스를 만든다.
Note) 실행 결과 - 성공
- AnnotationConfigApplicationContext를 사용하는 것은 동일한다.
- 설정 정보로 AutoAppConfig 클래스를 넘겨준다.
- "ClassPathBeanDefinitionScanner": 스캔할 클래스 후보에 대해 보여준다.
- 싱글톤을 여러 개 생성했다고 나온다.
- 주입에 관한 내용도 나온다.
(1) @ComponentScan
- @ComponentScan은 @Component가 붙은 모든 클래스를 스프링 컨테이너에 스프링 빈으로 등록한다.(싱글톤)
- 빈 이름 기본 전략: 이 때 스프링 빈의 기본 이름은 클래스 명을 사용하되 맨 앞 글자는 소문자로 (카멜 표기법)
- @Component() 의 괄호 안에 빈 이름을 직접 지정할 수도 있다.
(2) @AutoWired 의존 관계 자동 주입
- Impl의 생성자에 @AutoWired를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈(MemberRepository)의 타입을 찾아서 주입한다.
- 여기서 같은 타입이 여러 개인 경우, 충돌한다. 뒤에서 살펴볼 예정
- 기본 조회 전략에 따라 타입이 같은 빈을 찾아서 주입한다.
- "getBean(MemberRepository.class)"와 동일하다.
- 생성자에 매개변수가 많아도 다 찾아서 자동으로 주입한다.
6.2 탐색 위치와 기본 스캔 대상
탐색할 패키지의 시작 위치 지정
- 꼭 필요한 위치부터 스캔하도록 시작 위치를 지정할 수 있다. @ComponentScan(basePackages= 패키지명)
- basePackages: 탐색할 패키지의 시작위치를 지정한다. 지정한 패키지부터 하위 패키지를 모두 탐색한다.
- 지정하지 않는 경우, @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
- 권장하는 방법: 패키지 위치를 지정하지 않고 설정 정보의 클래스 위치를 프로젝트 최상단에 두는 것이다.
- ex) 프로젝트가 시작하는 com.hello.service, com.hello.repository 가 있는 경우 com.hello에 메인 설정 정보와 관련된 클래스 AppConfig을 위치시키고 @ComponentScan 애너테이션을 붙이고 basePackage는 생략
ComponentScan 기본 대상
- 컴포넌트 스캔은 @Component 뿐만 아니라 다음 내용도 추가로 대상에 포함한다. (아래의 인터페이스에 모두 @Component가 붙어있다.)
- @Component: 컴포넌트 스캔에 사용한다.
- @Controller: 스프링 MVC 컨트롤러로 인식한다.
- @Service: 스프링 비즈니스 로직이 여기에 있을거라고 예상할 수 있다. 특별한 처리는 없음.
- @Repository: 스프링 데이터 접근 계층으로 인식한다. 데이터 계층 예외를 스프링 예외로 변환해준다. ex) JPA, Jdbc
- @Configuration: 스프링 설정 정보로 인식하고 스프링 빈이 싱글톤을 유지하도록 추가 처리한다.
- cf) 애너테이션은 상속 개념이 없다. 어떤 애너테이션이 특정 애너테이션에 대한 포함 여부를 인식할 수 있는 것은 자바가 아니라 스프링이 지원하는 기능이다.
컴포넌트 스캔 용도 뿐만 아니라 다음 애너테이션이 있으면 스프링은 부가 기능을 수행한다.
- @Controller: 스프링 MVC 컨트롤러로 인식한다.
- @Repository: 스프링 데이터 접근 계층으로 인식하고 데이터 계층의 예외를 스프링의 추상화된 예외로 바꿔준다.
- @Configuration: 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.
- @Service: 핵심 비즈니스 계층을 인식하는데 도움이 된다. 중요한 서비스 로직이 있다.
6.3 필터(filter)
필터
- includeFilters: 컴포넌트 스캔 대상을 추가로 지정한다.
- excludeFilters: 컴포넌트 스캔에서 제외할 대상을 지정한다.
Ex) 필터
- @MyIncludeComponent애너테이션을 만든다. 애너테이션이 붙은 것은 컴포넌트 스캔 대상에 추가하겠다는 의미
- @MyExcludeComponent애너테이션이 붙으면 컴포넌트 스캔에서 제외할 것이다.
<hide/>
package hello.core.scan.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE) // TYPE은 클래스 레벨에 붙인다는 뜻
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}
<hide/>
package hello.core.scan.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
<hide/>
package hello.core.scan.filter;
@MyIncludeComponent
public class BeanA {
}
<hide/>
package hello.core.scan.filter;
@MyExcludeComponent
public class BeanB {
}
<hide/>
@Test
void filterScan(){
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
assertThat(beanA).isNotNull();
// ac.getBean("beanB", BeanB.class); // 없으니까 예외 발생한다.
assertThrows(
NoSuchBeanDefinitionException.class,
() -> ac.getBean("beanB", BeanB.class));
}
@Configuration
@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentFilterAppConfig{
}
Note) 실행 결과 - 성공
- 빈 A는 컨테이너에 등록되고 B는 등록되지 않는다.
- 참고로 요즘에는 @Component로 충분하기 때문에 includeFilters를 사용할 일이 없다.
- excludeFilters는 가끔 사용하지면 많지는 않다.
- 스프링 부트는 컴포넌트 스캔을 기본으로 제공하니 기본 설정에 맞추어 사용한다.
FilterType옵션
- ANNOTATION: 기본값, 애너테이션을 인식해서 동작한다.
- ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
- ASPECT: AspectJ 패턴 사용, 패턴으로 찾아온다.
- REGEX: 정규 표현식
- CUSTOM: 'TypeFilter'이라는 인터페이스를 구현해서 정리한다.
6.4 중복 등록과 충돌
중복 등록과 충돌
- 자동 빈 등록 vs 자동 빈 등록 => ConflictingBeanDefinitionException 예외 발생
- 수동 빈 등록 vs 자동 빈 등록 => 수동 빈
- 수동 빈이 자동 빈을 오버라이드해버린다.
- 최근 스프링 부트에서는 수동 빈과 자동 빈이 충돌하면 오류가 발생하도록 기본 값을 바꾸었다.
Ex)
- OrderServiceImpl클래스, MemberServiceImpl 클래스 에 다음과 같이 컴포넌트 애너테이션을 추가한다.
- 충돌을 발생시키기위해서 둘다 똑같은 이름 "service"를 준다
<hide/>
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import org.springframework.stereotype.Component;
// 추가
@Component("service")
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository; // = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy; // = new RateDiscountPolicy();
// 추가
public MemberRepository getMemberRepository(){
return memberRepository;
}
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId); // 회원정보를 조회한다.
int discountPrice = discountPolicy.discount(member, itemPrice); // 최종적으로 할인된 금액
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
<hide/>
package hello.core.member;
import org.springframework.stereotype.Component;
@Component("service")
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
public MemberRepository getMemberRepository(){
return memberRepository;
}
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
- 전에 만들어둔 basicScan ()을 돌린다.
<hide/>
package hello.core.scan;
import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class AutoAppConfigTest {
@Test
void basicScan(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
Note) 실행 결과 - BeanDefinitionStoreException, ConflictingBeanDefinitionException
- 이미 service라는 빈이 있다고 나온다.
- Component() 안에 넣어준 이름을 지우면 테스트를 통과한다.
Ex) 수동 빈 등록 vs 자동 빈 등록
- 빈 네임이 memoryMemberRepository인 이유는? 원래 클래스 MemoryMemberRepository가 맨 앞 글자만 소문자로 바뀌어서 컴포넌트로 등록되기 때문이다.
<hide/>
package hello.core;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
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) // 제외할 것을 지정해준다.
)
public class AutoAppConfig {
@Bean(name = "memoryMemberRepository")
MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
Note) 실행 결과
============================= 오류 =============================
- 강사님 화면에는 오버라이드 관련된 설명이 나오는데 내꺼에는 안 나온다.
Ex) 스프링 부트를 실행해보자
<hide/>
package hello.core;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CoreApplication {
public static void main(String[] args) {
SpringApplication.run(CoreApplication.class, args);
}
}
Note) 실행 결과
- 이미 정의가 등록되어 있다.
- 오바라이딩에 disabled 되어 있다.
- 위 실행 화면에서 권해주는 것에 맞춰서
-> application.properties에 spring.main.allow-bean-definition-overriding=true를 추가한다.
- 그 다음, coreApplication을 돌리면?
'Spring Framework > [인프런] Spring 핵심 원리' 카테고리의 다른 글
Chapter 08. 빈 생명주기(Bean Life Cycle) 콜백 (0) | 2022.08.16 |
---|---|
Chapter 07. 의존 관계 자동 주입 (0) | 2022.08.15 |
Chapter 05. 싱글톤 컨테이너 (0) | 2022.08.14 |
Chapter 04. 스프링 컨테이너와 스프링 빈 (0) | 2022.08.12 |
Chapter 03. 스프링 핵심 원리 이해2 - 객체 지향 원리 적용 (0) | 2022.08.12 |