포스트

스프링 핵심 원리 (8) - 컴포넌트 스캔과 의존관계 자동 주입

스프링 핵심 원리 (8) - 컴포넌트 스캔과 의존관계 자동 주입

1. 개요

지금까지는 @Bean이 붙은 메서드를 하나하나 작성해서 스프링 빈을 등록했다. 등록할 빈이 몇 개 안 될 때는 문제가 없지만, 빈이 수십~수백 개로 늘어나면 이야기가 다르다. 설정 클래스가 끝없이 길어지고, 등록을 깜빡하는 실수도 생긴다. 스프링은 이 문제를 설정 정보 없이도 빈을 자동으로 찾아 등록하는 컴포넌트 스캔과, 의존관계를 자동으로 연결해주는 @Autowired로 해결한다.


2. 컴포넌트 스캔용 설정 클래스 만들기

기존 AppConfig는 그대로 남겨두고, 컴포넌트 스캔을 사용하는 새 설정 클래스를 하나 추가하는 방식으로 실습해보자.

1
2
3
4
5
@Configuration
@ComponentScan(
        excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}

이전 설정 클래스들과 다르게 @Bean 메서드가 하나도 없고, @ComponentScan 애노테이션만 붙어 있다. 그런데 여기에 excludeFilters가 붙어 있는 이유가 있다. 컴포넌트 스캔은 @Configuration이 붙은 클래스도 스캔 대상으로 삼기 때문에, 아무 필터 없이 스캔하면 이미 만들어둔 AppConfig 같은 기존 설정 클래스까지 함께 빈으로 등록되어 버린다. 예제 코드를 유지한 채로 이 문제를 피하려고 설정 정보 클래스를 스캔 대상에서 빼둔 것이다.


3. @Component와 @Autowired로 빈 등록·주입하기

컴포넌트 스캔은 이름 그대로 @Component가 붙은 클래스를 찾아 스프링 빈으로 등록한다.

1
2
3
4
5
6
7
@Component
public class MemoryMemberRepository implements MemberRepository {
}

@Component
public class RateDiscountPolicy implements DiscountPolicy {
}

AppConfig에서는 @Bean 메서드 안에서 new MemberServiceImpl(memberRepository())처럼 의존관계를 코드로 직접 명시했다. 컴포넌트 스캔 방식에는 그런 설정 코드가 없으므로, 의존관계 연결은 대상 클래스 내부에서 @Autowired로 해결한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

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

생성자에 @Autowired를 붙이면 스프링 컨테이너가 파라미터 타입과 일치하는 빈을 찾아서 자동으로 넣어준다. OrderServiceImpl처럼 파라미터가 여러 개여도 각각의 타입에 맞는 빈을 모두 찾아서 주입한다. 기본 조회 전략은 ac.getBean(타입.class)와 같다고 생각하면 된다.

여기서 기준이 되는 타입은 파라미터에 선언된 타입이다. 위 코드에서는 파라미터를 구현체인 MemoryMemberRepository가 아니라 인터페이스 MemberRepository로 선언했기 때문에, 컨테이너도 MemberRepository 타입을 기준으로 빈을 찾는다. 즉 ac.getBean(MemberRepository.class)와 같은 방식으로, 등록된 빈 중 이 타입에 할당 가능한(assignable) 빈을 후보로 삼는 것이다. MemoryMemberRepositoryMemberRepository를 구현하고 있으므로 매칭되는 것뿐이지, 파라미터 타입이 알아서 인터페이스로 바뀌는 것은 아니다. 만약 파라미터를 구현체 타입으로 선언했다면 그 구현체(와 하위 타입)만 후보가 되므로, 나중에 구현체를 교체하기 어려워진다. 그래서 가급적 인터페이스 타입으로 선언하는 것이 권장된다.

참고: 같은 타입의 빈이 두 개 이상 등록되어 있으면 타입만으로는 어떤 빈을 주입할지 컨테이너가 판단할 수 없어 예외가 발생한다. 이 경우 @Qualifier나 빈 이름 등으로 후보를 좁혀야 한다.


4. 동작 확인

1
2
3
4
5
6
7
@Test
void basicScan() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

    MemberService memberService = ac.getBean(MemberService.class);
    assertThat(memberService).isInstanceOf(MemberService.class);
}

AnnotationConfigApplicationContext에 설정 정보로 AutoAppConfig를 넘기는 방식은 이전과 동일하다. 실행 로그를 보면 ClassPathBeanDefinitionScanner@Component가 붙은 클래스들을 후보로 인식해서 등록하는 것을 확인할 수 있다.

이때 빈 이름은 별도로 지정하지 않으면 클래스명의 첫 글자만 소문자로 바꾼 이름이 기본값으로 쓰인다. 예를 들어 MemberServiceImpl 클래스는 memberServiceImpl이라는 이름으로 등록된다. 이름을 직접 정하고 싶다면 @Component("memberService2")처럼 값을 넣어주면 된다.


5. 탐색 위치 지정하기

프로젝트의 모든 클래스를 매번 다 스캔하면 그만큼 시간이 걸린다. 그래서 탐색을 시작할 패키지 위치를 직접 지정할 수 있다.

1
2
3
@ComponentScan(
        basePackages = "hello.core"
)
  • basePackages: 탐색을 시작할 패키지를 지정한다. 지정한 패키지와 그 하위 패키지가 모두 대상이 된다. {"hello.core", "hello.service"}처럼 여러 위치를 한 번에 지정할 수도 있다.
  • basePackageClasses: 특정 클래스가 속한 패키지를 탐색 시작 위치로 삼는다.
  • 아무것도 지정하지 않으면 @ComponentScan이 붙은 설정 클래스가 위치한 패키지가 시작 위치가 된다.

실무에서는 대부분 basePackages를 따로 지정하지 않고, 설정 클래스를 프로젝트의 최상위 패키지에 두는 방식을 사용한다. com.hello 아래에 service, repository 같은 하위 패키지가 있다면, com.hello에 메인 설정 클래스를 두고 @ComponentScan만 붙이면 하위 패키지 전체가 자동으로 스캔 대상이 된다. 스프링 부트의 @SpringBootApplication을 프로젝트 최상단에 두는 관례도 같은 이유에서다. @SpringBootApplication 안에 이미 @ComponentScan이 포함되어 있다.


6. 컴포넌트 스캔의 기본 대상

컴포넌트 스캔은 @Component뿐 아니라 다음 애노테이션들도 함께 스캔 대상으로 삼는다.

애노테이션주 용도
@Component컴포넌트 스캔의 기본 대상
@Controller스프링 MVC 컨트롤러
@Service비즈니스 로직 계층
@Repository데이터 접근 계층
@Configuration스프링 설정 정보

이 애노테이션들의 소스 코드를 열어보면 내부에 @Component가 붙어 있다. 자바 언어 자체에는 애노테이션 상속이라는 개념이 없지만, 스프링이 이 관계를 인식해서 컴포넌트 스캔 대상으로 처리해준다. 그리고 스캔 대상이 되는 것 외에도 각 애노테이션은 스프링에게 추가 신호를 준다. @Controller는 MVC 컨트롤러로, @Repository는 데이터 계층 예외를 스프링 예외로 변환해주는 대상으로, @Configuration은 앞서 다룬 것처럼 싱글톤을 보장하는 설정 정보로 인식된다. @Service는 특별한 처리를 하지는 않지만, 핵심 비즈니스 로직이 위치하는 계층임을 코드로 드러내는 역할을 한다.


7. 정리

컴포넌트 스캔을 사용하면 @Bean 메서드를 일일이 작성하지 않아도 @Component 계열 애노테이션만으로 빈이 자동 등록되고, @Autowired가 타입을 기준으로 의존관계를 자동으로 연결해준다. 빈이 늘어날수록 설정 코드의 반복이 줄어드는 효과가 커진다.

탐색 범위는 basePackages로 직접 지정할 수도 있지만, 설정 클래스를 프로젝트 최상위 패키지에 두고 범위를 생략하는 방식이 더 자주 쓰인다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.