[스프링] 컴포넌트 스캔

gyeol·2023년 7월 21일

스프링

목록 보기
16/50
post-thumbnail

김영한 님의 '스프링 핵심 원리 - 기본편' 을 수강하고 적은 글입니다.

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

지금까지 스프링 빈을 등록하기 위해 @Bean을 통해 설정 정보에 직접 등록할 스프링 빈을 나열했다. 예제에서는 비록 몇개가 안되었지만, 수많은 스프링 빈을 일일히 등록한다면 귀찮고 설정 정보도 커지고, 누락할 수도 있다. 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다. 그리고 의존관계를 주입하는 @Autowired 라는 기능도 제공한다.

먼저 hello.core > 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)
        //Configuration이 붙은 클래스는 빼고 등록
)
public class AutoAppConfig {

}

컴포넌트 스캔을 사용하기 위해서는 @ComponentScan을 설정 정보에 붙여준다. 이 전의 AppConfig.java 클래스와 다르게 @Bean이 붙어있지 않은 것을 확인해볼 수 있다.
그리고 컴포넌트 스캔을 사용하게되면 @Configuration이 붙은 설정 정보도 자동으로 등록되기에 exculdeFilters를 사용해 설정정보는 컴포넌트 스캔 대상에서 제외했다.
@Component뿐만 아니라 @Configuration이 붙은 애들이 스캔의 대상이 되는 이유는 @Configuration 소스 코드를 열어보면 @Component 애노테이션이 붙어있기 때문이다.

우리가 이전에 만들어 놓았던 MemoryMemberRepository.java, RateDiscount.java, @MemberServiceImpl, @OrderServiceImpl에 @Component 애노테이션을 붙여준다.

이렇게 @Component 통해 빈으로 따로따로 등록하지 않아도 된다. 하지만 이렇게 되면 의존관계 주입은 어떻게 해야할까?

바로 생성자에 자동 의존관계주입을 해줘야한다. 우리는 @Autowired를 사용해야한다. 각각 @MemberServiceImpl, @OrderServiceImpl 생성자에 적어준다.
@Autowired는 스프링이 타입에 맞는 애를 찾아와서 의존관계 주입을 자동으로 연결해준다. @ComponentScan을 사용하면 @Autowired 를 사용하게 된다.

그리고 이를 테스트하기 위해 테스트 케이스를 만들어보자. test > scan > 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.*;

public class AutoAppConfigTest {
    @Test
    void basicScan(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

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

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

1. @ComponentScan

@ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다. 이때 스프링 빈의 기본 이름은 클래스명을 사용하되 앞글자만 소문자로 변경해 사용한다.
ex) MemberServiceImpl 클래스 -> memberServiceImpl

빈 이름을 직접 지정하려면 @Component("memberService5") 이렇게 지정해주면 된다.

2.@Autowired

생성자에 @Autowired를 지정하면, 컨테이너가 자동으로 해당 스프링 빈을 찾아 주입한다. getBean(MemberRepository.class)와 동일하다고 생각하면 된다.
생성자에 파라미터가 많아도 자동으로 주입한다.

✔️ 탐색 위치와 기본 스캔 대상

필요한 위치부터 자바 클래스를 탐색할 수 있도록 시작 위치를 지정할 수 있다.

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

이렇게 적으면 hello.core 폴더 밑에 있는 자바 클래스들을 탐색하겠다는 의미이다. basePackages = {"hello.core", "hello.service"} 이렇게 여러 시작 위치를 지정할 수도 있다. 만약 시작 위치를 지정해주지 않으면 @ComponentScan 이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.

스프링에서 기본으로 제공하는 방법은, 설정 정보 클래스 위치를 프로젝트 최상단에 두는 것이라고 한다. 이렇게 하면 자바 클래스 파일 모두 컴포넌트 스캔의 대상이 되기 때문이다. 스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication을 프로젝트 시작 루트 위치에 두는 것이 관례이다. @SpringBootApplication의 소스 코드를 열어다보면 @ComponentScan이 들어있다.

컴포넌트 스캔 기본 대상

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

컴포넌트 스캔 용도 뿐만 아니라 다음 애노테이션이 존재하면 부가 기능을 수행한다.

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

✔️ 필터

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

일단 scan > filter 패키지를 만들어준다. 그리고 그 안에 MyIncludeComponent, MyExcludeComponent애노테이션을 추가해준다.

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 {
}


package hello.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}

애노테이션에 주의하면서 코드를 적어야한다 !!

그리고 이들을 테스트하기 위한 테스트 케이스도 추가해준다.
scan > filter > ComponentFilterAppConfigTest.java 클래스를 만들어준다.

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;

import java.lang.annotation.Target;

import static org.assertj.core.api.Assertions.*;
import static org.springframework.context.annotation.ComponentScan.*;

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); 
        //이 상태로 돌리면 beanB는 컴포넌트스캔에서 빠져버리기에 오류남
        
        org.junit.jupiter.api.Assertions.assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("beanB", BeanB.class)
        );


    }

    @Configuration
    @ComponentScan(
            includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            //MyIncludeComponent 클래스는 등록
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
            //MyExcludeComponent 클래스는 등록X
    )
    static class ComponentFilterAppConfig{

    }
}

IncludeFilters에 @MyIncludeComponent를 추가해서 BeanA가 스프링 빈에 등록된다. 반대로 ExcludeFilters에는 @MyExcludeComponent를 추가했기에 BeanB는 스프링 빈에 등록되지 않는다.

FilterType 옵션

  • ANNOTATION : 기본값, 애노테이션을 인식해 동작한다.
  • ASSIGNABLE_TYPE : 지정한 타입과 자식 타입을 인식해 동작한다.
  • ASPECTJ : AspectJ 패턴 사용
  • REGEX : 정규표현식
  • CUSTOM : TypeFilter라는 인터페이스를 구현해 처리한다.

✔️ 중복 등록과 충돌

컴포넌트 스캔에서 같은 빈이름을 등록하게되면 두가지 상황이 존재한다. 첫번째는 자동 빈 등록 vs 자동 빈 등록이고, 두번째는 수동 빈 등록 vs 자동 빈 등록이다.

1. 자동 빈 등록 vs 자동 빈 등록

ConflictingBeanDefinitionException 오류를 발생시킨다.

2. 수동 빈 등록 vs 자동 빈 등록

수동 빈 등록이 우선권을 가진다. (수동 빈이 자동 빈을 오버라이딩 한다)
수동 빈 등록시,

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

이런 로그가 남는다. 그래서 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.

profile
공부 기록 공간 '◡'

2개의 댓글

comment-user-thumbnail
2023년 7월 21일

스프링의 '@ComponentScan'과 '@Autowired'에 대해 아주 자세하게 설명해주셨네요. 이 글 덕분에 컴포넌트 스캔과 의존관계 자동 주입에 대한 이해가 훨씬 깊어졌습니다. 감사합니다!

1개의 답글