컴포넌트 스캔과 의존관계 자동 주입 시작하기
기존까지 스프링 빈을 등록할때 우리는 Annoation기반의 @Bean Annotation이나 XML기반의 <bean>을 사용했다.
예제에서는 몇개 되지 않은 숫자라 편리하게 등록할 수 있겠으나, 예제가 복잡해지거나 실무에 들어가면 숫자가 상당히 많아진다.
그래서 스프링 빈은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 ComponentScan이라는 기능을 제공한다.
또 의존관계를 자동으로 주입하는 Component Scan이라는 기능도 제공한다.
AutoAppConfig.java를 생성하고 이 의존관계 자동주입과 ComponentScan에 대해 알아보도록 하자
package hello;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
@Configuration // 내부 들어가보면 @Component 있음
@ComponentScan(
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
) // AppConfig.class를 끌어올리면, 충돌나서 제외한 것임 (AppConfig.class는 수동으로 등록한것이고 여기서는 자동으로 사용할것이기 때문)
public class AutoAppConfig {
}
@Component Annotation의 경우 Spring Bean을 자동으로 끌어올려야한다.
컴포넌트 스캔의 대상으로 지정하려면 먼저 @ComponentScan을 설정 정보에 붙여주자!
기존의 AppConfig와는 다르게 @Bean으로 등록한 클래스가 하나도 없는 모습을 볼 수 있다.
excludeFilters의 경우 AutoAppConfig에 @Configuration Annotation을 붙이게 되면, @Configuration이 붙은 설정정보인 AppConfig도 같이 등록되어버리기 때문에 충돌이 발생한다. 따라서 excludeFilters를 통해 설정정보는 컴포넌트 스캔 대상에서 제외했다
includeFilters를 주면 컴포넌트 스캔의 대상을 추가로 지정한다.
@ComponentScan은 이름 그대로 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.
@Component를 붙여주면 된다.
package hello.springintroduction.Member;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
@Component
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
// 동시성 이슈에 의해 ConcurrentHashMap 사용
// ConcurrentHashMap에 대한 설명은 생략한다.
@Override
public void save(Member member) {
store.put(member.getId(), member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);
}
}
package hello.springintroduction.discount;
import hello.springintroduction.Member.Grade;
import hello.springintroduction.Member.Member;
import org.springframework.stereotype.Component;
@Component
public class RateDiscountPolicy implements DiscountPolicy{
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
}
else{
return 0;
}
}
}
package hello.springintroduction.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MemberServiceImpl implements MemberService{
// 다형성에 의해 MemberRepository로 MemoryMemberRepository 가져옴.
private MemberRepository memberRepository;
@Autowired // ac.getBean(MemberRepository.class)로 넣어주는 것과 비슷
// 의존관계를 자동주입함 (@Component, @AutoAppConfig 생성에 따름)
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);
}
// 클래스 다이어그램 - 정적
// 객체 다이어그램 - 동적
}
이전의 AppConfig에서는 다음과 같은 코드로 @Bean을 이용하여 설정 정보를 직접 작성함과 동시에, 의존관계도 동시에 명시했지만
이제는 이런 설정정보 자체가 없기 때문에 의존관계 주입 또한 이 클래스들에서 직접 명시해주어야 한다.
@Autowired는 이러한 의존관계 주입을 자동으로 시행한다. 자세한 룰의 경우 후술한다.
package hello.springintroduction.order;
import hello.springintroduction.Member.Member;
import hello.springintroduction.Member.MemberRepository;
import hello.springintroduction.Member.MemoryMemberRepository;
import hello.springintroduction.discount.DiscountPolicy;
import hello.springintroduction.discount.FixDiscountPolicy;
import hello.springintroduction.discount.RateDiscountPolicy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy; // Interface에만 의존하게 됨.
// 이 상태로만 두면 NPE -> 구체 클래스 어떻게 주입?
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
this.memberRepository=memberRepository;
this.discountPolicy=discountPolicy;
}
// DiscountPolicy만 의존하는게 아니라 FixDiscountPolicy도 의존하고 있었다!!
// DIP 위반임. 결론적으로 정책 변경으로 인해, OrderServiceImpl의 코드 또한 변경해야함.
// DiscountPolicy 확장해서 변경하면 클라이언트 코드에 영향을 주므로 OCP 위반
@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);
}
}
package hello.springintroduction.scan;
import hello.AutoAppConfig;
import hello.springintroduction.Member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class AutoAppConfigTest {
@Test
void basicScan(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(
AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}
}
AnnotationConfigApplicationContext의 경우 동일하게 사용하나 인자설정정보로 AutoAppConfig.class를 넘긴다.
Autowired와 ComponentScan의 동작방식
스프링이 실제 동작하게 되면 제일 먼저 자바 내에 있는 모든 @Component가 붙은 클래스들 이잡듯 뒤져서 끌어온다
그러나 빈 이름에 주의해야 할 필요가 있다 기본값은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.
빈 이름을 @Component 애노테이션의 name 인자로 전달할 수도 있으나 권장하진 않는다. (애매한건 하지마라)
@Autowired를 이용한 의존관계 자동주입
- 생성자에 @Autowired를 지정하면 스프링 빈이 잦동으로 해당 스프링 빈을 찾아서 주입한다.
- 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다. (디폴트)
.getBean(MemberRepository.class)와 동일하다고 이해하면 된다, (여전히 이름이 같은 빈인 경우 충돌된다)
생성자에 파라미터가 많더라도 자동으로 주입이 진행된다.
컴포넌트 스캔 탐색 위치와 기본 스캔 대상
기본적으로 시작 위치를 basePackages를 통해 지정할 수 있는데,
그냥 설정 정보는 프로젝트 최상단 위치에 두자 스프링은 기본값으로 최상단부터 탐색한다.
컴포넌트 스캔 기본 대상
@Component: 컴포넌트 스캔에서 사용 // 기본적으로 아래 것들에 다 포함되어 있음
@Controller: 스프링 MVC 컨트롤러에서 사용
@Service: 스프링 비즈니스 로직에서 사용
@Repository: 스프링 데이터 접근 계층에서 사용
@Configuration: 스프링 설정정보에서 사용
1) 컴포넌트 스캔 대상에 추가할 애노테이션
package hello.springintroduction.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) // TYPE -> 모든 클래스에 붙이겠다.
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
package hello.springintroduction.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 {
}
다음 두 코드는 1) 컴포넌트 스캔 대상에 추가할 애노테이션 2) 컴포넌트 스캔 대상에서 제외할 애노테이션을 추가한것이다.
@MyIncludeComponent
public class BeanA {
}
@MyExcludeComponent
public class BeanB {
}
다음과 같이 클래스에 두 Annotation을 추가해줌으로써,
BeanA를 등록하겠다, BeanB는 등록제외대상이라는 사실을 스프링에 알려줄 수 있다.
정확히는 이 클래스는 등록하고 이 클래스를 등록하지 말라는 내용은 컴포넌트 스캔의 includeFilters, excludeFilters에서 명시한다.
package hello.springintroduction.scan.filter;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
public class ComponentFilterAppConfigTest {
@Test
void filterScan(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(
ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
assertThat(beanA).isNotNull();
//BeanB beanB = 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{
}
}
중복 등록과 충돌
1) 자동 VS 자동 -> ConflictingBeanDefinitionException 발생!!
2) 자동 VS 수동 -> 수동 빈 등록이 우선권을 가진다 (누누히 말하지만 애매하면 하지 마라, 명확하게 하는게 중요!)
최근 들어서는 스프링 부트가 자동 수동이 충돌시 에러를 발생하도록 기본값을 변경함.
그냥 모호한 상황을 만들지 말자, 뭐든 명확하게 하는게 중요하다!
'스프링 공부 (인프런 김영한 선생님) > 스프링 핵심원리' 카테고리의 다른 글
[스프링 핵심원리] 9. @Autowired 필드명, @Qualifier, @Primary (0) | 2023.05.15 |
---|---|
[스프링 핵심원리] 8. 의존관계 자동 주입 (0) | 2023.05.15 |
[스프링 핵심원리] 6. 싱글톤 컨테이너 (0) | 2023.05.10 |
[스프링 핵심원리] 5. 스프링 빈 조회 상속관계 BeanFactory와 ApplicationContext (0) | 2023.05.10 |
[스프링 핵심원리] 4. 스프링 전환 / 스프링 컨테이너 기본 (1) | 2023.05.10 |