[Spring Boot] Bean과 의존성 주입(Dependency Injection)
Bean에 대해서 설명하기에 앞서 Spring에서 등장하는 IoC 컨테이너에 대해서 이야기 해본다. IoC는 Inversion Of Control Container의 약자로 기존의 모든 제어를 클라이언트의 코드가 가지도록 구현하던 것을 Framwork가 제어를 나누어 가져가 의존 관계의 방향이 달라지게 되는 것을 말한다.
즉, IoC는 Spring Framwork로 객체를 관리하고 객체의 생성을 책임지고, 의존성까지 관리해주는 컨테이너
이다. 좀더 간단하게 이야기하면 Spring Framwork의 IoC가 객체의 생명주기를 관리하며 DI(Dependency Injection) 패턴을 제공하여 클라이언트는 비즈니스 로직에 집중할 수 있도록 해주는 것이다.
이러한 IoC를 담당하는 핵심 컨테이너가 BeanFactory이고, 이를 확장한 IoC 컨테이너가 ApplicationContext 이다. 이는 기본적인 객체로써 접근할 수 있다.
Bean 이란?
Bean은 Spring Framwork의 IoC가 관리하는 객체를 말한다. 즉, IoC에 의해서 자바 객체가 생성되면 이 객체를 Bean라 한다. 이러한 Bean은 @Bean, @Component, @Service, @Repository와 같은 어노테이션으로 생성될 수 있으며, application.xml와 같은 XML 파일에 Bean을 직접 설정해주는 것도 가능하다.
application.xml 파일에 Bean을 직접 등록하는 것은 고전적인 방법이다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
// Bean을 등록하는 과정
<bean id="bookService" class="com.ex.forblog.book.BookService">
<property name="bookRepository" ref="bookRepository"/>
</bean>
// Bean을 의존성 주입(DI)하는 과정
<bean id="bookRepository" class="com.ex.forblog.book.BookRepository"/>
</beans>
그러면 최근에는 어노테이션으로 어떻게 Bean을 등록하는지 알아보자
어노테이션으로 Bean 추가
Spring Boot의 경우 @Component, @Service, @Controller, @RestController, @Repository, @Bean, @Configuration 등오로 필요한 Bean을 등록할 수 있다.
위 그림에서 볼 수 있듯이 각 어노테이션들은 @Component 어노테이션을 상속받고 있다. 이러한 @Controller, @Service, @Repository 어노테이션은 @Component 어노테이션보다 조금 더 구체적이라고만 알고 있으면 될듯 하다.
위 어노테이션 뿐만 아니라 @Bean, @Configuration 어노테이션을 통해 Bean을 등록할 수 있다.
이 방법은 자바 설정 클래스를 이용하는 것이다. 앞서 말했듯이 초기 스프링 기반 개발에서 Bean 생성은 스프링 설정파일(XML)을 통해 이루어졌으며 지금은 자바 클래스에서 관련 설정을 대신하는 방법을 주로 사용한다. 물론 필요에 따라서 고전적인 방법도 여전히 사용 가능하다.
설정 클래스는 @Configuration 어노테이션을 클래스 선언부 앞에 추가하면 된다. 또한 특정 타입을 리턴하는 메서드를 만들고 @Bean 어노테이션을 붙여주면 자동으로 해당 타입의 Bean이 생성된다.
@Bean 어노테이션의 세부 내용은 다음과 같다.
- @Configuration 설정된 클래스의 메서드에서 사용가능하다.
- 메서드의 리턴 객체를 IoC의 Bean으로 등록한다.
- Bean의 이름은 기본적으로 메서드 이름으로 등록된다.
- @Bean(name="name") 으로 이름을 변경할 수 있다.
- @Scope를 통해 객체 생성을 조정할 수 있다.
- @Component 에너테이션을 통해 @Configuration 없이도 Bean을 등록할 수 있다.
- Bean에 init(), destory() 등 라이프사이클 메서드를 추가한 후 @Beam에 지정할 수도 있다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ApplicationConfig {
@Bean
public BookRepository bookRepository() {
return new BookRepository();
}
@Bean
public BookService bookService() {
return new BookService();
}
}
이와 같이 @Configuration 어노테이션을 추가한 설정 파일에 @Bean 어노테이션을 추가한 메서드를 추가함으로써 Bean을 등록할 수 있다.
Bean 등록을 위한 어노테이션을 추가할 때 @Primary 어노테이션을 통해 Bean의 우선권을 부여할 수도 있고, @Qualifier("name") 어노테이션을 통해 특정 이름을 가지는 Bean을 찾을 수 있다.
이 두가지 어노테이션을 동시에 사용했을 경우 @Qualifier 어노테이션을 우선적으로 적용한다.
의존성 주입(DI, Dependency Injection)
의존성 주입이란, Spring Framwork의 IoC에서 관리하고 있는 Bean들 중에서 필요한 것을 객체에 주입하는 것을 말한다. Spring은 기본적으로 @Autowired 어노테이션을 이용한 의존성 주입을 제공한다. 이러한 의존성 주입의 세 가지 방법을 소개한다.
첫 번째로 생성자를 통해 Bean의 의존성 주입을 할 수 있다.
@Component
public class SampleController {
private SampleRepository sampleRepository;
@Autowired
public SampleController(SampleRepository sampleRepository) {
this.sampleRepository = sampleRepository;
}
}
두 번째로 보다 간편하게 필드(Property)에 직접 @Autowired 어노테이션을 추가해 의존성을 주입할 수 있다.
@Component
public class SampleController {
@Autowired
private SampleRepository sampleRepository;
}
마지막으로 Setter를 통해 의존성을 주입할 수 있다.
@Component
public class SampleController {
private SampleRepository sampleRepository;
@Autowired
public void setSampleRepository(SampleRepository sampleRepository) {
this.sampleRepository = sampleRepository;
}
}
개인적으로 코딩할때는 두 번째 필드에 직접 @Autowired 어노테이션을 추가해 의존성을 주입하는 방법을 가장 많이 사용한 것 같다. 아무래도 가장 간단하기 때문에..?
하지만, Spring Framework Reference에서 권장하는 방법은 생성자를 통해 Bean의 의존성을 주입하는 방법이다. 왜냐하면 필수적으로 사용해야하는 의존성 없이는 인스턴스를 만들지 못하도록 강제할 수 있기 때문이다. 예를 들면, SampleController가 SampleRepository 없이는 제대로 동작할 수 없는 구조라면, SampleRepository Bean의 의존성 주입을 생성자를 통해서 하게되면 SampleRepository 없이 인스턴스를 만들지 못하도록 강제할 수 있다.
@ComponentScan
마지막으로 @ComponentScan 어노테이션에 대해서 설명한다. 해당 어노테이션이 붙어있는 클래스가 Component를 Scan할 시작 지점임을 나타내는 어노테이션이다.
기본적으로 메인 함수에 붙어있는 @SpringBootApplication 어노테이션이 있는데, 해당 어노테이션의 내부를 확인해보면 @ComponentScan 어노테이션이 있음을 확인할 수 있다.
따라서 기본적으로 메인 함수가 Component를 Scan하는 시작지점이 된다. (그럼 별로 건드릴게 없지않나...?)
두 가지 사용법에 대해서 간단하게 예제 코드만 살펴보고 넘어가자.
@Configuration
@ComponentScan(
basePackages = "com.ch4njun.example.app1"
)
public class TestConfiguration {
}
@Configuration
@ComponentScan(
basePackageClasses = TestConfiguration.class
)
public class TestConfiguration {
}
두 가지 방법중에 basePackageClasses로 설정하는 것이 더 Type Safe한 방법이라고 한다. 이러한 basePackages나 basePackageClasses를 등록하지 않는다면 자동으로 @ComponentScan 어노테이션이 추가되어있는 TestConfiguration 클래스가 Component Scan의 시작지점이 된다.
또한 @ComponentScan 어노테이션에 excludeFilters 속성을 사용해 특정 Bean을 등록하지 않도록 제외시킬 수 있는데 굳이..? 이러한 방법이 있다는 것만 알고 넘어가자.
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
@Configuration
@ComponentScan(
basePackageClasses = ApplicationConfig.class,
excludeFilters = @Filter(
type = FilterType.ANNOTATION,
classes = {IgnoreDuringScan.class}
)
)
public class ApplicationConfig {
}
또 Bean을 직접 등록하는 Functional한 방법이 있다는데...