[SpringBoot] [3] 6. 컴포넌트 스캔

윤경·2021년 8월 23일
0

Spring Boot

목록 보기
28/79
post-thumbnail

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

지금까지는 @Bean 또는 XML <Bean\>으로 설정 정보에 직접 등록할 스프링 빈을 나열해왔다.

우리는 아직 등록할 빈이 많지 않았지만 나중에 큰 프로젝트를 할 때 등록할 빈이 수백개라면 어떻게 다 등록하고 관리할까,,? 그리고 일단 귀찮지 않나.

그런데 스프링은 항상 우리에게 실망을 주지 않는다.

설정 정보 없이 자동으로 스프링 빈을 등록할 수 있게 컴포넌트 스캔이라는 기능과 @Autowired를 제공한다.

✔️ hello.core 밑에 AutoConfig.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 {
// AppConfig와 다르게 @Bean으로 등록한 클래스가 하나도 없음.
}

➡️ 컴포넌트 스캔을 사용하기 위해서는 @ComponentScan을 설정 정보에 붙여주자.

📌 (참고): 컴포넌트 스캔을 사용하면 @Configuration이 붙은 설정 정보도 자동 등록되기 때문에 AppConfig, TestConfig 등 앞서 만들었던 설정 정보도 함께 등록, 실행돼 버린다. 그래서 excludeFilters를 이용해 설정 정보를 컴포넌트 스캔 대상에서 제외해버렸다.

보통은 설정 정보를 컴포넌트 스캔 대상에서 제외하지 않지만 우리는 기존 예제 코드를 최대한 유지하기 위해 이렇게 함.

이제 각 클래스가 컴포넌트 스캔 대상이 되도록 @Component 애노테이션을 붙여줄 차례.
( 컴포넌트 스캔은 이름 그대로 @Component 애노테이션이 붙은 클래스를 스캔해 스프링 빈으로 등록 )

📌 (참고): @Configuration이 컴포넌트 스캔 대상이 된 이유는 @Configuration소스 코드를 열어보면 @Component애노테이션이 붙어있기 때문

✔️ MemoryMemberRepository.java

@Component
public class MemoryMemberReposiotry implements MemberRepository {

✔️ RateDiscountPolicy.java

@Component
public class RateDiscountPolicy implements DiscountPolicy {

✔️ MemberServiceImpl.java (@component@Autowired)

@Component
public class MemberServiceImpl implements MemberService {
@Autowired	// 의존관계 주입을 위함
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

➡️ @Autowired는 ac.getBean(MemberRepository.class) 처럼 동작한다고 생각

😺: @Autowired는 왜 쓰는거야?

예전 AppConfig에서는 @Bean으로 직접 설정 정보를 작성하고 의존관계를 명시했으나 지금 하는 방법은 설정정보 자체가 없어 의존관계 주입이 불가능해 이 클래스 안에서 해결해야 한다.

그래서 @Autowired는 의존관계를 자동으로 주입해주는 역할을 한다. (MemberRepository.class라는 타입을 찾아 의존관계로 설정)

✔️ OrderServiceImpl.java (@Component, @Autowired)

@Component
public class OrderServiceImpl implements OrderService {
```java
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {

➡️ 의존관계가 하나 이상이어도 한 번에 주입 가능 (@Autowired)

이제 테스트해보자.

✔️ test/scan 패키지 생성 AutoAppConfigTest.java

package hello.core.scan;

import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
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);
    }
}

➡️ AnnotationConfigApplicationContext 사용은 동일하고
설정 정보로 AutoAppConfig를 넘겨준다.

1. @ComponentScan

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


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

시작 위치 지정

스캔할 때 탐색할 패키지의 시작 위치를 지정할 수 있다.

(사실 굳이 설정하지 않아도 스프링부트가 모든 패키지를 스캔하도록 해주지만)
모든 클래스를 컴포넌트 스캔하려면 시간이 너무 오래 걸린다. 그래서 시작 위치를 지정하는 것.

✔️ AutoAppConfig.java

@ComponentScan(
        basePackages = "hello.core",       
        // 컴포넌트 스캔으로 쫙 스캔해서 스프링 빈으로 등록할 때 뺄 것을 지정해줌.
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)

이렇게 basePackages를 추가하면 된다.

basePackages: 탐색할 패키지의 시작 위치 지정. 이 패키지부터 하위 패키지 모두 탐색.
basePackages = {"hello.core", "hello.service"} 이렇게 여러개를 설정할 수도 있음.

basePackagesClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정. (코드 가장 위에 package 하고 적힌 부분)

혹시 지정하지 않는다면 @ComponentScan이 붙은 설정 정보 클래스 패키지부터 탐색하게 됨.

📌 (권장)
패키지 위치를 직접 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두기. 최근 스프링 부트의 default.

즉, 우리 프로젝트에서 hello.core 이게 프로젝트 시작 루트이므로 여기 AppConfig와 같은 메인 설정 정보@ComponentScan 애노테이션 붙이기. (이때, basePackages는 생략)
→ 이는 hello.core 밑 하위 패키지는 모두 컴포넌트 스캔의 대상이 되어 탐색된다.
(프로젝트 메인 설정 정보(AppConfig)는 프로젝트 대표 정보이기 때문에 시작 루트 위치에 두는 것이 좋음.)

📌 (참고)
스프링 부트를 사용하면 @SpringBootApplication(스프링 부트 대표 시작 정보)를 프로젝트 시작 루트에 두는 것이 관례.
(이 설정 안에 @ComponentScan이 있기 때문)

기본 스캔 대상

@Component: 컴포넌트 스캔에서 사용

@Controller: 스프링 MVC 컨트롤러에서 사용

@Service: 스프링 비즈니스 로직에서 사용

@Repository: 스프링 데이터 접근 계층에서 사용

@Configuration: 스프링 설정 정보에서 사용

📌 (참고)
사실 애노테이션은 상속관계라는 개념이 없다.
애노테이션이 특정 애노테이션 되어있는 것을 인식할 수 있는 이유는 스프링이 이 기능을 제공하기 때문이다. 자바 언어 자체는 못 한 다 !!

컴포넌트 스캔뿐만 아니라 다음 애노테이션은 스프링에서 부가 기능을 수행한다.

@Controller: 스프링 MVC 컨트롤러로 인식

@Repository: 스프링 데이터 접근 계층으로 인식. 데이터 계층의 예외를 스프링(의 추상화 된) 예외로 변환해줌. (예를 들어, 특정 예외가 터졌는데 나중에 DB를 교체하게 됐다? 그럼 예외 자체가 변해버림. 그런 일이 없도록 스프링이 예외를 추상화해서 변환해준다는 것.)

@Configuration: 스프링 설정 정보로 인식, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.

@Service: 👥: 핵심 비즈니스 로직이 여깄구나
(사실 이건 별 처리를 하지 않고 개발자들이 비즈니스 계층을 인식하는데 도움을 줌.)

(몰라도 되는 참고) useDefaultFilters 옵션을 끄면 기분 스캔 대상들이 제외됨.


3️⃣ 필터

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

이제 예제를 보면서 알아보도록 하자.

✔️ test/scan/filter 패키지 생성 후 MyIncludeComponent.java ‼️annotation‼️ 생성 ➡️ 추가할 대상 지정

package hello.core.scan.filter;

import java.lang.annotation.*;

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

✔️ filter 패키지 MyExcludeComponent.java ‼️annotation‼️ 생성 ➡️ 제외할 대상 지정

package hello.core.scan.filter;

import java.lang.annotation.*;

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

✔️ filter 패키지 Bean!.java (추가)

package hello.core.scan.filter;

@MyIncludeComponent
public class BeanA {
}

✔️ filter 패키지 BeanB.java (제외)

package hello.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}

✔️ filter 패키지 ComponentFilterAppConfigTest.java (test)

package hello.core.scan.filter;

import org.junit.jupiter.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.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Component;

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

public class ComponentFilterAppConfigTest {

    @Test
    void filterScan() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA", BeanA.class);
        assertThat(beanA).isNotNull();

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

    @Configuration
    // @ComponentScan(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class));
    // 19줄을 아래의 22줄처럼 static으로 바꿀 수 있음. 바꿔주기 (option + enter)
    @ComponentScan(
            includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig {

    }
}

⬇️ static import 할 수 있으니까 해주기.
⬇️ 결과 BeanAincludeFiltersMyIncludeComponent annotation을 추가했기 때문에 스프링 빈에 등록 되었지만 (→ isNotNull()통과)
BeanBexcludeFiltersMyExcludeComponent annotation을 추가해 스프링 빈에 등록되지 않았다.

📌 FilterType의 5가지 옵션

ANNOTATION: 기본값, annotation 인식해 동작 (ex. org.example.SomeAnnotation)

ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해 동작 (ex. org.example.SomeClass → 클래스로 지정한 것)

ASPECTJ: AspectJ 패턴 사용 (ex. org.example..*Service+)
( ✔️ AspectJ: PARC에서 개발한 자바 프로그래밍 언어용 관점 지향 프로그래밍(AOP) 확장 기능 )

REGEX: 정규 표현식 (ex. org.example.Defualt.*)

CUSTOM: TypeFilter 인터페이스를 구현해 처리 (ex. org.example.MyTypeFilter)

📌 사실, @Component만 있어도 충분하기 때문에 includeFilters는 거의 사용하지 않는다. (다만, excludeFilters는 여러 이유로 간혹 사용할 때가 있긴 하다.)

특히 최근 스프링 부트는 컴포넌트 스캔을 기본으로 제공하는데 개인적으로 옵션을 변경하며 사용하기 보단 스프링 기본 설정에 최대한 맞춰 사용을 권장, 선호하는 바이다.


4️⃣ 중복 등록과 충돌

😺: 컴포넌트 스캔에서 같은 빈 이름을 등록한다면?

경우 1) 자동 빈 등록(컴포넌트 스캔) VS 자동 빈 등록(컴포넌트 스캔)

자동 빈 등록의 경우에는 컴포넌트 스캔에 의해 등록되는 경우를 말한다.
이름이 같을 때 스프링은 오류를 발생시켜준다.

➡️ ConflictingBeanDefinitionException 예외 발생
스프링 너 최고 >^<

경우 2) 수동 빈 등록 VS 자동 빈 등록(컴포넌트 스캔) ➡️ 많이 발생 !!

MemoryMemberRepository.java에서 이와 같이 컴포넌트 등록을 했었다.

✔️ AutoAppConfig.java에 아래와 같이 코드를 추가

public class AutoAppConfig {
    @Bean(name = "memoryMemberRepository")	// 이렇게 이름이 일치하도록 설정해봄
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

✔️ AutoAppConfigTest에서 basicScan() 실행
➡️ 성공 !! 하지만 잘 보면 .. 오버라이딩 어쩌고 ..

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

이 말은, 수동 빈이 자동 빈을 오버라이딩 해버렸다는 것 !!
(📌 수동 빈이 우선권을 가짐)

현실에서는 개발자가 의도적으로 이런 결과를 만들기보단 의도치 않게 설정이 꼬여서 이런 결과가 나와버린다.😅
(이런 버그는 잡기 정말 어렵고 시간도 많이 잡아먹는..)

그래서 최근 스프링 부트의 default 값이 자동 빈 등록수동 빈 등록이 충돌되면 오류가 발생하게끔 바뀌었다.


😺: 스프링부트야 고마워 ~~!
🌱:

profile
개발 바보 이사 중

0개의 댓글