[Spring] IoC 컨테이너 - 클래스패스 스캐닝과 컴포넌트 관리 - 2 Spring

- 레퍼런스 : https://docs.spring.io/spring/docs/5.2.5.RELEASE/spring-framework-reference/

- 컴포넌트에서 Bean 메타데이터 정의하기
스프링 컴포넌트는 컨테이너에 메타데이터 bean 정의를 도와준다. 또한 @Configuration 어노테이션을 사용한 클래스안에서 bean 메타데이터를 정의하기 위해 @Bean 어노테이션을 사용해도 같은 효과를 볼 수 있다. 아래 예제처럼 사용하면 된다.
 1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class FactoryMethodComponent {

@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}

public void doWork() {
// Component method implementation omitted
}
}
앞의 클래스는 어플리케이션 코드(doWork() 메소드)를 가지고있는 스프링 컴포넌트이다. 또한 publicInstance() 팩토리 메소드를 를 통해 bean 정의를 하고 있다. @Bean 어노테이션은 팩토리 메소드를 식별하고, @Qualifier 어노테이션을 통해서 qualifier 값과 같이 다른 bean 정의 property 들도 식별하고 있다. 다른 메소드단에서 기술될 수 있는 어노테이션들은 @Scope, @Lazy 그리고 커스텀 qualifier 어노테이션들이 있다.
컴포넌트 초기화의 역할과 더불어 주입지점에 @Autowired나 @Inject와 함께 @Lazy 어노테이션을 사용할 수 있다. 이런 상황에서는 프록시를 뒤늦게 주입하는 결과가 나타난다.
@Bean 메소드들의 autowiring을 제공하기 위해서 앞에서 논의한것과 같이 autowired 필드와 메소드들이 제공된다. 아래 예제를 살펴보자.
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Component
public class FactoryMethodComponent {

private static int i;

@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}

// use of a custom qualifier and autowiring of method parameters
@Bean
protected TestBean protectedInstance(
@Qualifier("public") TestBean spouse,
@Value("#{privateInstance.age}") String country) {
TestBean tb = new TestBean("protectedInstance", 1);
tb.setSpouse(spouse);
tb.setCountry(country);
return tb;
}

@Bean
private TestBean privateInstance() {
return new TestBean("privateInstance", i++);
}

@Bean
@RequestScope
public TestBean requestScopedInstance() {
return new TestBean("requestScopedInstance", 3);
}
}
위 예제에서 String형 메소드 파라미터 country에 privateInstance 이름을 가진 다른 bean의 age property 값을 연결하였다. 스프링 Expression 언어 항목은 #{ <expression> } 기호를 통해 property의 값을 정의한다. @Value 어노테이션을 위해서 expression resolver(expression 글자를 해석하는 모듈 및 파서)는 expression 글자들을 해석할 때 bean 이름들을 살펴보기 위해서 미리 설정되어있다.
스프링 프레임워크 4.3 부터 현재 bean을 생성하는 주입 지점 요청에 접근하기 위해서 InjectionPoint(또는 하위 클래스인 DependencyDescriptor) 타입의 메소드 파라미터를 선언할 수 있다. 하지만 존재하는 인스턴스의 주입이 아니라, bean 인스턴스가 실제로 생성될때만 적용된다는것에 주의해야 한다. 결국 이 기능은 프로토타입 scope bean들에서 가장 효과를 발휘한다. 다른 scope 에서는 팩토리 메소드는 주어진 scope(lazy 싱글톤 bean의 생성이 발생되는 의존관계)에서 새로운 bean 인스턴스가 생성되는 주입시점만을 볼수밖에 없다. 아래는 InjectionPoint 사용예를 보여준다.
1
2
3
4
5
6
7
8
@Component
public class FactoryMethodComponent {

@Bean @Scope("prototype")
public TestBean prototypeInstance(InjectionPoint injectionPoint) {
return new TestBean("prototypeInstance for " + injectionPoint.getMember());
}
}
일반적인 형태의 스프링 컴포넌트에서 @Bean 메소드는 @Configuration 클래스 안에서의 @Bean 메소드들과는 다른방식으로 처리된다. @Component 클래스에서는 메소드의 실행과 필드를 인터셉트하기 위해 CGLIB를 사용하지 않는다. CGLIB 프록시는 @Configuration 클래스에서 메소드를 실행하거나 @Bean 메소드들의 필드가 협력 객체들에 대한 메타데이터 참조를 생성하기위한 수단이다. 이런 메소드들은 일반적인 Java 의미대로 수행된다기보다는 일반적인 생명주기 관리와 스프링 bean 프록시를 제공하기위해 컨테이너를 거치는 과정을 수행하며, 심지어 @Bean 메소드안에서 프로그래밍적인 호출을 통하여 다른 bean을 참조할때도 그렇다. 반대로 일반적인 @Component 클래스 안에서 @Bean 메소드 안의 필드나 메소드 실행은 특별한 CGLIB 과정이나 다른 과정없이 Java에서의 의미대로 수행된다.
@Bean 메소드를 static으로 선언하면, 그들이 포함하고있는 설정 클래스 생성없이 호출될 수 있다. 이런 방식은 특히 후 처리자 bean들(BeanFactoryPostProcessor 나 BeanPostProcessor)를 정의할때 효과적인데, 이런 bean들은 컨테이너 생명주기에서 일찍 초기화되고, 해당 시점에 설정의 다른 부분을 일으키는것을 피해야 하기 때문이다.
Static @Bean 메소드 호출은 @Configuration 클래스안에서 호출하는것 조차도 컨테이너에 의해서 인터셉트 되지 않는데, 다음과 같은 기술적인 이유가 있다. CGLIB 하위 클래스들은 static 메소드가 아닐때만 재정의할 수 있다. 결론적으로 다른 @Bean 메소드를 직접 호출하는것은 표준적인 Java 의미를 가지고 있고, 그 결과 팩토리 메소드 자체에서 곧바로 독립적인 인스턴스가 반환된다.
@Bean 메소드의 Java 언어의 가시성은 스프링 컨테이너의 bean 정의에 곧바로 영향을 주지 않는다. @Configuration 클래스가 아니면 팩토리 메소드와 static 메소드 어디든지 마음대로 선언할 수 있다. 그러나 @Configuration 클래스의 @Bean 메소드에서는 private나 final이 아닌 형태로 재정의 되어야 한다. 
@Bean 메소드는 컴포넌트나 설정클래스에 의해 구현된 인터페이스의 Java 8 default 메소드뿐만 아니라 해당 컴포넌트나 설정 클래스의 기본 클래스에서도 발견된다. 이것은 복잡한 설정 방식을 유연하게 할 수 있게 하며, 스프링 4.2 부터는 Java 8 default 메소드를 통해 다중 상속을 가능하게 한다.
마지막으로 단일 클래스는 실행시점에 이용가능한 종속관계에서 의존성을 사용하기 위한 여러개의 팩토리 메소드들을 조합하여 같은 bean에 대해 여러개의 @Bean 메소드를 가진다. 이것은 다른 설정 시나리오에서 팩토리 메소드나 생성자를 "탐욕적"으로 고르는것과 같은 알고리즘이다. 종속성을 만족하는 많은수의 변종이 생성시점에 결정되는데, 어떻게 컨테이너가 여러개의 @Autowired 생성자들 사이에서 선택하는지와 비슷하다.
- 자동탐지되는 컴포넌트 Naming
스캐닝 과정중에서 컴포넌트가 자동으로 탐지될 때, 해당 bean 이름은 BeanNameGenerator 전략에 의해 생성되어 스캐너 에게 알려준다. 기본적으로 어떤 스프링 스테레오타입 어노테이션이라도(@Component, @Repository, @Service, @Controller) 이름 value를 포함하기 때문에 해당하는 bean 정의에 이름을 제공한다.
만약 그런 어노테이션이 어떤 다른 탐지된 컴포넌트(예를 들면 커스텀 필터들에 의해 발견된)나 이름 value를 포함하지 않았다면, 기본 bean 이름 생성자는 소문자의 non-qualified 클래스 이름을 반환한다. 예를 들면 아래 컴포넌트 클래스들이 탐지된다면 이름은 myMovieLister 그리고 movieFinderImpl이 된다.
1
2
3
4
@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
1
2
3
4
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
만약 기본으로 생성되는 bean 이름 패턴이 마음에 들지 않는다면, 커스텀 bean 이름 패턴을 지정할 수 있다. 우선 BeanNameGenerator 인터페이스를 구현하고, 인자가 없는 기본 생성자를 작성한다. 그리고 스캐너를 설정할 때 완전한 클래스명을 제공하면 된다.
만약 같은 non qualified 클래스명을 가진 컴포넌트들이 여러개 탐지되어 이름이 충돌난다면, 생성된 bean 이름을 위해서 완전한 클래스 명을 기본적으로 가지는 BeanNameGenerator 를 설정할 필요가 있다. 스프링 5.2.3부터는 이런 목적을 위해  org.springframework.context.annotation 패키지에 FullyQualifiedAnnotationBeanNameGenerator 클래스를 제공한다.
1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
// ...
}
1
2
3
4
<beans>
<context:component-scan base-package="org.example"
name-generator="org.example.MyNameGenerator" />
</beans>
일반적으로는 다른 컴포넌트들이 해당 컴포넌트를 명확히 참조할 수 있도록 하기 위해서 어노테이션에 이름을 지정하는것을 고려해야 한다. 반면 컨테이너가 wiring을 한다면 자동 생성되는 이름으로도 충분하다.
- 자동 탐지되는 컴포넌트들에 Scope 제공
일반적으로 스프링이 관리하는 컴포넌트들이 그렇듯이, 자동 탐지되는 컴포넌트에 대한 가장 일반적이고 기본적인 scope는 싱글톤이다. 그러나 때로는 @Scope 어노테이션을 이용하여 다른 스코프가 필요할때도 있다. 어노테이션에 scope 이름을 사용하면 된다.
1
2
3
4
5
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
@Scope 어노테이션들은 콘크리트 bean 클래스(컴포넌트가 기술된)나 팩토리 메소드(@Bean 메소드) 에서만 효과가 있다. XML bean 정의와 대조적으로 bean 정의 상속에는 어떤 기호도 없고, 클래스단의 상속 구조는 메타데이터 목적과는 관계가 없다.
스프링에서 "request"나 "session" 같은 웹 종속적인 scope에 대한 자세한 사항은 스프링의 다른 문서들을 참조하길 바란다. 이런 scope 들을 위해 미리 정의된 어노테이션에서도 마찬가지로, 스프링 메타 어노테이션 방식으로 자신만의 scoping 어노테이션을 만들 수 있다. 예를 들면 @Scope("prototype")에 대한 커스텀 어노테이션이나 또 커스텀 scoped-proxy 모드와 같은것도 가능하다.
scope 해결을 위한 커스텀 전략을 제공하는것은 어노테이션 접근에 의존하기보다는 ScopeMetadataResolver 인터페이스를 구현할 수 있다. 인자가 없는 기본 생성자를 구현해야 한다. 그러면 스캐너를 구성할 때 완전한 클래스명을 제공할 수 있다.
1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
// ...
}
1
2
3
<beans>
<context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>
싱글톤이 아닌 특정 scope를 사용할 때, scoped 객체를 위한 프록시를 생성할 필요가 있을것이다. 이런 목적으로 component-scan 항목에서 scoped-proxy 속성을 이용할 수 있다. 속성값으로는 no, interfaces, targetClass 3가지의 값을 가질 수 있다. 아래 설정은 JDK 다이나믹 프록시를 설정한 예시이다.
1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
// ...
}
1
2
3
<beans>
<context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>
- 어노테이션을 이용한 Qualifier 메타데이터 제공
앞에서는 autowire 후보자들을 세밀하게 제어하기 위한 @Qualifier 어노테이션의 쓰임새와 qualifier 어노테이션을 커스터마이징 하는 방법을 설명하였다. 해당 예제들은 XML bean 정의를 기반이었는데, qualifier 메타데이터는 XML bean 태그에 qualifier 나 meta 자식 태그를 사용한 후보자 bean 정의에서 제공되었다. 컴포넌트 자동 탐지를 위한 클래스 패스 스캐닝을 사용할 때, 후보자 클래스 타입단에 어노테이션과 함께 qualifier 메타데이터를 사용할 수 있다. 아래 3 가지 예제를 살펴보자.
1
2
3
4
5
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
1
2
3
4
5
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
1
2
3
4
5
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
// ...
}
대부분 어노테이션기반으로 사용할 때, 어노테이션 메타데이터는 클래스 정의자체에 엮여있다는것을 명심해야 한다. 반면 XML을 사용하면 같은 type의 여러 bean들이 그들의 qualifier 메타데이터에 변형을 제공할 수 있는데, 메타데이터가 클래스당이 아니라 인스턴스당 제공되기 때문이다.
- 후보자 컴포넌트의 index 생성
클래스패스 스캐닝은 매우 빠르지만, 컴파일 시점에 후보자 static 목록을 만들면 대규모 어플리케이션에서 성능향상을 시킬 수 있다. 이 모드에서는 컴포넌트 스캔의 대상이 되는 모든 모듈은 이런 메커니즘을 사용해야 한다.
@ComponentScan 이나 <context:component-scan> 지시자는 특정 패키지에서 후보자들을 스캔하기 위한 context 요청으로 남아있어야 한다. ApplicationContext 가 이런 index를 탐지할 때, 자동으로 클래스패스 스캐닝이 아닌 인덱스 방식을 사용한다.
Index를 생성하기 위해서 컴포넌트 scan 지시자들의 타겟 컴포넌트를 포함하는 각 모듈에 추가적인 종속성을 추가해야 한다. 아래 예제는 maven 으로 종속성을 추가하는 예제이다.
1
2
3
4
5
6
7
8
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<version>5.2.5.RELEASE</version>
<optional>true</optional>
</dependency>
</dependencies>
Gradle 4.5 나 이전버전에서 종속성은 compileOnly 설정에 선언되어야 한다.
1
2
3
dependencies {
compileOnly "org.springframework:spring-context-indexer:5.2.5.RELEASE"
}
Gradle 4.6 부터는 종속성은 annotationProcessor 설정에 선언되어야 한다.
1
2
3
dependencies {
annotationProcessor "org.springframework:spring-context-indexer:{spring-version}"
}
이렇게 하면 META-INF/spring.components 파일이 포함된 jar 파일이 생성된다.
IDE 에서 이 모드를 사용할 때, spring-context-indexer 가 annotation 처리기로 등록되어있어야 하는데, 후보자 컴포넌트가 갱신될 때 index가 최신이라는 것을 보장해주기 때문이다.
Index는 META-INF/spring.components 가 클래스패스에서 발견되면 자동으로 활성화된다. 만약 Index가 어떤 라이브러리에서만 부분적으로 이용가능하다면 전체 어플리케이션에 적용될 수는 없는데, system property나 루트 클래스패스에서 spring.properties 파일의 spring.index.ignore를 true로 설정해서 정규 클래스패스를 배치할 수 있다.


덧글

댓글 입력 영역