컴포넌트 스캔과 의존관계 자동 주입

지금까지 스프링 빈을 등록할 때는 @Bean 애노테이션이나 XML파일의 설정을 통해 등록할 스프링 빈을 직접 정의했다. 등록해야 할 빈이 여러 개라면 설정 정보가 커지고 누락할 가능성이 높아진다. 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다. @ComponentScan 애노테이션을 이용하면 된다. 스프링이 @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다. 또한, 의존관계를 자동을 주입해 주는 @Autowired 애노테이션도 제공한다.

컴포넌트 스캔을 사용하려면 아래 코드와 같이 설정 정보에 @ComponentScan 애노테이션을 붙이면 된다. 기존의 AppConfig와는 @Bean으로 등록한 클래스가 하나도 없다. 그리고 스프링 빈으로 등록할 클래스에 @Component 애노테이션을 붙인다.

@Configuration
@ComponentScan
public class AutoAppConfig {

}
@Component
public class OrderServiceImpl implements OrderService {
    private final OrderRepository orderRepository;
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

@Component
public class MemoryOrderRepository implements OrderRepository { }

@Component
public class RateDiscountPolicy implements DiscountPolicy { }

기존의 AppConfig에서는 @Bean 애노테이션이 붙은 메서드를 통해 의존관계에 대한 정보를 알 수 있었다. 그러나 @ComponentScan을 사용하면 등록할 스프링 빈을 직접 명시하지 않기 때문에 의존관계에 대한 정보를 스프링은 알 수가 없다. 그래서 @Autowired 애노테이션을 통해 의존관계 주입을 한다. @Autowired를 사용하면 스프링 컨테이너가 등록된 빈들 중에서 필요한 빈을 찾아서 자동으로 주입해 준다.

 

의존관계 자동 주입의 동작 과정

앞서 언급했듯 @Autowired를 사용하면 스프링이 알아서 의존관계 주입을 실행한다. 이것을 의존관계 자동 주입이라고 하고 자세한 동작과정은 다음과 같다.

  1. 컴포넌트 스캔
    • 설정 정보에 @ComponentScan을 붙이고 스프링 빈으로 등록할 클래스에 @Component를 붙인다.
    • 스프링은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.
    • 빈 이름과 빈의 주소값의 쌍으로 저장된다.
    • 빈 이름의 기본값은 클래스명이되 맨 앞글자는 소문자이다. ex) orderServiceImpl, orderServiceImpl@x01
  2. 의존관계 자동 주입
    • 생성자에 @Autowired를 붙인다.
    • 스프링 컨테이너가 필요한 빈을 찾아서 자동으로 주입한다.
    • 조회 전략의 기본값은 타입이 같은 빈을 찾아서 주입한다.
    • 생성자의 매개변수 타입 중 하나가 discountPolicy이므로 getBean(discountPoliciy.class)를 통해 조회하는 것과 같은 맥락이다.

 

컴포넌트 스캔의 시작 위치

@ComponentScan 애노테이션을 사용할 때, 클래스를 스캔할 패키지의 시작 위치를 지정할 수 있다. @ComponentScan(basepackage = "패키지 경로")와 같이 설정하면 해당 패키지를 포함하여 모든 하위 패키지를 탐색한다. @ComponentScan(basepackage = "패키지 경로1, 패키지 경로2")와 같이 콤마를 사용해 여러 패키지를 지정할 수도 있다. basepackage 대신 basepackageClasses를 사용하거나 아예 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다. 즉, 기본값은 설정 정보 클래스가 있는 패키지이다.

이러한 설정이 존재하는 이유는 스프링이 모든 자바 클래스를 스캔하기에는 너무 비효율적이기 때문이다. 그래서 꼭 필요한 위치부터 탐색을 시작할 수 있도록 설정하는 것이다. 스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication을 프로젝트의 시작 위치에 두는 것이 관례다. 그리고 @SpringBootApplication 애노테이션 안에 @ComponentScan이 들어있다.

지금은 new AnnotationConfigApplicationContext(AutoAppConfig.class);를 통해 직접 스프링 컨테이너를 생성하고 사용하기 때문에 @ComponentSacn을 사용하는 것이지 스프링 부트를 활용하면 이 애노테이션을 사용할 필요가 없다. 앞서 말했듯이 @SpringBootApplication 안에 @ComponentScan이 들어있기 때문이다. 또한, @SpringBootApplication이 붙은 클래스의 main 메서드가 실행될 때 스프링 컨테이너가 생성된다.

 

컴포넌트 스캔의 기본 대상

컴포넌트 스캔은 @Component가 붙은 클래스뿐만 아니라 다음과 같은 애노테이션이 붙은 클래스도 탐색 대상에 포함된다. 또한, 컴포넌트 스캔 대상 이외의 부가 기능도 수행한다.

  • @Controller : 스프링 MVC 컨트롤러로 인식
  • @Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해 준다.
  • @Configuration : 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.
  • @Service : 컴포넌트 스캔 외에 다른 기능은 하지 않는다. 대신 이 애노테이션을 붙임으로써 해당 클래스가 비즈니스 로직을 수행한다는 것을 알려준다.

 

컴포넌트의 중복 등록과 충돌

컴포넌트 스캔에서 같은 이름의 빈이 중복으로 등록되는 경우는 다음 두 가지 상황이 있다.

자동 빈 등록 vs 자동 빈 등록

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록될 때, 그 이름이 같은 경우 ConflictingBeanDefinitionException 예외가 발생한다.

자동 빈 등록 vs 수동 빈 등록

다음과 같이 @Component를 통해 빈이 등록되고 설정 파일 내에서 @Bean을 통해 같은 이름의 빈을 수동을 등록한다면 수동으로 등록한 빈이 우선권을 가진다. 즉, 수동 빈이 자동 빈을 오버라이딩 한다.

@Configuration
@ComponentScan
public class AutoAppConfig {

    @Bean(name = "memoryOrderRepository")
    public OrderRepository orderRepository() {
        return new MemoryOrderRepository();
    }
}

@Component
public class MemoryOrderRepository implements OrderRepository { }

테스트를 해보면 로그 중 다음과 같은 문장을 볼 수 있다.

Overriding bean definition for bean 'memoryOrderRepository' with a different definition: replacing

물론 개발자가 오버라이딩을 의도했다면, 자동보다는 수동 빈 등록이 우선권을 갖는 것이 좋다. 하지만 현실은 개발자가 의도적으로 설정해서 이런 결과가 만들어지기보다는 여러 설정들이 꼬여서 이런 결과가 나타난다. 그래서 스프링 부트에서는 자동 빈 등록과 수동 빈 등록이 충돌하면 오류가 발생하도록 기본값을 설정해 두었다. 충돌 시 로그는 아래와 같다.

Description:

The bean 'memoryOrderRepository', defined in class path resource [수동 빈 클래스 경로], could not be registered. A bean with that name has already been defined in file [자동 빈 클래스 경로] and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

+ Recent posts