[Spring] Chapter 6. 컴포넌트 스캔

들어가기 앞서

이 글은 김영한 님의 스프링 핵심 원리 - 기본편(https://www.inflearn.com/스프링-핵심-원리-기본편/dashboard)을 수강하며 학습한 내용을 정리한 글입니다. 모든 출처는 해당 강의에 있습니다.


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

  • 지금까지는 자바 코드의 @Bean이나 XML의 <bean>등을 통해 설정 정보에 스프링 빈을 직접 나열하여 등록함
  • 스프링에서는 컴포넌트 스캔과 @Autowired 기능을 제공함
    • 컴포넌트 스캔 : 설정 정보가 없어도 자동으로 스프링 빈 등록
      → 실무에서는 등록해야 할 스프링 빈의 개수가 상당히 많음
      • 설정 정보의 증가 및 누락
      • 비효율적인 반복
    • @Autowired : 의존관계 자동 주입

AutoAppConfig.java

기존의 AppConfig.java는 그대로 두고 AutoAppConfig.java를 새로 생성한다.

[src/main/java/hello/core/AutoAppConfig.java]

package hello.core;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(  //컴포넌트 스캔 사용
		 //@Component가 적용된 클래스를 스캔하여 스프링 빈으로 등록
        //컴포넌트 스캔 대상에서 제외 할 설정 정보 지정
        excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}
  • 기존의 AppConfig와는 다르게 @Bean으로 등록한 클래스가 존재하지 않음

💡 참고

  • 컴포넌트 스캔 사용 시 @Configuration이 붙은 설정 정보도 자동으로 등록됨
    • 앞서 생성한 설정 정보(AppConfig, TestConfig 등)도 함께 등록되어 실행 됨
    • 내부적으로 @Component가 적용되어 있으므로 수동 등록과 같음

=> excludeFilters 적용하여 제외

  • 일반적으로 설정 정보는 컴포넌트 스캔 대상에서 제외하지 않음

✅ 어노테이션 추가

[src/main/java/hello/core/member/MemoryMemberRepository.java]

@Component
public class MemoryMemberRepository implements MemberRepository { ... }

[src/main/java/hello/core/discount/RateDiscountPolicy.java]

@Component
public class RateDiscountPolicy implements DiscountPolicy { ... }

[src/main/java/hello/core/member/MemberServiceImpl.java]

@Component
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    @Autowired  //ac.getBean(MemberRepository.class)와 같음
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
    ...
}
  • AutoAppConfig에 설정 정보가 없으므로, 이 클래스 내에서 의존관계 주입 필요
    @Autowired로 해결

[src/main/java/hello/core/order/OrderServiceImpl.java]

@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 사용 시 여러 의존관계 한 번에 주입 가능

✅ 테스트

💻 테스트 코드

[src/test/java/hello/core/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.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;

public class AutoAppConfigTest {

    @Test
    void basicScan(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);

        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

💻 결과

...
ClassPathBeanDefinitionScanner - Identified candidate component class:
... RateDiscountPolicy.class
... MemberServiceImpl.class
... MemoryMemberRepository.class
... OrderServiceImpl.class
...
Autowiring by type from bean name 'memberServiceImpl' via constructor to bean named 'memoryMemberRepository'
...
... 'orderServiceImpl' via constructor to bean named 'memoryMemberRepository'
... 'orderServiceImpl' via constructor to bean named 'rateDiscountPolicy'
...

✅ 동작

  1. @ComponentScan
  • @Component가 적용된 모든 클래스를 스프링 빈으로 등록
  • 스프링 빈 이름 ★
    • 기본 전략 : 맨 앞글자소문자를 적용한 클래스명
      ex) MemberServiceImpl 클래스 → memberServiceImpl
    • 직접 지정 : @Component("원하는 이름")
      ex) @Component("memberService2")
  1. @Autowired : 의존관계 자동 주입
  • 스프링 컨테이너가 스프링 빈을 찾아 자동 주입
  • 기본 조회 전략 : 타입이 같은 빈(자식도 가능)
    getBean(MemberRepository.class)와 동일한 의미
  • 생성자의 파라미터 갯수 상관 없이 모두 조회하여 자동 주입


📖 탐색 위치와 기본 스캔 대상

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

@ComponentScan(
    basePackages = "hello.core",
    ...
)
  • basePackages
    • 탐색할 패키지의 시작 위치 지정
    • 해당 패키지를 포함한 하위 패키지까지 모두 탐색
    • 여러 시작 위치 지정 가능
      ex) basePackages = {"hello.core", "hello.service"}
  • basePackageClassed
    • 지정한 클래스의 패키지를 탐색 시작 위치로 지정
    • @ComponentScan이 적용된 설정 정보 클래스의 패키지(기본) ★

📝 권장하는 방법

  • 패키지 위치를 지정하지 않고 설정 정보 클래스를 프로젝트 최상단에 둔다.
    • 프로젝트 메인 설정 정보 = 프로젝트 대표 정보
      → 프로젝트 시작 루트 위치에 두는 것이 좋음
    • 예시
      • 프로젝트 구조
        • com.hello → 프로젝트 시작 루트
          • 메인 설정 정보를 둠 ex) AppConfig
          • @ComponentScan 설정
          • basePackages 생략
        • com.hello.service
        • com.hello.repository
  • 스프링 부트에서는 관례적으로 @SpringBootApplication(내부적으로@CompoentScan 포함)을 프로젝트 시작 루트 위치에 둠

✅ 컴포넌트 스캔 기본 대상

항목설명부가 기능
@Component컴포넌트 스캔에서 사용
@Controller스프링 MVC 컨트롤러에서 사용스프링 MVC 컨트롤러로 인식
@Service스프링 비즈니스 로직에서 사용◾ 특별한 처리 x
◾ 개발자들의 비즈니스 계층 인식에 도움
@Repository스프링 데이터 접근 계층에서 사용
ex) JPA, JDBC
◾ 스프링 데이터 접근 계층으로 인식
◾ 데이터 계층의 예외를 스프링 예외로 변환
  ex) 다른 DB로 변경하는 경우, 예외 발생 시
       다른 DB의 예외가 발생
   → 서비스 및 다른 계층의 코드에도 영향
   ∴ 스프링이 예외를 추상화하여 반환
@Configuration스프링 설정 정보에서 사용◾ 스프링 설정 정보로 인식
◾ 스프링 빈의 싱글톤 유지를 위한 추가 처리 수행

내부적으로 @Component를 포함한다.

@Component
public @interface Controller {
}

@Component
public @interface Service {
}

@Component
public @interface Configuration {
}

💡 애노테이션에는 상속관계가 존재하지 않는다. 스프링에서 지원하는 기능을 통해 애노테이션에 적용된 특정 애노테이션이 인식 가능한 것이다.

💡 userDefaultFilters

  • 유효한 패키지 경로에서 스테레오 타입 애노테이션이 붙은 클래스를 빈으로 등록하는 속성 ex) @Bean, @Component
  • 기본값은 true
  • false로 지정시 기본 스캔 대상들 제외


📖 필터

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

✅ 예제

  • 애노테이션은 Annotation으로 클래스 생성한다.

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

[src/test/java/hello/core/scan/filter/MyIncludeComponent.java]

package hello.core.scan.filter;

import java.lang.annotation.*;

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

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

[src/test/java/hello/core/scan/filter/MyExcludeComponent.java]

package hello.core.scan.filter;

import java.lang.annotation.*;

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

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

[src/test/java/hello/core/scan/filter/BeanA.java]

package hello.core.scan.filter;

@MyIncludeComponent
public class BeanA {
}

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

[src/test/java/hello/core/scan/filter/BeanB.java]

package hello.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}

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

[src/test/java/hello/core/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.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ComponentFilterAppConfigTest {

    @Test
    void filterScan() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentfilerAppConfig.class);

        BeanA beanA = ac.getBean("beanA", BeanA.class);
        assertThat(beanA).isNotNull();

        //ac.getBean("beanB", BeanB.class); → 에러 발생

        assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("beanB", BeanB.class));
    }

    @Configuration
    @ComponentScan(
    	    //BeanA 스프링 등록
            includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            //BeanB 스프링 등록 제외
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentfilerAppConfig {

    }
}

✅ FilterType 옵션

항목설명
ANNOTATION◾ 기본값 → 생략 가능
◾ 애노테이션을 인식하여 동작
ex) org.example.SomeAnnotation
ASSIGNABLE_TYPE지정한 타입과 자식 타입 인식하여 동작
→ 클래스 직접 지정
ex) org.example.SomeClass
ASPECTJAspectJ 패턴 사용
ex) org.example..*Service+
REGEX정규 표현식
ex) org\.exampe\.Default.*
CUSTOMTypeFilter 인터페이스 구현하여 처리
ex) org.example.MyTypeFilter

📝 예시

BeanA 제외하고 싶은 경우

@ComponentScan(
    includeFilters = {
        @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class)
    },
    excludeFilters = {
        @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class),
        @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = BeanA.class)
    }
)

💡 참고

  • includeFilters는 거의 사용되지 않음
  • excludeFilters는 간혹 사용될 때가 있지만 많지 않음
  • 스프링 부트에서 컴포넌트 스캔을 기본으로 제공
    → 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장


📖 중복 등록과 충돌

컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까?

✅ 자동 빈 등록 vs 자동 빈 등록

  • 컴포넌트 스캔에 의해 자동으로 스프링 빈 등록
  • 이름이 같은 경우 스프링이 오류 발생시킴
    ConflictingBeanDefinitionException 예외 발생

✅ 수동 빈 등록 vs 자동 빈 등록

@Component  //기본 이름 → memoryMemberRepository
public class MemoryMemberRepository implements MemberRepository {}
@Configuration
@ComponentScan {
    excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
}
public class AutoAppConfig {

    @Bean(name = "memoryMemberRepository")
    pubic MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}
  • 수동 빈 등록이 우선권을 가짐
    → 수동 빈이 자동 빈을 오버라이딩 해버림

📝 수동 빈 등록시 남는 로그

Overrriding bean definition for bean 'memoryMemberRepository'
with a different definition: replacing
  • 개발자가 의도한 결과라면 수동이 우선권을 가지는 것이 좋음
  • 보통은 여러 설정들이 꼬여서 충돌이 생기는 경우가 대부분
    → 애매한 버그, 즉 해결하기 매우 어려운 버그가 발생하게 됨

📝수동 빈 등록, 자동 빈 등록 오류시 스프링 부트 에러

  • 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록 충돌 시 오류가 발생하도록 기본 값을 false로 변경함
  • 스프링 부트인 CoreApplication을 실행하면 다음과 같은 오류가 발생함
Consider renaming one of the beans or enabling overriding by setting
spring.main.allow-bean-definition-overriding=true
  • 오버라이딩하여 사용하고 싶을 경우, application.properties에 등록하면 됨



📖 참고

좋은 웹페이지 즐겨찾기