jhhan의 블로그

스프링 코어(9) - 컴포넌트 스캔_2 본문

Spring

스프링 코어(9) - 컴포넌트 스캔_2

jhhan000 2021. 5. 13. 23:46

저번 포스트에 이어서 컴포넌트 스캔에 대해서 계속 알아보겠습니다.

 

이번엔 필터에 대해 더 알아보죠.

필터의 종류로 2개가 있습니다.

  • includeFilters: 컴포넌트 스캔 대상에 추가함
  • excludeFilters: 컴포넌트 스캔 대상에서 제외함

단어만으로 바로 예측할 수 있죠.

 

그러면 이번엔 어노테이션도 만들어보면서 확인해보겠습니다.

전에 만들어놨던 scan 패키지 안에 filter 패키지를 따로 만들고 진행합니다.

2개의 어노테이션은 각각 어노테이션 클래스로 진행합니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
    /**
     * 이 어노테이션이 사용되면
     * 컴포넌트 스캔에 추가되는 의미 부여
     */
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
    /**
     * 이 어노테이션이 사용되면
     * 컴포넌트 스캔에서 제외된다는 의미
     */
}

이렇게 2개의 어노테이션 클래스를 만듭니다.

그리고 새롭게 만든 어노테이션을 적용할 클래스도 만들어보겠습니다.

@MyIncludeComponent
public class ExampleBeanA {
    
}
@MyExcludeComponent
public class ExampleBeanB {

}

이렇게 만들었습니다.

그러면 이제 테스트 코드를 통해 검증해보겠습니다.

package hello.core.scan.filter;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

public class ComponentFilterAppConfigTest {

    @Configuration
    @ComponentScan(
            includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig {

    }

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

        // 여기는 테스트 성공인 것을 확인할 수 있음
        ExampleBeanA exampleBeanA = ac.getBean("exampleBeanA", ExampleBeanA.class);
        System.out.println("exampleBeanA = " + exampleBeanA);
        assertThat(exampleBeanA).isNotNull();

//        여기부터 테스트 실패가 됨
//        excludeFilters로 컴포넌트 스캔이 되지 않았기 때문에 Exception이 발생한다.
        ExampleBeanB exampleBeanB = ac.getBean("exampleBeanB", ExampleBeanB.class);
    }
}

컴포넌트 스캔을 통해서 추가할 클래스와 제외할 클래스를 설정합니다.

그리고 filterScan() 메서드로 확인을 합니다.

  • ExampleBeanA exampleBeanA : 테스트가 성공인 것을 알 수 있음
  • ExampleBeanB exampleBeanB : 테스트가 실패함
  • 이유: excludeFilters 필터로 인해 컴포넌트 스캔이 되지 않았기 때문

이런 Exception이 발생하는 것을 알 수 있습니다.

그러면 테스트 코드를 잘 마무리 할 수 있게 코드를 다시 짜봅니다.

import static org.junit.jupiter.api.Assertions.*;

...
        
        assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("exampleBeanB", ExampleBeanB.class)
        );
//        ExampleBeanB는 검색할 수 없으므로 예외가 발생된다는 테스트 구문을 넣어줘야 함.
  • 테스트 코드가 성공할 수 있도록 변경한 코드
  • assertThrows 메서드는 org.junit.jupiter.api.Assertions.*;에 있음
  • NoSuchBeanDefinitionException이라는 Exception클래스를 잡게 함
  • exampleBeanB는 검색할 수 없어 예외가 발생된다는 테스트 구문인 것임

이렇게 한다면 테스트 코드가 성공합니다.

 

그리고 더! 

FilterType에 대해서 잠깐 알아보겠습니다.

  1. ANNOTATION: 기본값. 어노테이션을 인식해서 동작아래와 같은 코드로 해도 똑같이 동작합니다.
  2. @Configuration @ComponentScan( // includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class), // excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class) includeFilters = @ComponentScan.Filter(classes = MyIncludeComponent.class), excludeFilters = @ComponentScan.Filter(classes = MyExcludeComponent.class) ) static class ComponentFilterAppConfig { }​
  3. ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작
  4. ASPECTJ: AspectJ 패턴 사용
               ex) org.example..&Service+
  5. REGEX: 정규식 사용
               ex) org\.example\.Default.*
  6. CUSTOM: TypeFilter라는 인터페이스 구현 후 사용
               ex) org.example.MyTypeFilter

예를 들어 다음과 같이 작성할 수도 있습니다.

@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)
  }
)

이렇게 사용한다면 ExampleBeanA도 제외할 수 있습니다.

 

[

참고: @Component만 사용해도 ComponentScan 하는데에는 큰 문제가 없습니다.

        그래서 includeFilters를 사용할 일이 거의 없을 것입니다. 

        excludeFilters는 사용할 때도 있지만, 많이 사용하지는 않는다고 합니다.

]

 

 

필터에 대해서는 이정도까지 알아보고 다음으로 넘어갑니다.

 

컴포넌트 스캔을 통해 중복 등록이 될 수 도 있습니다.

과연 어떤 경우일까요..

  1. 자동 빈 등록 & 자동 빈 등록
  2. 자동 빈 등록 & 수동 빈 등록

2가지가 있습니다.

 

1번의 경우에는 컴포넌트 스캔을 통해 자동으로 빈 등록을 하는데 이 때 이름이 같으면 발생하는 문제입니다.

한번 예시를 보겠습니다.

MemberServiceImpl.java
OrderServiceImpl.java

만약 MemberServiceImpl과 OrderServiceImpl의 이름을 Service라고 등록해버리는 경우..

아마 중복 이름으로 등록된다는 오류가 발생할 것입니다.

이전에 작성한 AutoAppConfigTest를 통해 확인합니다.

이런 오류가 발생합니다.

그리고 오류 내용은 → 같은 이름으로 빈을 등록해서 오류가 발생했다고 적혀있습니다.

 

너무 작위적인 예시이긴 하지만

어쨌든 같은 이름으로 빈 등록을 하지만 않으면 일어나지 않을 오류입니다. ㅎ

 

2번의 경우가 아마 실무에서 좀 더 많이 발생할 듯 싶습니다.

컴포넌트 스캔을 통해 자동으로 빈 등록을 한 후

혹시 까먹어서 동일한 빈을 다시 등록할 수도 있습니다.

이것도 예시를 통해 알아보죠.

AutoAppConfig.java로 알아봅니다.

기존에 MemoryMemberRepository는 컴포넌트 스캔을 통해 자동 등록이 됩니다.

이렇게 등록이 됩니다.

근데 까먹고 다시 직접 빈 등록을 할 수도 있습니다.

그럼 오류가 발생할까요?

역시 AutoAppConfigTest을 통해 알아보면 됩니다.

돌려보면... 테스트 코드가 통과하는 것을 알 수 있습니다..!

자동 빈 등록과 수동 빈 등록 사이에는 충돌이 발생하지 않는 걸까요?

로그를 통해 확인해 봅니다.

원래는 오류가 나는 것이 정상이지만

스프링 시스템 자체적으로 Overriding 처리를 한 것을 확인할 수 있습니다.

 

??? : 그럼 수동 빈 등록을 해도 상관이 없겠네요!

...

 

사실 제가 든 예시는 굉장히 작위적입니다. ㅎ

MemoryMemberRepository가 등록이 된 것을 알고 일부러 다시 등록했기 때문입니다.

과연 실무에서 이럴 일이 얼마나 있을까요...

 

현실적으로 생각한다면 

자동등록이 된 줄 모르고 수동으로 다시 등록했을 경우가 훨씬 많을 것입니다.

그럼 이렇게 오버라이딩이 되면 좋을까요?

좋을 수도 있지만, 나쁠 수도 있을 것입니다.

나쁜 경우라면, 에러가 발생했는데도 왜 발생했는지 알 수 없습니다.

왜냐하면 테스트 코드 상에서는 문제가 없으니까요.

이런 버그는 찾기가 참 힘들 것 같습니다...

 

이런 문제가 있기 때문에 스프링에서는 자동 빈 등록과 수동 빈 등록에 충돌이 생길 경우

오류가 발생하도록 기본값을 변경했다고 합니다.

테스트 코드가 아닌 실제 코드를 돌려보겠습니다.

CoreApplication을 열어서 실행해봅니다.

바로 에러가 나오는 것을 확인할 수 있습니다.

이미 등록된 빈이 있다고 로그 창에서 알려주고 있네요.

 

만약 이런 에러는 그냥 넘기고 싶다면 옵션을 추가하면 됩니다.

application.properties 파일에 다음의 옵션을 추가합니다.

spring.main.allow-bean-definition-overriding=true

// application.properties 에 작성

그리고 다시 CoreApplication을 실행해 봅니다.

정상적으로 작동하는 것을 확인할 수 있습니다.

 

 

 

이렇게 해서 2개의 포스트를 통해 컴포넌트 스캔에 대해 알아보았고,

특히 이번에는 필터와 중복 등록에 대해 알아봤습니다.

 

컴포넌트 스캔은 스프링 자체적으로 설정이 잘 되어있어서

이정도만 익혀도 구현하는데 큰 문제는 없을 것 같습니다.

 

 

이렇게 포스트를 마칩니다.

 

 

 

 

 

출처: 인프런 - 스프링 핵심원리(기본편) by 김영한