컴포넌트 스캔과 자동주입

hyun·2024년 2월 21일

Spring

목록 보기
4/6

🎯컴포넌트 스캔

앞에서 포스팅 한 글에서는 스프링빈 등록과 의존성 주입을 아래와 같이 설정 정보에 수동으로 넣어주었다.
AppConfig.class

    @Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){
        System.out.println("AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService(){
        System.out.println("AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }


}

현재는 등록해야 할 스프링 빈이 몇개 안되지만 만약 등록해야 할 빈이 수 십개가 넘어가면 너무 귀찮아질 것이다. 이런 문제를 해결할 수 있게 해주는 것이 컴포넌트 스캔이다. 컴포넌트 스캔을 사용하기 위해서는 위의 AppConfig와 같은 설정 정보에 @ComponentScan을 붙여주면된다.

@Configuration
@ComponentScan
public class AutoAppConfig {

}

그런데 AppConfig와는 다르게 스프링빈 등록 정보가 하나도 없다. 그런데 어떻게 빈을 등록하고 의존성 주입을 하게 되는 것일까?

🫛 @Component

바로 아래와 같이 등록할 클래스에 @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;

        return 0;
    }
}

그러면 컴포넌트 스캔이 시작되면서 @Componenet가 붙은 클래스들을 찾아 스프링빈으로 등록해준다. 등록된 스프링빈의 기본 이름은 @Component가 붙은 클래스명(ex:RateDiscountPolicy)으로 하되 맨 앞글자만 소문자로 바꾼다(ex:rateDiscountPolicy). 만약 빈이름을 직접 지정하고 싶으면 @Component("원하는이름")이런식으로 이름을 부여할 수 있다.
그런데 만약 빈이름에 중복이 일어나면 어떻게 될까? 컴포넌트스캔으로 등록된 빈이름이 겹치는 경우 바로 ConflictingBeanDefinitionException 예외가 발생한다. 그리고 수동으로 등록한 빈이름과 컴포넌트 스캔으로 등록된 빈이름이 같다면 수동으로 입력한 빈이 우선권을 가지게 된다. 하지만 이런 애매한 상황은 만들지 않는 것이 좋다!

🔍컴포넌트 스캔 대상(아래의 어노테이션 안에는 @Component가 있음)

@Component : 컴포넌트 스캔에서 사용
@Controller : 스프링 MVC 컨트롤러에서 사용
@Service : 스프링 비즈니스 로직에서 사용
@Repository : 스프링 데이터 접근 계층에서 사용
@Configuration : 스프링 설정 정보에서 사용

그리고 위에서 설명하지 않았지만 @SpringBootApplication안에는 @ComponentScan이 들어있어 스프링 애플리케이션을 시작하면 컴포넌트 스캔이 자동으로 된다.

❓그럼 의존성 주입은?

정답은 @AutoWired이다.

@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;
    }

    @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);
    }

위 코드처럼 생성자에 @AutoWired를 추가해주면 파라미터에 해당하는 타입의 빈을 조회하여 의존성을 주입해준다. 더 놀라운 것은 만약 생성자가 하나밖에 없다면 @AutoWired를 생략해도 의존성 주입을 시켜준다. 생성자에 주입하는 방식 말고도 수정자 주입(setter), 필드 주입, 일반 메서드 주입이 있다. 하지만 생성자 주입을 사용하는 것이 가장 바람직한데 그 이유는 아래와 같다.

1. 불변

  • 대부분의 의존관계 주입은 애플리케이션 종료시점까지 변경할 일이 없다.
  • 수정자 주입을 사용하면, setter 메소드를 public으로 설정해야되는데 개발자들의 실수를 야기할 수 있다.
  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에는 호출할 일이 없다. 따라서 불변하게 설계할 수 있다.

2. 누락

  • 생성자 주입을 선택하면 주입 데이터를 누락했을 때 컴파일 오류가 발생하여 쉽게 누락된 부분을 찾을 수 있다.

3. final 키워드

  • 생성자 주입은 초기화 단계에 실행되므로 필드에 final 키워드를 사용할 수 있다. 따라서 혹시라도 필드에 생성되지 않는 오류를 컴파일 시점에서 막아준다.

❗조회된 스프링빈이 2개 이상

@AutoWired는 타입으로 조회한다. 예를들어 DiscountPolicy의 하위 타입인 FixDiscountPolicy, RateDiscountPolicy가 스프링빈으로 등록되어있다고 가정하자.

@AutoWired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

위 코드에서는 어떤 타입을 받아와야할까? 저 코드는 실행하면 NoUniqueBeanDefinitionException예외가 터진다. 그렇다고 하위타입으로 지정하면 DIP가 깨지게된다. 그럴 때는 다음과 같은 방법이 있다.

1. @AutoWired 필드명 매칭

@AutoWired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = rateDiscountPolicy;
    }

위 코드와 같이 DiscountPolicy ->rateDiscountPolicy로 변경하여 주입을 받고자 하는 빈의 이름으로 매칭시킬 수 있다.

2. @Qualifer

RateDiscountPolicy

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

OrderServiceImpl

@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
 @Qualifier("mainDiscountPolicy") DiscountPolicy
discountPolicy) {
 this.memberRepository = memberRepository;
 this.discountPolicy = discountPolicy;
}

위 코드와 같이 이름이 중복되는 스프링빈에 @Qualifier("원하는 이름")을 넣어주면된다. 스프링 빈이름이 바뀌는게 아니라 구별하기 위해서만 쓰이는 것이다.

3. @Primary
RateDiscountPolicy

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

위 코드처럼 @Primary를 추가하게 되면 해당 스프링빈이 주입된다. 가장 추천하는 방법이다.

0개의 댓글