스프링의 삼각형 이해하기! 포스트에서는 가장 기본적인 빈 등록 및 의존성 주입 방법을 숙지했고 IoC 컨테이너? 빈? ApplicationContext? 포스트에서는 컨테이너와 빈에 대해서 정리해보았다. 가장 기초가 되는 사용법과 개념을 숙지했으니, 이번 글에서는 조금 더 개선된 어노테이션 기반의 빈 등록 방법에 대해 알아보자.
대부분의 프로젝트를 보면, 스프링의 삼각형 이해하기! 포스트에서 설명한 방법보다 스프링 2.5부터 가능해진 Component Scanning을 더 많이 사용하는 것을 알 수 있다. Component Scanning은 하단의 어노테이션들을 이용하면, 어노테이션이 적용된 클래스들을 빈에 자동으로 등록시킬 수 있다.
@Component
@Repositoy
@Service
@Controller
@Configuration
Component Scanning을 적용하기 위한 방법은 @ComponentScanning
어노테이션을 이용하거나 하단의 코드처럼 XML 파일에 정의해주면 된다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.example.application.scanning"/>
</beans>
추가적으로 Component Scanning 기술에서 가장 중요한 것은 범위라는 점을 반드시 기억하자. 특정 클래스를 만들어서 구성한다고 했을 때, Component Scanning의 범위 밖에 있다면 빈으로 등록이 안되어 문제가 발생할 수 있으니 항상 염두해두고 개발을 해주어야 한다.
💡 의존성 주입 어노테이션은 무엇을 가장 많이 사용할까?
각 회사의 코드 컨벤션마다 다르지만 의존성 주입에 사용되는
@Autowired, @Resource, @Inject
어노테이션 중에서도 스프링이 제공하는@Autowired
를 더 많이 사용하는 편인 것 같다!
XML을 이용한 빈 등록보다는 자바 설정 파일을 이용한 빈 등록 방식을 더 많이 사용하는 편이다. 하나의 파일에서 직접 빈을 등록하기 때문에 빈으로 등록할 클래스에는 어노테이션을 붙이지 않아도 된다.
@Configuration
public class ApplicationConfig {
@Bean
public BookRepository bookRepository() {
return new BookRepository();
}
@Bean
public BookService bookService() {
BookService bookService = new BookService();
bookService.setBookRepository(bookRepository());
return bookService;
}
}
만약 자바 설정 파일에서 Component Scanning을 이용하고자 한다면, 빈으로 등록할 클래스에 어노테이션을 붙이고 @ComponentScan
어노테이션을 자바 설정 파일에 지정해주면 된다.
@Configuration
@ComponentScan(basePackageClasses = Application.class)
public class ApplicationConfig {
}
💡
@ComponentScan
어노테이션에서 살펴봐야하는 내용?이 어노테이션에서 살펴봐야하는 것은
@Filter
를 통해 어떤 어노테이션을 스캔할지 혹은 하지 않을지에 대한 내용과 스캔할 basePackage 설정 정도다. (문자열로 처리하는 방법보다는 그보다 타입 세이프한 방법으로 basePackageClasses를 이용하는 것을 권장한다고 한다.)
💡 Spring Boot 프로젝트에서는
@ComponentScan
어노테이션을 설정하지 않았음에도 불구하고@Component, @Repository, @Service ...
어노테이션들을 이용하여 빈을 등록할 수 있을까?정답은
@SpringBootApplication
어노테이션에 있다. 이 어노테이션 내부에는@ComponentScan
어노테이션이 지정되어있기 때문에 별도의 XML이나 자바 설정 파일을 안만들어도 빈을 등록할 수 있다.@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class} ), @Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class} )} ) public @interface SpringBootApplication { // (생략...)
싱글톤 스코프인 빈들을 초기에 다 생성을 하고 등록하기 때문에 구동할 때 오래걸릴 수도 있다. 구동이 되고 나서 부터는 성능적인 문제가 거의 없다. 하지만 애플리케이션 구동 시간에 예민한 사람이라면, 스프링 5부터 들어간 functional 빈을 등록을 이용할 수 있다. Functional 빈 등록은 사실 그렇게 크게 필요하다고 생각하지 않기 때문에 간단하게만 이해하고 넘어가자.
public static void main(String[] args) {
var app = new SpringApplication(Application.class);
app.addInitializers((ApplicationContextInitializer<GenericApplicationContext>) ctx -> {
ctx.registerBean(MyService.class);
ctx.registerBean(ApplicationRunner.class, () -> args1 -> System.out.println("Functional Bean Registration");
});
app.run(args);
}
나였다면 시도도 하지 않고 넘어갔을 법한 의존성 주입시에 발생할 수 있는 여러가지 경우의 수를 다뤄보고자 한다.
의존성 주입을 하고자 하는데, 해당 타입의 빈이 없는 경우
빈이 없는데 의존성을 주입하려는 것은 말도 안되는 행위이기에 당연히 에러가 발생한다.
의존성 주입을 하고자 하는데, 해당 타입의 빈이 한 개인 경우
빈이 한 개일 경우에는 의존성 주입할 때 타입만 같다면 문제 없이 돌아간다.
의존성 주입을 하고자 하는데, 해당 타입의 빈이 여러개인 경우
의존성 주입을 하려는데 빈이 동일한 타입의 빈이 여러개 있을 경우에는 해당 빈의 이름으로 주입을 시도한다. 만약 여기서 이름도 못찾게 되면 실패하게 된다.
3번에 해당하는 해결책은 총 3가지가 있다.
@Primary
@Qualifier
@Primary
는 여러 개의 빈 중 특정 빈에 우선순위를 두어 주입하여 해결할 수 있고, @Qualifier
는 빈의 이름으로 주입받아 해결할 수 있다.
마지막으로 해당 타입의 빈을 모두 주입받는 방법은 리스트 자료구조에 빈을 모두 담는 형태로 해결할 수 있는데, 이 방법은 현업에서 많이 사용한다고 한다. 정확한 메시지 전송 코드는 아니지만, 하단의 예시처럼 사용한다고 한다.
@Configuration
public class MessageConfig {
@Bean
@Order(1)
MessagePostProcessor indexer() {
return new MessagePostProcessor() {
@Override
public void postProcess(Message message) {
System.out.println("indexer()");
}
};
}
@Bean
@Order(2)
MessagePostProcessor alarmSender() {
return new MessagePostProcessor() {
@Override
public void postProcess(Message message) {
System.out.println("alarmSender()");
}
};
}
static class Message {
}
interface MessagePostProcessor {
void postProcess(Message message);
}
@Component
static class MessageSender {
@Autowired
private List<MessagePostProcessor> postProcessorList;
public void sendMessage() {
// code for sending message
Message message = new Message();
for(MessagePostProcessor postProcessor : postProcessorList) {
postProcessor.postProcess(message);
}
}
}
}