본문 바로가기
Back-end/java spring

[Spring] 컴포넌트 스캔

by kkkdh 2023. 1. 3.
728x90

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

  • 지금까지 스프링 빈을 등록할 때에는 자바 코드@Bean이나 XML<bean> 등을 통해 설정 정보에 직접 등록할 스프링 빈을 나열했다.
  • 예제에서는 몇 개가 안되었지만, 실제 구현 상황에서는 이렇게 등록해야 할 스프링 빈이 수십, 수백 개가 되면 일일이 등록하기 귀찮고, 설정 정보가 커지며 누락하는 상황도 발생
  • 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
  • 또 의존 관계를 자동으로 주입해 주는 @Autowired라는 기능도 제공한다.

 

코드를 통해 컴포넌트 스캔 + 의존관계 자동 주입을 알아보자.

 

먼저 기존의 AppConfig.java를 남겨두고, 새로운 코드인 AutoAppConfig.java를 만든다.

package hello.core;

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

import static org.springframework.context.annotation.ComponentScan.*;

@Configuration
@ComponentScan(
        excludeFilters = @Filter(type=FilterType.ANNOTATION, classes=Configuration.class)
)
public class AutoAppConfig {

}
  • 컴포넌트 스캔을 사용하려면, 먼저 @ComponentScan 어노테이션을 설정 정보에 붙여주면 된다.
  • 기존의 AppConfig와는 다르게 @Bean 어노테이션을 이용해 직접 스프링 빈으로 등록한 클래스가 하나도 없다.
참고: 컴포넌트 스캔을 사용하려면, @Configuration이 붙은 설정 정보 또한 자동으로 등록되기 때문에(@Configuration의 코드를 보면, @Component 어노테이션이 같이 붙어있다.), 앞서 만든 여러 가지 설정 정보가 같이 등록되고, 실행되어 버린다. 이렇게 되면, AutoAppConfig만을 이용한 설정 정보 등록이 확인인 안 되기 때문에, excludeFilters를 이용해 설정 정보들을 컴포넌트 스캔 범위에서 제외한 것이다.

 

컴포넌트 스캔은 이름 그대로 @Component 어노테이션이 붙은 클래스를 모두 스캔해서 스프링 빈으로 등록하는 작업을 수행한다.

 

이제 스프링 빈으로 등록해 스프링 컨테이너에서 관리할 클래스에 @Component 어노테이션을 모두 붙여줘야 한다.

구체를 구현한 MemoryMemberRepository, RateDiscountPolicy 클래스에 @Component를 작성
구체를 구현한 MemberServiceImpl에 @Component 어노테이션 부착 + 자동 의존관계 주입을 위해 생성자에 @Autowired 부착

이전에 직접 구성 정보를 코드로 작성한 경우(AppConfig이용)에는 @Bean을 이용해 직접 설정 정보를 작성했었고, 그렇기 때문에 의존관계 또한 직접 주입해 줬다.

 

하지만, 이제는 AutoAppConfig를 활용하며, 이것이 불가능하기 때문에 의존관계 주입 또한 클래스 안에서 해결해야 한다.

 

이를 위해서 @Autowired라는 어노테이션을 활용하는데, @Autowired의 자세한 룰은 조금 뒤에 정리해 보자.

 

이어서 OrderServiceImpl에도 어노테이션들을 작성해준다.

위 코드를 보면 알 수 있듯이, @Autowired 한 개를 이용해 여러 의존관계를 한 번에 주입받을 수도 있다.

 

이렇게 설정 정보를 등록해도 테스트 실행 결과 기존의 AppConfig 활용의 경우와 동일한 작업을 수행할 수 있음을 확인할 수 있다.

 

@ComponentScan

  • @ComponentScan은 @Component 어노테이션이 붙은 모든 클래스를 스프링 빈에 등록
  • 이때, 스프링 빈의 기본 이름은 클래스명을 그대로 따르되, 맨 앞글자만 소문자로 변환해서 사용
    • 빈 이름 기본 전략 예시: MemberServiceImpl -> memberServiceImpl
    • 빈 이름 직접 지정 예시: 스프링 빈의 이름을 직접 지정하고 싶은 경우 @Component("memberService2") 이렇게 지정할 수 있다.

@Autowired 의존관계 자동 주입

  • 생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 등록
  • 이때, 기본 조회 전략은 타입이 같은 빈을 찾아서 주입하는 것이다.
    • getBean(MemberRepository.class)와 같은 구조 -> 즉, 부모 타입으로 조회해도 자식을 모두 조회 가능하다.
    • 더 자세한 내용은 뒤에서

OrderServiceImpl에서 처럼 의존관계가 많아도 @Autowired만 붙이면, 스프링 컨테이너에서 다 찾아 자동으로 주입해준다고 한다.


탐색 위치와 기본 스캔 대상

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

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

@ComponentScan(
	basePackages = "hello.core",
)
  • basePackages: 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함한 하위 패키지를 모두 탐색한다.
    • basePackages = {"hello.core", "hello.service"} 이렇게 여러 시작 위치를 지정할 수도 있다.
  • basePackagesClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.
  • 만약 지정하지 않으면, @ComponentScan이 붙은 설정 정보 클래스의 패키기가 탐색의 시작 위치가 된다.

권장하는 방법!!

김영한 님이 개인적으로 즐겨 사용하는 방법은 패키지 위치를 따로 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트의 최상단에 두어 최상단부터 탐색하도록 설정하는 것이라고 한다. 최근 스프링 부트도 이 방식을 지원한다고 한다.

 

예를 들어 다음과 같은 프로젝트 구조가 있을 때

  • com.hello
  • com.hello.service
  • com.hello.repository

com.hello: 여기에 @ComponentScan이 붙은 설정 정보를 두고, basePackages 속성 설정을 생략하면 된다.

 

참고: 프로젝트의 메인 설정 정보는 프로젝트를 대표하는 정보이므로 프로젝트의 시작 위치에 두는 것이 좋다고 한다.

그리고 스프링 부트를 사용하면, 스프링 부트의 대표적인 시작 정보인 @SpringBootApplication를 이 프로젝트 시작 루트 위치에 두는 것이 관례이다. (바로 여기 안에 @ComponentScan이 들어있다)

 

그래서 사실 스프링 부트를 사용하는 경우 @ComponentScan을 사용하지 않아도 컴포넌트 스캔이 최상단에서부터 동작한다고 함.

 

컴포너트 스캔 기본 대상

@Component 어노테이션이 붙은 클래스 말고 다음과 같은 어노테이션을 대상으로도 컴포넌트 스캔이 이루어진다.

  • @Component: 컴포넌트 스캔에서 사용
  • @Controller: 스프링 MVC 컨트롤러에서 사용
  • @Service: 스프링 비즈니스 로직에서 사용
  • @Repository: 스프링 데이터 접근 계층에서 사용
  • @Configuration: 스프링 설정 정보에서 사용

각 어노테이션의 코드를 보면, @Component 어노테이션이 위와 같이 붙어있음

참고: 사실 어노테이션 자체적으로는 상속 관계 같은 것이 없다. 그래서 이렇게 어노테이션이 특정 어노테이션을 들고 있는 것을 인지하는 것은 스프링이 지원하는 기술이다.

컴포넌트 스캔의 용도뿐만 아니라 다음 어노테이션이 있는 경우 스프링은 다음과 같은 부가 기능을 수행한다.

  • @Controller: 스프링 MVC 컨트롤러로 인식한다.
  • @Repository: 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링의 예외로 변환해 준다.
  • @Configuration: 앞서 보았듯 스프링 구성 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가적인 처리를 진행해 준다.
  • @Service: 사실 별다른 기능을 하지는 않는다고 한다. 다만, 개발자들이 핵심 비즈니스 로직이 여기에 있음을 인식하기 위해서 사용한다고 함

참고로 userDefaultFilters라는 옵션은 디폴트로 켜져 있다고 하는데, 이 옵션은 끄면 기본 스캔 대상들이 제외된다고 한다. 그냥 알고만 넘어가자..


필터

필터를 이용해 컴포넌트 스캔의 대상으로 추가할 대상과 제외할 대상을 구분하기 위한 어노테이션을 추가해 보자.

두 개의 어노테이션을 위와 같이 만든다. 코드가 의미하는 바는 개인적으로 공부가 필요할 것 같다.

컴포넌트 스캔 대상에 추가 또는 제외할 어노테이션을 다음과 같이 ComponentScan 어노테이션의 속성에 작성해서 컴포넌트 스캔 대상을 직접 지정해줄 수 있다.

앞서 정의한 MyIncludeComponent는 컴포넌트 스캔 대상에 추가하고, MyExcludeComponent는 제외하도록 스캔 대상을 설정

참고로 FilterType 옵션에는 5가지가 존재한다.

  • ANNOTATION: 기본값, 어노테이션을 인식해서 동작한다.
    • ex) org.example.SomeAnnotation
  • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
    • ex) org.example.SomeClass
  • ASPECTJ: AspectJ 패턴 사용
    • ex) org.example..*Service+
  • REGEX: 정규 표현식 사용
    • ex) org\.example\.Default.*
  • CUSTOM: TypeFilter라는 인터페이스를 구현해서 처리한다.
    • ex) org.example.MyTypeFilter
참고: @Component면 충분해서, includeFilters를 사용할 일은 거의 없다고 한다. exlucdeFilters는 여러 가지 이유로 간혹 사용한다고 함.
특히 최근 스프링 부트는 컴포넌트 스캔을 기본적으로 제공하기 때문에, 강사님 개인적으로는 옵션을 변경하면서 사용하는 것보다는 스프링의 기본 설정에 최대한 맞추는 방식을 권장하고 선호한다고 하심

중복 등록과 충돌

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

 

다음 두 가지 상황이 있다.

  1. 자동 빈 등록 vs 자동 빈 등록, ComponentScan vs ComponentScan
  2. 수동 빈 등록 vs 자동 빈 등록, 수동 등록 vs ComponentScan

첫 번째 상황 자동 등록 vs 자동 등록

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 이름이 같은 빈이 중복해서 등장하는 경우 스프링은 오류를 발생시킨다.

ConflictBeanDefinitionException 예외가 발생

위처럼 같은 이름의 빈이 자동으로 등록되는 경우 스프링은 오류를 발생시킨다.

 

두 번째 상황 수동 등록 vs 자동 등록

이러한 경우 기본적으로 수동 빈 등록이 우선권을 가진다고 한다. (수동 빈이 자동 빈을 오버라이딩 하는 구조)

위와 같이 수동 빈이 자동 빈을 오버라이딩한다는 문구와 함께 성공하게 되는데

 

물론 개발자가 이러한 결과를 의도했다면, 자동보다 수동이 우선권을 가지는 것은 문제가 될 것이 없지만, 현실적으로 이러한 경우는 거의 개발자가 의도적으로 설정해서 만들어지기보다 여러 설정들이 꼬여서 만들어지는 것이 대부분이라고 한다!

 

이렇게 되면, 정말 잡기 어려운 버그가 만들어지고, 항상 잡기 어려운 버그는 애매한 버그이다!!

 

그래서 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌 나면 오류가 발생하도록 기본 값을 바꾸었다고 한다.

이미 등록되있는 이름과 같은 빈을 수동으로 등록해보자.
실행 결과 이렇게 에러가 발생한다.

728x90

댓글