Spring

[스프링] 스프링 핵심 원리 기본편 정리: 6. 컴포넌트 스캔

아윤_ 2023. 9. 1. 19:04

컴포넌트 스캔

 

 

컴포넌트 스캔과 의존관계 자동 주입 시작하기

 

현재까지 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML의 <bean> 등을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다. 하지만, 등록해야 할 스프링 빈이 수백 개가 되면 일일이 등록하기 귀찮을 뿐만 아니라, 설정정보가 커지고, 누락하는 문제가 발생한다.

 

스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔과 의존관계를 자동으로 주입하는 @Autowired라는 기능을 제공한다. 이에 대해 코드를 통해 살펴보도록 하자.

 

 

AutoAppConfig.java

 

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 {

}

 

컴포넌트 스캔을 사용하기 위해서는 다음과 같이 @ComponentScan 애노테이션을 설정 정보에 붙여주면 된다. 컴포넌트 스캔은 @Component 애노테이션이 붙은 클래스들을 스프링 빈에 자동 등록한다.

 

기존의 AppConfig.java와는 다르게 @Bean으로 등록한 클래스가 하나도 없는 걸 알 수 있다.

 

이제 각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component 애노테이션을 붙여주면 된다.

 

 

MemoryMemberRepository @Component 추가

 

@Component
public class MemoryMemberRepository implements MemberRepository {}

 

 

RateDiscountPolicy @Component 추가

 

@Component
public class RateDiscountPolicy implements DiscountPolicy{
}

 

 

MemberServiceImpl @Component , @Autowired 추가

 

@Component
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

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

 

이전의 AppConfig에서는 @Bean으로 직접 설정 정보를 작성하고, 의존관계도 직접 명시했지만, 이제는 이런 설정 정보 자체가 없기 때문에 의존관계 주입도 해당 클래스 안에서 해결해야 된다.

 

@Autowired를 사용하면 스프링에 등록된 빈의 타입을 보고 타입에 맞는 의존관계를 자동으로 주입할 수 있다.

 

 

OrderServiceImpl @Component , @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;
    }
}

 

 

AutoAppConfigTest.java

 

package hello.core.scan;


import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
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);
    }

}

 

AnnotationConfigApplicationContext를 사용하는 것은 기존과 동일하며, 설정 정보로 AutoAppConfig 클래스를 넘겨주었다. 실행했을 때, 기존과 같이 잘 동작하는 것을 알 수 있다.

 

다음과 같은 로그를 통해 컴포넌트 스캔이 잘 동작하는 것을 확인할 수 있다.

ClassPathBeanDefinitionScanner - Identified candidate component class:
.. RateDiscountPolicy.class
.. MemberServiceImpl.class
.. MemoryMemberRepository.class
.. OrderServiceImpl.class

 

이제 컴포넌트 스캔과 자동 의존관계 주입이 어떻게 동작하는지 그림으로 알아보자.

 

 

1. @Componentscan

 

 

 

먼저, @Componentscan은 @Component가 붙은 클래스들을 모두 스프링 빈으로 등록한다. (싱글톤)

 

여기서, 빈 이름에 주의해야 한다. 스프링 이름의 기본 이름은 클래스 명을 사용하지만, 맨 앞글자만 소문자를 사용한다.

ex) MemberServiceImpl 클래스의 경우 memberServiceImpl로 빈 이름이 등록된다.

 

빈 이름을 직접 지정할 수도 있는데, 이 경우 @Component("빈 이름")과 같은 방식으로 이름을 부여할 수 있다.

 

 

2. @Autowired 의존관계 자동 주입

 

 

생성자에 @Autowired를 지정하면, MemberServiceImpl을 생성하면서 스프링이 스프링 컨테이너에 있는 타입이 같은 스프링 빈을 찾아 자동으로 의존관계를 주입한다.

 

마치 ac.getBean(MemberRepository.class); 와 동일하다고 보면 된다.

 

 

다음과 같이 생성자에 파라미터가 많은 경우에도 자동으로 의존관계를 주입한다.

 

 

 

탐색 위치와 기본 스캔 대상

 

탐색할 패키지의 시작 위치 지정

 

모든 자바 클래스를 컴포넌트 스캔하면, 시간이 오래 걸린다. 따라서 탐색할 패키지의 시작 위치를 다음과 같이 지정할 수 있다.

@ComponentScan(
 basePackages = "hello.core",
}

 

basePackages를 통해 탐색할 패키지의 시작 위치를 지정하면, 해당 패키지를 포함해서 하위 패키지를 모두 탐색한다.

 

basePackages = {"hello.core", "hello.service"}와 같이 여러 개의 패키지를 시작 위치로 지정할 수도 있다.

 

@ComponentScan(
 basePackageClasses = AutoAppConfig.class,
}

다음과 같이 basePackageClasses를 사용하면, 지정한 클래스의 패키지를 탐색 위치로 지정한다.

 

만약, 탐색 위치를 지정하지 않을 경우, @ComponentScan 애노테이션이 붙은 설정 정보 클래스의 패키지를 탐색 위치로 지정하게 된다!

 

 

권장하는 방법

 

권장하는 방법은 패키지 위치를 설정하지 않고, 설정 정보 클래스의 위치를 프로젝트의 최당산에 두는 것이다. 최근 스프링 부트도 이 방법을 기본으로 제공하고 있다.

 

또한, 프로젝트 메인 설정 정보는 프로젝트를 대표하는 정보이기 때문에 프로젝트 시작 위치에 두는 게 좋다고 한다.

참고로, 스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication를 이 프로젝트 시작 루트 위치에 두는 것이 관례이다. 실제로, 해당 애노테이션을 살펴보면, 그 안에 @ComponentScan이 들어있다.

 

 

컴포넌트 스캔의 기본 대상

 

컴포넌트 스캔은 @Component가 붙은 클래스뿐만 아니라 다음의 내용도 추가로 대상에 포함한다.

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

 

실제로 해당 클래스의 코드를 보면, @Component 애노테이션이 붙은 것을 확인할 수 있다.

 

※ 주의

애노테이션은 상속 관계가 없으며, 자바 언어가 지원하는 기능이 아닌, 스프링이 지원하는 기능이다.

 

 

컴포넌트 스캔의 용도뿐만 아니라 다음 애노테이션이 있으면 스프링은 부가 기능을 수행한다.

  • @Controller: 스프링 MVC 컨트롤러로 인식
  • @Repository: 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 계층의 예외로 변환
  • @Configuration: 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리
  • @Service: 사실 특별한 처리를 하지 않지만, @Service가 있을 경우 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나라고 비즈니스 계층을 인식하는데 도움

 

 

 

필터 

 

  • includeFilters: 컴포넌트 스캔 대상을 추가로 지정한다.
  • excludeFilters: 컴포넌트 스캔에서 제외할 대상을 지정한다.

 

코드를 통해 알아보자.

 

 

컴포넌트 스캔 대상에 추가할 애노테이션

 

package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}

 

 

컴포넌트 스캔 대상에서 제외할 애노테이션

 

package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}

 

 

컴포넌트 스캔 대상에 추가할 클래스

 

package hello.core.scan.filter;

@MyIncludeComponent
public class BeanA {
}

 

@MyIncludeComponent 애노테이션을 적용해 준다.

 

 

컴포넌트 스캔 대상에서 제외할 클래스

 

package hello.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}

 

@MyExcludeComponent 애노테이션을 적용한다.

 

 

설정 정보와 전체 테스트 코드

 

package hello.core.scan.filter;

import org.assertj.core.api.Assertions;
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.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);
        Assertions.assertThat(beanA).isNotNull();

        org.junit.jupiter.api.Assertions.assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("beanB", BeanB.class));
    }

    @Configuration
    @ComponentScan(
            includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig {

    }
}

 

includeFilters에 MyIncludeComponent 애노테이션을 적용하여 BeanA는 스프링 빈에 등록되고,

excludeFilters에는 MyExcludeComponent 애노테이션을 적용하여 BeanB는 스프링 빈에 등록되지 않는다.

 

 

 

중복 등록과 충돌

 

컴포넌트 스캔에서 같은 빈 이름을 등록하게 될 경우, 다음과 같이 두 가지 상황이 발생한다.

 

  1. 자동 빈 등록 vs 자동 빈 등록
    • 둘 다 컴포넌트 스캔을 했는데, 이름이 똑같은 경우이다.
  2. 수동 빈 등록 vs 자동 빈 등록

 

 

자동 빈 등록 vs 자동 빈 등록

 

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같아 오류가 발생하는 경우이다.

 

이 경우, ConflictingBeanDefinitionException 예외가 발생한다.

 

하지만, 자동 빈끼리 충돌이 일어나는 경우는 거의 없다고 보면 된다.

 

 

수동 빈 등록 vs 자동 빈 등록

 

이번에는 수동 빈 등록과 자동 빈 등록에서 빈 이름이 충돌될 때 어떻게 되는지 코드를 통해 알아보자.

 

@Configuration
@ComponentScan(
        excludeFilters= @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {

    @Bean(name = "memoryMemberRepository")
    MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }


}

 

다음과 같이 수동으로 빈을 등록하고, 컴포넌트 스캔을 통해 등록되는 빈과 동일한 이름을 지정해 주었다. 예상과는 다르게, 테스트 코드 실행을 해보면 오류가 발생하지 않는 것을 알 수 있다.

 

그 이유는 자동 빈 등록과 수동 빈 등록에서 충돌이 일어날 경우, 수동 빈 등록이 우선권을 가지기 때문에 충돌이 일어나지 않는다.

 

수동 빈 등록 시에 남는 로그는 다음과 같다.

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

 

대부분의 경우, 여러 설정들이 꼬여서 이런 결과가 발생하게 되는데, 이러한 오류는 매우 잡기 어렵다. 따라서, 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌되면 오류가 발생되도록 기본값을 바꾸었다고 한다.

 

스프링 부트인 CoreApplication을 실행하면 다음과 같은 오류가 발생한다.

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

 

결론만 얘기하자면, 애매한 상황은 만들지 않는 것이 좋다. 애매하지만 코드가 조금 더 이뻐지는 것과, 명확하지만 코드가 좀 더 길어지는 것 중 하나를 선택해야 한다면 무조건 명확한 것을 선택하는 것이 좋다.