인프런/스프링핵심원리(기본)

14)★★★컴포넌트 스캔과 의존관계 자동주입(@ComponentScan,@Autowired), 탐색위치와 기본 스캔 대상(@Service~) , 스캔필터링 , 중복 등록과 충돌

backend dev 2022. 12. 28.

AppConfig라는 구성설정 파일을 만들어서 스프링빈을 등록했고 등록하는 과정에서 자동으로 의존관계도 주입되었다.

하지만 등록해야하는 스프링빈이 많아지면 설정 정보도 커지고 누락하는 문제가 발생한다. ( + 귀찮음)

 

그래서 스프링에서는 설정정보 없이 자동으로 스프링 빈을 등록하는 컴포넌트 스캔을 제공한다.

의존관계도 자동으로 주입하는 @Autowired라는 기능도 제공한다.

 

공부를 위해 원래있던 AppConfig는 냅두고 새로 AutoAppConfig를 생성한다.

 

 

 

@ComponentScan을 사용하면 @Component 어노테이션이 붙은 모든 클래스를 찾아서 자동으로 스프링빈으로 등록해준다.

 

AutoAppConfig.java

@Configuration // 설정정보 파일이니까 @Configuration 어노테이션을 필수다.
@ComponentScan(
    excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Configuration.class)
) // 컴포넌트 스캔을 이용하여 @Component 어노테이션이 붙은 클래스를 전부 스프링빈으로 등록해줄것이다.
//그전에 필터를 통해서 @Configuration 어노테이션이 붙은 클래스는 등록하지않는다 -> 기존에있는 AppConfig가 등록되면 안되니까.
//컴포넌트스캔에 걸리는 이유는 @Configuration안에는 @Component이 있기떄문이다.
public class AutoAppConfig {
    //내용이 없다.

}

 

AppConfig.java와 다르게 @Bean~ 이런 빈등록하는 코드가 없다.

@Component가 붙은 클래스를 자동으로 등록해주기 때문이다.

그렇다면 스프링빈으로 등록될수 있게끔 클래스들에 @Component를 추가해준다.

 

사용할 구현체클래스에 가서 @Component를 붙여준다.

@Component
public class RateDiscountPolicy implements DiscountPolicy {

AppConfing에서 

@Bean
public DiscountPolicy DiscountPolicy() {
    return new RateDiscountPolicy();
}

이런식으로 등록을 했는데 이때 메소드명이 빈 이름이고, 빈 타입은 반환값이였다.

즉 구현체를 스프링빈으로 등록하는것이니까 구현체 클래스에 가서 @Component를 붙여준다.

AutoAppConfig 내부 내용을 보면 아무런 코드가 없다! @ComponentScan으로 빈등록을 하기 때문이다.

@Component
public class MemoryMemberRepository implements MemberRepository {

메모리멤버리포지토리에도 붙여주고

 

@Component
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;


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

멤버서비스임플에도 붙여줬는데 멤버서비스구현체 안에는 

원래 스프링빈에 등록된 멤버리포지토리를 주입받아 필드로 사용중이였다. 

 

@Component만 쓰면 AutoAppConfig의 @ComponentScan으로 인해 클래스를 빈으로 등록해줄뿐

멤버서비스임플이 멤버리포지토리를 의존하는 의존관계 설정은 해주지 못한다.

이럴때 의존관계 설정을 위해 (의존관계 주입) @Autowired를 사용한다.

@Component
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

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

다음과 같이 의존관계를 주입받기위해 있는 생성자위에다가 @Autowired를 붙여준다.

@Autowired가 붙어있으면 전달인자인 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;
    }

오더서비스임플도 어노테이션 붙여준다.

 

빈등록이 잘됬는지 테스트

public class AutoAppConfigTest {

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

        String[] beanDefinitionNames = ac.getBeanDefinitionNames(); //빈 메타정보들 이름을 가져온다. = 빈 이름을 가져온다.
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName); //빈 메타정보 이름을 이용해서 해당 빈 메타정보를 가져온다.
            // 빈 이름을 이용해서 빈 메타정보를 가져온다. getBeanDefinition메소드를 쓰기위해 AnnotationConfigApplicationContext객체에 담아 사용한다.

            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) { //빈 메타정보안에 Role정보를 이용해서, 내가 등록한 빈이라면 (스프링 자체에서 올린게아니고)
                System.out.println("beanDefinitionName = " + beanDefinitionName); //빈 이름을 출력해본다.
            }
        }

        MemberService memberService = ac.getBean(MemberService.class); //검증을 위해 스프링빈에 등록된 멤버서비스 구현체를 꺼내온다.
        assertThat(memberService).isInstanceOf(MemberService.class); //멤버서비스의 구현체인지 검증

    }

}

@Component를 붙여준 구현체클래스가 잘 출력되는것을 확인할 수 있다.

위쪽 로그를 보면 스캐너가 @Component붙은 클래스를 어디서 찾았는지 동작하는 로그가 보인다.
creating~ 을 통해 스프링빈으로 등록되는 로그도 확인가능하고, Autowiring~을 통해 의존관계주입되고있는 로그도 확인 가능하다.

 

@ComponentScan 동작방식

@Component가 붙은 클래스명이 빈이름이 되고(맨앞글자는 소문자로 바뀌어서) 그 클래스의 객체가 빈 객체가 된다.

컴포넌트스캔을 안쓰고 AppConfig에서 직접 @Bean을 이용해서 등록해줄때는 메소드명이 빈이름이 됬고

반환되는값(구현체)가 빈객체가 됬었다.

물론 빈이름을 직접설정해줄 수 있다.

@Autowired 동작방식

타입(클래스타입)이 같은 빈을 찾아서 주입.

 

탐색위치와 기본 스캔 대상

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

 

모든 자바 클래스를 컴포넌트 스캔하면 시간이 오래걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작위치를 지정할 수 있다. 

@Configuration // 설정정보 파일이니까 @Configuration 어노테이션을 필수다.
@ComponentScan(
    excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class),
    basePackages = "com.example.demo"
) 
public class AutoAppConfig {

}

basePackages = "찾기 시작할 시작위치"로 지정가능하다.

 

만약

basePackages = "com.example.demo.member"

로 수정했다면 member 패키지 아래있는것들만 스캔이 되서 등록이 될것이다.

테스트해보자.

기본적으로 등록되는 구성설정파일과, 멤버 밑에 멤버서비스, 메모리멤버리포지토리가 등록되었다.

이렇게 탐색위치를 지정해주지않으면 모든 클래스파일을 찾아볼것이다 (라이브러리까지도) , 그런 작업은 시간이 오래 소요된다.

 

탐색할 패키지의 시작위치를 지정하지않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작위치가 된다.

그러므로 설정정보(구성정보)클래스  ex) AppConfig.java  AutoAppConfig.java 

를 프로젝트 최상단에 위치시키는것이다. 

package com.example.demo; // AutoAppConfig가 속한 패키지 위치


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),
    basePackages = "com.example.demo.member"
)
public class AutoAppConfig {
}

프로젝트 최상위위치에 ~Application이라고 있을것이다.(자동으로 거기 생성됨) 이름은 내가 지정한 이름이 앞에 붙는다.

(== 스프링부트를 실행시키는 클래스)

내용을 보면 @SpringBootApplication이 있고 @SpringBootApplication를 클릭해서 들어가보면 

컴포넌트 스캔이 존재한다.

즉 스프링 부트를 쓰면 내 프로젝트 위치부터 다 컴포넌트스캔을 하겠다는것이다.

(스프링부트를 쓰면 사실 @ComponentScan을 쓸 필요가없다고한다.  ~Application 클래스에서 스프링을 부팅하면서 다 스캔을 해주니까)

 

 

컴포넌트 스캔 기본대상

다음과 같이 Service, Controller 등 내부에 @Component가 존재하기 때문에 컴포넌트 스캔 대상이다.

 

컴포넌트 스캔 기본대상 설명

 

 

필터

테스트 해보기전에 임의로 어노테이션을 만든다.

new java에서 어노테이션을 선택해준다.

 

package com.example.demo.scan.filter;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
//위의 3가지 어노테이션은 @Component 내부의 어노테이션을 복사붙여넣었다.
public @interface MyIncludeComponent { //임의로 만든 어노테이션,이 어노테이션이 붙은 애들만 컴포넌트스캔할것이다.

}

@MyIncludeComponent 해당 어노테이션이 있으면 컴포넌트 스캔을한다.

package com.example.demo.scan.filter;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyexcludeComponent { //임의로 만든 어노테이션,이 어노테이션이 붙은 애들은 스캔에 제외한다.

}

 

@MyexcludeComponent  해당 어노테이션이 있으면 스캔에서 제외한다.

 

 

빈등록할 임의의 클래스를 만든다.

BeanA.class

package com.example.demo.scan.filter;

@MyIncludeComponent
public class BeanA {

}

@MyIncludeComponent이 있으므로 스캔이 되야한다.

package com.example.demo.scan.filter;

@MyexcludeComponent
public class BeanB {

}

@MyexcludeComponent이 있으므로 스캔이 되면 안된다.

 

테스트코드

public class ComponentFilterAppConfigTest {
    @Test
    void filterScan(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(
            ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA", BeanA.class); // 클래스명이 빈 이름인데 , 첫글자는 소문자가 된다. 그래서 빈 이름은 beanA
        assertThat(beanA).isNotNull(); // beanA는 스캔대상이므로 null이면 안된다.

        assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("beanB", BeanB.class)); //beanB를 가져오려고하면 예외가 발생한다.

    }


    @Configuration
    @ComponentScan(
        includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class), //스캔에 포함 설정
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyexcludeComponent.class) // 스캔 제외 설정
    )
    static class ComponentFilterAppConfig { //테스트용 구성설정 클래스이다, 구성설정 클래스이므로 @Configuration붙여줘야하고
        //스캔을 테스트할거니까 @ComponentScan 붙여준다.
    }
}
type = FilterType.ANNOTATION,

부분은 기본값이라 생략해도 돌아간다.

 

 

중복 등록과 충돌

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

 

두가지 경우가있다.

1. 수동 빈등록 vs 수동 빈등록     서로 충돌

2. 자동 빈등록 vs 수동 빈등록     서로 충돌

 

예외발생하므로 쉽게 해결가능

 

수동 빈 등록 -> @Bean을 이용해서 빈 등록 메소드를 만든다 (수동으로 빈 생성,의존관계 생성[전달인자 적어야하니까])

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

충돌을 만들기위해 AutoAppConfig에 빈등록을 해준다. 이름은 memory~로 해준다. 

컴포넌트스캔으로 만들어지는 빈이름은 클래스이름인데 맨앞글자가 소문자이기 때문이다.

 

그렇게 해놓고 테스트를 해보면 오류없이 정상동작한다. 로그에서는 같은 이름이 빈이 등록되어서 오버라이드 됬다고 나온다.

자동 빈등록 vs 수동 빈등록 해서 같은 이름의 빈이 2개 있을경우 수동 빈이 오버라이딩 해버린다.
최근에는 스프링부트 실행시 오류가 발생하도록 바뀌었다고 한다.

 

 

@SpringBootApplication
public class DemoApplication {

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

}

스프링부트 실행 클래스를 실행시 등록된 빈중 중복된 이름을 가진 빈이 있다면 아예 오류메시지를 띄우면서 스프링이 안올라간다.

 

application.properties에 오버라이딩 설정 코드를 추가해주면 수동빈으로 오버라이딩 시켜줘서 스프링이 잘 올라간다.

 

 

 

 

 

댓글