이 포스트는 인프런 김영한 님의 '스프링 핵심 원리 - 기본편' 강의를 들으면서 내용을 정리한 글입니다. 

 

[메인 컨텐츠]

스프링 핵심 원리 - 기본편 대시보드 - 인프런 | 강의 (inflearn.com)

 

📅 2022-05-23 

ℹ️ 목차

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

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

3. 필터

4. 중복 등록과 충돌

 


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

컴포넌트 스캔의 필요성

지금까지는 스프링 빈을 수동으로 하나씩 등록하는 방법을 알아보았다. 
그러나 등록할 빈의 개수가 많아질수록 단순 반복 작업이 될 수 있고, 누락할 수 있다는 문제점도 생긴다. 

 

스프링에서는 자동으로 빈을 등록해주는 컴포넌트 스캔이라는 기능이 있다. 

 

컴포넌트 스캔(Component Scan)스프링 빈을 자동으로 끌어오는 기술.

 

코드로 보자.

AppConfig.java : 수동으로 스프링 빈을 등록한 클래스 파일

AutoAppConfig.java : 자동으로 빈을 등록할 클래스 파일

 

컴포넌트 스캔의 원리

@ComponentScan을 사용해서 컴포넌트 스캔을 할 수 있다. 

@ComponentScan은 @Component이 붙은 클래스를 찾아서 전부 스프링 빈으로 등록한다. 

@Configuration // 싱글톤 패턴으로 객체 생성
@ComponentScan	// 컴포넌트 스캔 역할
public class AutoAppConfig {
	// 별도의 코드 필요 X
}

 

@ComponentScan@Component 어노테이션만 스캔할까?

@Component 어노테이션은 다른 어노테이션 안에 붙어있기도 한다. 

ex) 

@Configuration 어노테이션을 타고 들어가면(원본 코드를 보면), @Component 어노테이션이 포함되어 있다. 이 경우 @Configuration이 붙은 클래스도 스프링 빈으로 등록된다. 
물론 @Configuration 말고도 @Component을 포함하는 어노테이션들은 많고, 스프링 컨테이너는 이 어노테이션들이 붙은 클래스들도 모두 스프링 빈으로 등록한다. 

 

@Component로 원하는 클래스를 스프링 빈으로 등록시키는 방법

등록하고 싶은 스프링 클래스 선언부 위에 @Component만 붙이면 된다. 

@Component
public class SpringServiceImpl {
	// code
}

⚠️인터페이스 말고, 구현 클래스의 선언부에다가 붙여야 한다.

 

지금까지 자동으로 스프링 빈을 등록해보았다. 

이제는 의존관계를 자동으로 주입해 보자. 

 

자동 의존관계 주입

@Autowired 라는 자동 의존관계 주입 어노테이션을 사용하면 된다. 

@Autowired를 @Component이 붙은 클래스의 생성자 위에다가 붙인다. 

(지금은 생성자 주입 방식을 사용해서 생성자 위에다가 붙일 건데, 다른 방법도 있다. )

@Component
public class SpringServiceImpl implements SpringService {
	
    private final SpringRepository springRepository;	// 해당 클래스가 의존하는 다른 클래스 객체
    
    @Autowired
    public SpringServiceImpl(SpringRepository springRepository) {
    	this.springRepository = springRepository;
    }
    
}

그러면 스프링 컨테이너에서, @Component 가 붙은 스프링 빈들 중 SpringRepository 타입의 빈을 찾는다.

('타입'으로 조회한다.) 

그리고 SpringServiceImpl이 호출될 때 그 찾은 스프링 빈을 매개변수로 넣어준다. 

 

⚠️

@Autowired를 쓸 때에는 반드시 @Autowired를 사용한 생성자를 가진 클래스(SpringServiceImpl)와 해당 생성자가 필요로 하는 매개변수 클래스(여기서는 SpringRepository의 구현 클래스인 SpringRepositoryImpl)에 모두 @Component 어노테이션이 붙어 있어야 한다. 

@Component
public class SpringRepositoryImpl implements SpringRepository {
	// code
}

 

 

이제 테스트를 만들어 보자.

: AutoAppConfigTest.java

 

1) 스프링 빈을 불러오기 위해서 AnnotationConfigApplicationContext 객체를 생성하자. 

2) 해당 객체 안에는 AppConfig 클래스 대신, 컴포넌트 스캔을 사용한 AutoAppConfig 클래스를 넣는다. 

3) AutoAppConfig 클래스로 SpringService 타입의 스프링 빈을 조회할 수 있는지 확인하자. 

public class AutoAppConfigTest {
	
    @Test
    void test() {
    	ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        SpringService springService = ac.getBean(SpringService.class);
        assertThat(springService).isInstanceOf(SpringService.class);
    }
    
}

 

일부 클래스를 제외하고 스프링 빈을 등록할 수도 있다. 

excludeFilters 옵션을 지정하면 된다. 

세부 옵션으로는 type, classes 등 여러 가지가 있다. 

@ComponentScan(
	excludeFilters = @ComponentScan.Filter( 
		type = FilterType.ANNOTATION, 
		classes = Service.class, 
	)
)
public class AutoAppConfig {
	// no code
}

: @ComponentScan의 대상 중, Configuration 어노테이션은 스프링 빈으로 포함시키지 않겠다는 의미. 

 

자동 주입 방식에서 빈 이름을 정하는 방법

빈의 기본 이름은 맨 앞 글자만 소문자로 바뀐 클래스 이름 이다. 

@Component
public class SpringRepositoryImpl implements SpringRepository {
	// code
}

: 빈 이름은 springRepositoryImpl 으로 등록된다. 

 

빈 이름을 지정할 수도 있다. 

@Component("지정하고_싶은_이름")

@Component("spring_repo")
public class SpringRepositoryImpl implements SpringRepository {
	// code
}

 

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

컴포넌트 스캔을 할 때, 어느 위치부터 @Component 어노테이션을 탐색할지를 지정할 수 있다. 
basePackages 옵션으로 지정할 수 있다. 

@ComponentScan(basePackages = 'hello.core.member')
public class AutoAppConfig {
	// code
}

 

: hello.core.member 디렉토리 하위에 있는 파일들에서만 @Component를 탐색한다. 

 

기본값

아무런 값도 없다면, basePackage의 기본값은 @ComponentScan이 붙은 클래스의 바로 위 패키지가 된다. 
ex)

AutoAppConfig.java 클래스의 바로 위 폴더가 hello.core이라면,
: hello.core 아래의 파일들에 한해서 컴포넌트 스캔을 한다. 

 

AutoAppConfig.class 는 hello.core 디렉토리 바로 아래에 위치한다. 
-> hello.core 패키지(디렉토리) 아래에 있는 파일들만 탐색한다. 

@SpringBootApplication의 비밀

스프링 웹 어플리케이션을 실행할 때는 일반적으로 (패키지_이름)Application 클래스의 메인 메소드를 실행한다.

(여기서는 CoreApplication.java)
이 클래스는 기본적으로 최상단 패키지(여기서는 hello.core)의 바로 아래에 위치하는데, 그 이유가 있다. 

@SpringBootApplication 안에는 @ComponentScan이 포함되어 있다. 
: 실제 웹앱을 만들 때는 스프링에서 @ComponentScan을 명시적으로 사용할 일이 거의 없다. 어차피 메인 메소드에 붙어있는 @SpringBootApplication 어노테이션 안에 포함되어 있기 때문이다!


그래서 웹앱을 실행하면, 프로젝트 폴더에 있는 모든 스프링 빈들을 자동으로 등록할 수 있었던 것이다. 
<-> 만약 @SpringBootApplication이 붙은 메인 클래스가 특정 하위 디렉토리에만 속해 있었다면, 모든 빈을 불러올 수 없었을 것이다. 

 

컴포넌트 스캔의 대상

컴포넌트 스캔에서는 @Component만 스캔하지 않는다. 다른 어노테이션도 내부에 @Component를 포함하고 있기 때문이다. 
: @Controller, @Service, @Repository, @Configuration, ...

 

@Component을 상속하는 다른 어노테이션들

 

⚠️ 사실 어노테이션은 자바에서 지원하는 기능이 아니다.
⚠️ 한 어노테이션이 다른 어노테이션을 포함하는 상속 관계도 마찬가지다. 자바 언어는 어노테이션의 상속 관계 인식을 지원하지 않는다. 

 

어노테이션의 상속관계

보통 한 클래스가 다른 클래스를 상속하면, 상속받은 클래스에서 지원하는 기능 + a 를 수행한다. 

어노테이션도 마찬가지이다. 

 

@Component는 @ComponentScan이 어떤 클래스를 스프링 빈으로 등록할지를 알려주는 역할을 한다. 

그러나 @Component와 다른 어노테이션까지 같이 상속한 다른 어노테이션은 그 외의 부가적인 기능도 함께 제공한다. 

 

1) @Controller

스프링 MVC 컨트롤러로 인식한다. 


2) @Repository

스프링에서 데이터로 접근하는 클래스로 인식한다. 
또한 데이터 계층에서 예외가 발생했을 때, 이를 스프링 예외로 변환시킨다. 
✖️ 스프링 예외로 변환시킨다 = 예외를 추상화시킨다

 

3) @Service

특별한 처리를 하지 않는다. 개발자들이 비즈니스 계층을 인식하는 데 도움이 된다. 

 

3. 필터

컴포넌트 스캔에서 추가할 대상과 제외할 대상을 설정할 수 있다. 
: includeFilters, excludeFilters 옵션 사용

 

추가할 어노테이션, 제외할 어노테이션을 만들고 필터가 작용하는지를 직접 확인하기 위해 어노테이션을 만들어 보자. 


어노테이션을 만드는 방법

1) 인터페이스를 선언하고, 인터페이스 키워드 앞에 @을 붙인다.

public @interface IncludeAnnotation {

}

 

2) 해당 인터페이스 위에다 추가로 어노테이션을 더 설정할 수 있다. 

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

}

 

@Target : 해당 어노테이션이 어느 타입에 붙을지를 지정. 

ElementType.TYPE : 클래스 단위에 지정하는 어노테이션. 
TYPE이 아닌 다른 값으로 지정하면 어노테이션이 필드나 생성자에 붙게 할 수도 있다.

(ex. @Autowired)

방금은 컴포넌트 스캔에 추가할 어노테이션을 만들었다. 이제 제외할 어노테이션을 만들어 보자. 

: 같은 방법으로 ExcludeAnnotation을 만들자. 

 

테스트하기

위에서 만든 @IncludeAnnotation, @ExcludeAnnotation을 사용해서 테스트를 해 보자. 


1. BeanInclude와 BeanExclude 클래스를 만들고, 각각 @IncludeAnnotation, @ExcludeAnnotation을 붙이자. 
2. 테스트를 만들어 보자: ComponentFilterAppConfigTest.java
목적: includeFilter, excludeFilter 옵션이 잘 작동하는지 확인.

1) 테스트 클래스 안에 static class를 만들고, 해당 클래스를 @Configuration을 붙여서 스프링 빈 정보를 담는 클래스로 선언한다. 
2) 해당 클래스를 참고한 AnnotationContextApplicationConfig 객체를 만들고, 빈을 조회한다. 
3) 이때 BeanInclude(includeFilter)를 조회하면 조회가 되어야 한다. 
4) 반면 BeanExclude(excludeFilter)를 조회하면 에러(NoSuchBeanDefinitionException)가 발생해야 한다. 

 

에러는 최대한 구체적으로 명시하는 것이 좋은 테스트이다. 
<-> Exception.class -> 별로 도움 안 됨!

4. 중복 등록과 충돌

스프링 빈을 등록하는 방법으로 수동(AppConfig 클래스에 직접 등록)과 자동 등록(@ComponentScan 이용)이 있었다. 

수동 등록에서는 당연히 같은 이름의 빈이 2개 이상 있으면 충돌이 발생했다. 
그렇다면 자동 등록에서는 어떨까?


1) 자동 vs 자동

: ConflictingBeanDefinitionException이 발생한다. 


2) 자동 vs 수동

: 오류가 나지 않는다. 
수동 등록 빈이 자동 등록 빈을 오버라이딩(덮어 씌우기) 했기 때문이다. 
⚠️ 이름이 같을 경우, 자동 등록 빈보다 수동 등록 빈이 우선권을 가진다. 

그러나 이런 설정은 대부분 개발자의 의도적인 테스트에 의해 발생하기보다는, 수많은 설정이 꼬여서 발생하는 것이 대부분이다. 

따라서 스프링은 이후 자동 등록된 빈과 수동 등록한 빈의 이름이 같을 때, 어플리케이션을 종료(disable)시키는 방향으로 업데이트 되었다. 

 

disable 말고 overriding 시키는 방법

main>java>resources>application.properties

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

: 수동과 자동 등록된 빈 2개가 이름이 같을 때 overriding이 발생한다. 

(default : False)

 

 

+ Recent posts