Spring, Apache, Java

@ComponentScan

cwchoiit 2024. 5. 24. 19:51
728x90
반응형
SMALL

스프링을 사용할 때 스프링 빈으로 등록하는 가장 편리한 방법은 컴포넌트 스캔이다.

스프링 빈으로 등록하고 싶은 타입(클래스, 인터페이스)에 그저 `@Service`, `@Controller`, `@Repository`, `@Component` 이런 애노테이션을 붙이기만 하면 스프링이 자동으로 빈으로 등록해주기 때문에.

 

근데, 스프링에 용빼는 재주가 있는게 아니다. 결국 스프링이 컴포넌트 스캔을 하기 위해선 어디서부터 컴포넌트 스캔을 하고 어떤것들을 제외하고 등등의 설정을 다 해줘야한다. 그리고 그것을 이 `@ComponentScan`이라는 애노테이션으로 간단하게 할 수 있다. 그리고 이건? 컴포넌트 스캔을 위해 필수적으로 필요한 애노테이션이다. 

 

"네? 저는 스프링부트에서 저 애노테이션 안쓰고도 잘 되던데요?"

 

스프링부트에서 최초의 시작지점인 메인 클래스에 붙어있는 `@SpringBootApplication``@ComponentScan`을 가지고 있기 때문에 가능한 현상이다.

@SpringBootApplication

 

그래서 이 `@ComponentScan`이 어떤 것들을 해주는지 알아보자.

 

어디서부터 스캔할지를 알려준다.

만약, 스프링부트를 사용하지 않는다면 직접 `@ComponentScan`을 어딘가에 설정해줘야 한다. 다음 예시 코드를 보자. 

AutoAppConfig

package org.example.springcore;

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

@Configuration
@ComponentScan(
        basePackages = "org.example.springcore.member", // 컴포넌트 스캔 대상의 시작지점 지정
        basePackageClasses = AutoAppConfig.class, // 지정한 클래스가 위치한 패키지부터 하위 패키지까지 컴포넌트 스캔 대상이 된다
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class) // 컴포넌트 스캔 대상 제외
)
public class AutoAppConfig {

}

`@Configuration` 애노테이션은 스프링한테 "나를 빈으로 등록도 하고, 내가 스프링 컨테이너에 필요한 설정 파일이야."라고 말해주는 애노테이션이다. 이 애노테이션이 붙은곳을 보고 `AnnotationConfigApplicationContext`라는 애노테이션 기반의 설정 클래스가 스프링 컨테이너를 설정하는데 "아 이 녀석(@Configuration이 붙은 클래스)을 보고 스프링 빈으로 등록할 녀석들을 등록하면 되겠구나!" 라고 생각한다. 

 

근데 이 클래스를 보니 `@ComponentScan` 이라는 애노테이션이 달려있다. 

`@Component` 애노테이션이 붙은 모든 타입을 다 찾아서 이 AutoAppConfig 클래스에 빈으로 등록하라는 뜻으로 생각하면 된다.

 

근데 옵션이 있다.

  • basePackages: 컴포넌트 스캔을 시작할 패키지를 지정한다. 지정한 패키지부터 하위 패키지를 싹 돌면서 `@Component` 애노테이션이 붙은 곳을 찾는다.
  • basePackageClasses: 컴포넌트 스캔을 시작할 클래스를 지정한다. 이 클래스의 패키지부터 하위 패키지를 싹 돌면서 `@Component` 애노테이션이 붙은 곳을 찾는다.
  • excludeFilters: 컴포넌트 스캔에서 제외할 대상들을 설정한다.

근데 basePackages, basePackageClasses, excludeFilters는 전부 다 Optional이다. 심지어 스프링부트가 만들어준 @SpringBootApplication 애노테이션에도 basePackages, basePackageClasses 이들은 없다. 없으면 어떻게 되는걸까?

 

@ComponentScan 애노테이션이 붙은 클래스의 패키지부터 하위 패키지를 싹 스캔한다.

 

그래서 요새는 모두가 다 스프링 부트로 시작하기 때문에 굳이 `@ComponentScan` 애노테이션을 직접 설정하고 작업하지 않아도 알아서 다 해주니까 상관없지만 어떻게 컴포넌트 스캔이 될 수 있고 스프링부트가 우리 대신 뭘 해주는지 이해하는것은 의미가 있을 것 같았다.

 

@Service, @Controller 같은 애노테이션은 왜 스캔이 되는건가요?

`@ComponentScan``@Component` 애노테이션이 붙어 있는 곳을 싹 찾는다고 했다. 근데 @Service, @Controller, @Repository를 사용해도 전부 다 컴포넌트 스캔이 된다. 어떤 이유에서일까?

 

저 애노테이션들은 전부 다 `@Component` 애노테이션을 가지고 있기 때문이다.

@Service
@Repository
@Controller

 

전부 다 `@Component`를 가지고 있다.

 

"아아, 상속받는거네요 그럼?"

 

아니다. 애노테이션은 상속이라는 개념이 없다. 위 사진처럼 @Controller@Component를 붙인다고 상속받는것을 의미하지 않는다.

즉, 자바에서 제공하는 기능이 아니라 스프링이 우리 대신 어떤 작업을 해주는 것이다. 

 

내가 만든 애노테이션으로 빈을 등록할 수 있다.

거의 그럴일이 없는데, 내가 직접 애노테이션을 만들어서 빈으로 등록하게 설정할 수도 있다.

애노테이션을 먼저 만들어보자.

 

MyIncludeComponent

package org.example.springcore.scan.filter;

import java.lang.annotation.*;

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

 

MyExcludeComponent

package org.example.springcore.scan.filter;

import java.lang.annotation.*;

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

 

이 두개의 애노테이션을 직접 만든다. 이름만 봐도 알 수 있듯 MyIncludeComponent 이건 빈으로 등록할 애노테이션, MyExcludeComponent 이건 빈으로 등록하지 않을 애노테이션이다. 테스트 해보자.

 

ComponentFilterAppConfigTest

package org.example.springcore.scan.filter;

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 static org.assertj.core.api.Assertions.*;

public class ComponentFilterAppConfigTest {

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

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

    @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 애노테이션을 붙인다. 여기서 필터를 설정할 수 있는데 빈으로 등록시킬 필터와 빈에 제외시킬 필터를 설정할 수 있다.

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

 

이렇게 설정하면 MyExcludeComponent 애노테이션이 붙은 타입(클래스, 인터페이스)은 빈 등록에서 제외된다.

MyIncludeComponent 애노테이션이 붙은 타입(클래스, 인터페이스)은 빈 등록에 포함된다. 

그리고 참고로, 저 `type = FilterType.ANNOTATION`은 기본값이라 굳이 작성하지 않아도 된다.

이렇게 작성해도 무방하다는 소리다.

includeFilters = @ComponentScan.Filter(classes = MyIncludeComponent.class),
excludeFilters = @ComponentScan.Filter(classes = MyExcludeComponent.class)

 

이렇게 직접 애노테이션을 만들고 해당 애노테이션으로 컴포넌트 스캔을 통해 스프링 빈을 등록하게 할 수 있지만, 이런 경우는 거의 없다. 이미 스프링에서 필요한 애노테이션은 다 만들어놨기 때문에 가져다가 사용만 하면 된다. 그래도 직접 해보는것에 의의를 두자.

 

결론

결론은 스프링 부트를 모두가 사용하는 요즘엔 @ComponentScan 에 대해 생각할 필요없이 내가 컴포넌트 스캔을 하고 싶으면 대상에 @Component, @Repository, @Service, .. 이런 애노테이션을 붙여주면 된다. 

 

근데, 그 스프링부트가 우리 대신 @ComponentScan을 만들어줄 뿐이다. 

결론 = 스프링부트 짱🙌

 

728x90
반응형
LIST