[Spring-기본] 컴포넌트 스캔과 의존관계 자동 주입하기

DANI·2023년 11월 24일

Spring[김영한T]

목록 보기
24/31
post-thumbnail

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

만약, 등록해야할 스프링 빈이 수백개라면 설정정보도 커지고 누락하는 문제도 발생한다. 컴포넌트 스캔과 의존관계 자동 주입으로 좀 더 편하게 설정정보를 등록해보자!



💾 AutoAppConfig 파일 생성

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))
// 컴포넌트스캔 괄호안에 내용은 미리 만들어 둔 AppConfig파일을
// 컴포넌트 스캔 목록에서 제외시키는 것
public class AutoAppConfig {

}

@ComponentScan@Component 애너테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록해주고, @Autowired 애너테이션은 의존관계를 자동으로 주입해준다. 파일을 수정해보자




💾 OrderServiceImpl에 @Component, @Autowired 추가

@Component // 애너테이션 등록
public class OrderServiceImpl implements OrderService{

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired // 애너테이션 등록
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

💾 MemberServiceImpl에 @Component, @Autowired 추가

@Component
public class MemberServiceImpl implements MemberService{

    private MemberRepository memberRepository;

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

💾 RateDiscountPolicy에 @Component 추가

@Component
public class RateDiscountPolicy implements DiscountPolicy{
    private int discountRate = 10;

💾 MemoryMemberRepository에 @Component 추가

@Component
public class MemoryMemberRepository implements MemberRepository{




💾 AutoAppConfigTest 테스트

package hello.core;

import hello.core.member.MemberService;


import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;



class AutoAppConfigTest {

    @Test
    @DisplayName("컴포넌트 스캔 확인하기")
    void basicSacn(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService bean = ac.getBean(MemberService.class);
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for(String beans : beanDefinitionNames){
            BeanDefinition beanDefinition = ac.getBeanDefinition(beans);
            if(beanDefinition.getRole()==BeanDefinition.ROLE_APPLICATION){
                System.out.println("beanDefinitionNames : " + beans + ", beanDefinition : " + beanDefinition);
            }
        }
    }
}

🔵 실행 결과

beanDefinitionNames : autoAppConfig, beanDefinition : Generic bean: class [hello.core.AutoAppConfig$$SpringCGLIB$$0]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null
beanDefinitionNames : rateDiscountPolicy, beanDefinition : Generic bean: class [hello.core.Discount.RateDiscountPolicy]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [C:\Users\Dani\Downloads\core\out\production\classes\hello\core\Discount\RateDiscountPolicy.class]
beanDefinitionNames : memberServiceImpl, beanDefinition : Generic bean: class [hello.core.member.MemberServiceImpl]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [C:\Users\Dani\Downloads\core\out\production\classes\hello\core\member\MemberServiceImpl.class]
beanDefinitionNames : memoryMemberRepository, beanDefinition : Generic bean: class [hello.core.member.MemoryMemberRepository]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [C:\Users\Dani\Downloads\core\out\production\classes\hello\core\member\MemoryMemberRepository.class]
beanDefinitionNames : orderServiceImpl, beanDefinition : Generic bean: class [hello.core.Order.OrderServiceImpl]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [C:\Users\Dani\Downloads\core\out\production\classes\hello\core\Order\OrderServiceImpl.class]

beanDefinition의 메타정보를 확인하여 빈이 잘 생성되었음을 확인할 수 있다.



💡 만약 빈 이름을 직접 지정하고 싶다면..?

@Component("MemberService Auto") // 이름 지정
public class MemberServiceImpl implements MemberService{

    private MemberRepository memberRepository;

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

🔵 실행 결과

beanDefinitionNames : autoAppConfig, beanDefinition : Generic bean: class [hello.core.AutoAppConfig$$SpringCGLIB$$0]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null
beanDefinitionNames : rateDiscountPolicy, beanDefinition : Generic bean: class [hello.core.Discount.RateDiscountPolicy]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [C:\Users\Dani\Downloads\core\out\production\classes\hello\core\Discount\RateDiscountPolicy.class]
beanDefinitionNames : MemberService Auto, beanDefinition : Generic bean: class [hello.core.member.MemberServiceImpl]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [C:\Users\Dani\Downloads\core\out\production\classes\hello\core\member\MemberServiceImpl.class]
beanDefinitionNames : memoryMemberRepository, beanDefinition : Generic bean: class [hello.core.member.MemoryMemberRepository]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [C:\Users\Dani\Downloads\core\out\production\classes\hello\core\member\MemoryMemberRepository.class]
beanDefinitionNames : orderServiceImpl, beanDefinition : Generic bean: class [hello.core.Order.OrderServiceImpl]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [C:\Users\Dani\Downloads\core\out\production\classes\hello\core\Order\OrderServiceImpl.class]

MemberService Auto로 빈이름이 생성되었다!






1. 컴포넌트 스캔에서 컴포넌트를 가져와서 빈으로 등록한다



2. 의존관계 자동주입을 통해 적절한 구현체를 주입해준다.





🔍 컴포넌트 스캔의 시작위치는?

컴포넌트 스캔은 @Component 애너테이션이 붙은 클래스들을 자동으로 스프링 빈으로 등록해준다. 그렇다면 모든 클래스 파일들을 다 스캔하는 것일까?


@Configuration
@ComponentScan(
        basePackages = "hello.core",  // 지정한 패키지를 포함해서 하위 패키지 모두 탐색, {} 으로 여러 시작위치 지정 가능
       // basePackageClasses 시작 클래스 위치 설정
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}

위와 같이 basePackages, basePackageClasses로 스캔 시작 위치를 지정할 수 있다.

✨ 보통 시작 위치에 Config파일을 두는 것이 좋다. 따로 시작위치를 지정하지 않아도 되기 때문이다. ✨





💻 필터 적용해보기

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

다음의 파일들을 새로 생성하자

💾 MyExCludeComponent

package hello.core.scan;

import java.lang.annotation.*;

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

}

💾 MyInCludeComponent

package hello.core.scan;

import org.springframework.stereotype.Component;

import java.lang.annotation.*;

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

}

Configuration API

@Component를 제외한 나머지 애너테이션을 추가한다.


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
	boolean proxyBeanMethods() default true;
	boolean enforceUniqueMethods() default true;
}

💾 BeanA

package hello.core.scan.filter;

import hello.core.scan.MyInCludeComponent;

@MyInCludeComponent
public class BeanA {
}

💾 BeanB

package hello.core.scan.filter;

import hello.core.scan.MyExCludeComponent;

@MyExCludeComponent
public class BeanB {
}

💾 filterTest 테스트

package hello.core;

import hello.core.scan.MyExCludeComponent;
import hello.core.scan.MyInCludeComponent;
import hello.core.scan.filter.BeanA;
import hello.core.scan.filter.BeanB;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
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 filterTest {

    @Test
    @DisplayName("필터 테스트")
    void filter(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponenetFilterAppConfig.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 ComponenetFilterAppConfig{

    }
}

MyExclude에 등록된 BeanB는 등록되지 않았고, MyInclude에 등록된 BeanA는 등록되었다.





💡 FiterType 옵션

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



❓ 만약 자동 빈 등록과 수동 빈 등록이 충돌하게 된다면?

💾 AutoAppConfig에 memoryMemberRepository를 재정의

package hello.core;

import hello.core.Order.Order;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(
        basePackages = "hello.core",  // 지정한 패키지를 포함해서 하위 패키지 모두 탐색, {} 으로 여러 시작위치 지정 가능
       // basePackageClasses 시작 클래스 위치 설정
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {

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

}

🔴 AutoAppConfigTest 를 해보자!

beanDefinitionNames : autoAppConfig, beanDefinition : Generic bean: class [hello.core.AutoAppConfig$$SpringCGLIB$$0]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null
beanDefinitionNames : rateDiscountPolicy, beanDefinition : Generic bean: class [hello.core.Discount.RateDiscountPolicy]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [C:\Users\Dani\Downloads\core\out\production\classes\hello\core\Discount\RateDiscountPolicy.class]
beanDefinitionNames : MemberService Auto, beanDefinition : Generic bean: class [hello.core.member.MemberServiceImpl]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [C:\Users\Dani\Downloads\core\out\production\classes\hello\core\member\MemberServiceImpl.class]
beanDefinitionNames : memoryMemberRepository, beanDefinition : Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=autoAppConfig; factoryMethodName=memberRepository; initMethodNames=null; destroyMethodNames=[(inferred)]; defined in hello.core.AutoAppConfig
beanDefinitionNames : orderServiceImpl, beanDefinition : Generic bean: class [hello.core.Order.OrderServiceImpl]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [C:\Users\Dani\Downloads\core\out\production\classes\hello\core\Order\OrderServiceImpl.class]

memoryMemberRepository가 수동 빈 등록으로 인해 재정의 된다.





스프링부트인 CoreApplication을 실행해보자!

💾 CoreApplication

package hello.core;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CoreApplication {

	public static void main(String[] args) {
		SpringApplication.run(CoreApplication.class, args);
	}

}

🔵 실행결과

Description:

The bean 'memoryMemberRepository', defined in class path resource [hello/core/AutoAppConfig.class], could not be registered. A bean with that name has already been defined in file [C:\Users\Dani\Downloads\core\out\production\classes\hello\core\member\MemoryMemberRepository.class] and overriding is disabled.

Action:

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

수동 빈 등록과 자동 빈 등록이 충돌하면 오류가 발생하도록 기본값을 바꾸었기 때문에 스프링부트에서는 오류가 발생함을 알 수 있다.

0개의 댓글