예제 및 테스트 코드는 github 에서 확인 가능합니다.
@Component VS @Configuration
안녕하세요. 스프링을 사용하면서 위의 두 어노테이션을 자주 사용하고 계실텐데요.
저도 마찬가지로 두 어노테이션을 자주 사용하지만 Bean 으로 선언하거나 생성할 수 있다는 것만 알고있었지
둘 사이에 어떤 차이가 있는지 그리고 어떤 상황에 사용해야 할지에 대해서는 잘 알고있지 못했습니다.
그래서 둘 사이의 용도 그리고 동작방식에 어떤 차이가 있는지 좀 더 알아보기 위해 글을 작성하게 되었습니다.
@Component
먼저 @Component
에 대해 간단하게 어떤 어노테이션인지 알아보겠습니다.
스프링 공식문서에서는 @Component
를 다음과 같이 이야기 합니다.
Indicates that the annotated class is a component.
Such classes are considered as candidates for auto-detection when using annotation-based configuration and classpath scanning.
...(중략)
ApplicationContext 로딩시 @Component
가 달린 클래스에 대해 Bean 으로 사용될 수 있도록 탐지한다는 의미로 볼 수 있습니다.
즉, @Component
가 달린 클래스에 대해서는 스프링에서 Bean 으로 사용하겠다는 의미입니다.
간단하게 @Component
를 빈으로 등록하는 과정을 살펴보겠습니다.
@Component
가 정의된 클래스를 찾을때는 @ComponentScan
어노테이션을 이용하여 찾게됩니다.@ComponentScan
의 basePackageClasses or basePackages 를 지정하여 찾게됩니다.
만약 지정된 패키지가 없다면 @ComponentScan
가 정의된 패키지에서 부터 스캔이 이루어집니다.
ComponentScanAnnotationParser
그리고 ComponentScanAnnotationParser
클래스의 parse
메소드에서@ComponentScan
의 설정을 파싱하게 됩니다.
이때 @Component
를 스캔하기 위한 basePackages, excludeFilters, includeFilters 등등 여러 정보들을 파싱합니다.
ClassPathBeanDefinitionScanner
A bean definition scanner that detects bean candidates on the classpath, registering corresponding bean definitions with a given registry (BeanFactory or ApplicationContext).
Candidate classes are detected through configurable type filters. The default filters include classes that are annotated with Spring's @Component, @Repository, @Service, or @Controller stereotype.
BeanFactory 의 ClassPath 에서 Bean 후보를 탐지하고 해당 클래스를
BeanFactory 또는 ApplicationContext 에 등록하는 Scanner 이고
Bean 등록 후보 클래스는 구성 가능한 필터를 통해 감지되는데 Spring의 @Component, @Repository, @Service, @Controller 어노테이션이 포함된다고 이야기 하고 있습니다.
즉, ClassPathBeanDefinitionScanner
클래스에서 @Component
가 정의된
클래스를 스캔하고 Bean 으로 등록하는데요.
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
- Bean 등록 후보 클래스 조회
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
- Bean 이름 생성
registerBeanDefinition(definitionHolder, this.registry);
:- Bean 등록
다음과 같이 Bean 등록이 되는것을 볼 수 있습니다.
@Configuration
다음은 @Configuration 에 대한 스프링 공식문서에서 설명하는 내용입니다.
Indicates that a class declares one or more @Bean methods and may be processed by the Spring container to generate bean definitions and service requests for those beans at runtime, for example:
@Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
// instantiate, configure and return bean ...
}
}
@Configuration 이 선언된 클래스는 하나 이상의 @Bean 메소드를 선언하고 해당 @Bean 메소드는 스프링 컨테이너를 통해 처리된다고 이야기하고 있습니다.
그리고 @Configuration
내부에 @Component
를 가지고 있기에@Configuration
이 선언된 클래스도 Bean으로 등록됩니다.
@Configuration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
@AliasFor(annotation = Component.class)
String value() default "";
boolean proxyBeanMethods() default true;
boolean enforceUniqueMethods() default true;
}
또한, @Bean 메소드는 @Configuration 내부에서 선언해야 Singleton Scope 를 가질 수 있습니다.
바로 @Configuration 의 proxyBeanMethods 설정때문입니다.
proxyBeanMethods
Specify whether @Bean methods should get proxied in order to enforce bean lifecycle behavior, e.g. to return shared singleton bean instances even in case of direct @Bean method calls in user code. This feature requires method interception, implemented through a runtime-generated CGLIB subclass which comes with limitations such as the configuration class and its methods not being allowed to declare final.
Bean 메서드를 직접 호출하는 경우에도 싱글턴 Bean Instance 를 적용하기 위한
@Bean 메서드의 프록시 여부를 지정한다고 이야기합니다.
즉, proxyBeanMethods 이 true 이면 CGLib 프록시 패턴을 적용해 Bean 이 싱글톤으로 생성됩니다. false 라면 매번 호출시마다 새로운 객체가 반환됩니다.
예제코드
ExamConfiguration.java
@Configuration
public class ExamConfiguration {
}
ExamComponent.java
@Component
public class ExamComponent {
}
다음과 같이 @Configuration
이 선언된 클래스는 CGLIB Proxy 로 등록된것을 확인할 수 있습니다.
그래서 @Component, @Configuration 둘의 차이는 ??
사용 용도
@Component
의 경우 일반적으로 직접 컨트롤 가능한 Class에 대해 선언합니다.
자신이 작성한 Class 에 대해 @ComponentScan
을 이용한 Bean 등록도 편리하고
또한 @Component
어노테이션은 클래스 레벨에 선언할 수 있습니다.
@Component
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
}
@Configuration
의 경우에는 Bean 으로 등록하기 위해서는 @Bean
이 선언된 메서드 내부에서 생성한 객체를
Bean 으로 등록할 수 있는데요.
@Bean
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
}
다음과 같이 클래스 레벨에는 선언할 수 없습니다.
외부 라이브러리를 Bean 으로 등록해서 사용하려는 경우 해당 코드를 직접 수정할 수 없기에 @Component
는 사용할 수 없습니다.
대신 @Configuration
의 @Bean Method
을 이용해서 Bean 으로 등록해서 사용하게 됩니다.
@Configuration
public class ExampleConfiguration {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
이렇듯, @Configuration
은 외부 라이브러리같은 개발자가 제어할 수 없는 코드에 대해 Bean 으로 등록하거나 특정 Bean 들을 한곳에서 관리하고 싶을때 사용할 수 있습니다.
Bean LifeCycle 예제코드
위에서 이야기했든 @Configuration
에서 Bean Method
를 선언하면
proxyBeanMethods 옵션에 따라 싱글턴으로 Bean 이 등록될 수 있다고 했습니다.
그렇다면 @Component
에서는 Bean Method
를 선언했을때@Configuration
와 어떻게 다른지 코드로 한번 살펴보겠습니다.
Exam.java
public class Exam {
}
ExamComponent.java
@Component
public class ExamComponent {
@Bean(name = "exam2")
public Exam exam() {
return new Exam2();
}
}
ExamConfiguration.java
@Configuration
public class ExamConfiguration {
@Bean
public Exam exam() {
return new Exam();
}
}
ExamTest.java
@SpringBootTest
public class ExamTest {
@Autowired
private ExamConfiguration examConfiguration;
@Test
void configuration_test() {
Exam exam = examConfiguration.exam();
Exam exam2 = examConfiguration.exam();
Exam exam3 = examConfiguration.exam();
Exam exam4 = examConfiguration.exam();
Exam exam5 = examConfiguration.exam();
System.out.println("exam: " + exam);
System.out.println("exam2: " + exam2);
System.out.println("exam3: " + exam3);
System.out.println("exam4: " + exam4);
System.out.println("exam5: " + exam5);
}
}
@Configuration
이 싱글턴으로 Bean 을 생성했다면 해당 테스트 코드에서 Exam 객체의 모든 값은 동일해야 합니다.
위와 같이 모든 객체의 값이 동일한 것을 보아 싱글턴으로 Bean 이 등록된것을 알 수 있습니다.
ExamTest.java
@SpringBootTest
public class ExamTest2 {
@Autowired
private ExamComponent examComponent;
@Test
void configuration_test() {
Exam exam = examComponent.exam();
Exam exam2 = examComponent.exam();
Exam exam3 = examComponent.exam();
Exam exam4 = examComponent.exam();
Exam exam5 = examComponent.exam();
System.out.println("exam: " + exam);
System.out.println("exam2: " + exam2);
System.out.println("exam3: " + exam3);
System.out.println("exam4: " + exam4);
System.out.println("exam5: " + exam5);
}
}
@Component
는 생성된 모든 Bean 객체의 값이 상이한걸 확인할 수 있습니다.
이렇듯 @Component
내에서 Bean Method
를 선언하게 되면 싱글턴으로 생성되는것이 아니라 호출시마다
새로운 Bean 이 생성되는것을 확인할 수 있습니다.
reference
- http://dimafeng.com/2015/08/29/spring-configuration_vs_component/
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/stereotype/Component.html
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Configuration.html
- https://mangkyu.tistory.com/75
'Spring' 카테고리의 다른 글
[Spring] Spring Event 를 이용한 비동기 이벤트 처리 (0) | 2024.06.24 |
---|---|
[Spring] ContextCaching 으로 Test 성능 개선하기 (@MockBean, @SpyBean) (0) | 2024.06.02 |
[Spring] Transactional REQUIRES_NEW 옵션에서 예외 및 Rollback (2) | 2024.01.10 |
[Spring] 서로 다른 테스트 클래스에서 테스트 데이터를 공유하는 방법 (0) | 2023.11.05 |
SpEL(Spring Expression Langauge) 사용법 + 어노테이션에 SpEL로 값 전달하기 (1) | 2023.05.06 |