스프링을 이용하면서, Bean이 들어간 에러를 아주아주 많이 보았다. Bean에 대해서 잘몰라서, 매번 "잉?" 하면서 어림잡아 생각만 했다.
오늘은 Bean, IoC Container, DI, ApplicationContext에 대해서 확실히 알아보자.
스프링에서 빈(Bean)이란 "자바 객체"를 말한다.
정확히 말하면 "자바 객체 > Bean"이다.
Bean은 스프링 Ioc 컨테이너가 관리하는 자바 객체다.
자바 객체는 스프링 Ioc 컨테이너가 관리하지 않는 객체도 포함된다.
<?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 id="memberService"
class="me.wordbe.springgoahead.MemberService">
<property name="memberRepository" ref="memberRepository" />
</bean>
<bean id="memberRepository"
class="me.wordbe.springgoahead.MemberRepository" />
</beans>
Service 내부에 Repository가 존재하기 때문에 property로 repository를 등록한 것이다.
<?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="me.wordbe.springgoahead" />
</beans>
이렇게 설정하면, 다음과 같이 "@Component"를 설정해준 객체를 빈에 등록한다.
@Component
public class MemberService{
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ApplicationConfig {
@Bean
public MemberRepository memberRepository() {
return new memberRepository();
}
@Bean
public MemberService memberService(MemberRepository memberRepository) {
MemverService memberService = new memberService();
memberService.setMemberRepository(memberRepository);
return memberService;
}
}
Java Config에 BEAN을 하나씩 입력해야하는 불편함을 없애기 위한 방법이다.
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackageClasses = SpringApplication.class)
public class ApplicationConfig {
}
이렇게 하면 "@Component"를 모두 찾아서 Bean 등록해준다.
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
return args -> {
String[] beanDefinitionNames = ctx.getBeanDefinitionNames();
Arrays.stream(beanDefinitionNames).forEach(
System.out::println
);
};
}
}
spring boot에서는 위와 같이 해놓았다. "@SpringBootApplication"에 "@ComponentScan"과 "@Configuration"이 있다.
그래서 "@SpringBootApplication"을 적어주면 알아서 스캔, 등록을 다해준다.
IOC 컨테이너는 Inversion of Control(제어의 역전)의 약어다.
조금 더 쉽게 이야기하면 "내가 안 해, 니가 해"이다.
제어의 역전이란, 객체의 생성 및 생명주기에 대한 모든 객체에 대한 제어권이 바뀌었다는 것을 의미한다.
사용하는 이유는 여러가지다.
개발자가 객체를 new해서 생성하지않고, Ioc컨테이너에 존재하는 Bean 객체를 주입해준다. 해당 기능을 사용하면 싱글톤, 개발자의 편의, 성능 이슈 등등을 해결해준다.
Ioc 컨테이너에 객체의 제어권을 넘겨주면, Ioc 컨테이너가 해당 객체의 Scope를 관리해준다.
완벽히 구현되지 않은 클래스를 단위 테스트할 때, 테스팅을 도와준다.
복잡해서 다음에 제대로 정리하겠지만, 간단히 말하면
java static은 Classloader 기준으로 공유한다.
spring 은 ApplicationContext 기준으로 공유한다.
한마디로 static은 그지같다고 한다.
IOC 컨테이너에는 BeanFactory.class, ApplicationContext.class핵심적인 2가지의 클래스가 있다.
BeanFactory
자바 객체(bean) 인스턴스를 생성, 설정, 관리하는 실질적인 컨테이너이다.
ApplicationContext
BeanFactory를 상속받고 있다. BeanFactory의 확장 버전이다. 상속받아서 구현한 대표적인 차이점은 BeanFactory는 지연로딩, ApplicationContext는 pre로딩이다.
두 클래스 모두 Bean을 생성하고, 관리하는 클래스이다.
DI는 Defendency Injection(의존성 주입)의 약어이다.
위에서 언급해서 대충은 예상이 간다!
의존성 주입은 프로그램 디자인 결합도를 느슨하게 되도록하고, 의존관계 역전 원칙과 단일 책임 원칙을 따르도록 클라이언트의 생성에 대한 의존성을 클라이언트의 행위로부터 분리하는 것이다.
DI는 여러가지의 문제를 해결한다!
어떻게 어플리케이션이나 클래스가 객체의 생성 방식과 독립적일 수 있는가?
어떻게 객체의 생성 방식을 분리된 구성 파일에서 지정할 수 있는가?
어떻게 애플리케이션이 다른 구성을 지원할 수 있는가?
A객체에서 B객체를 직접 생성하면, 클래스로부터 독립적으로 인스턴스의 생성을 변경하는 것이 불가능해서 유연하지 못하다. 이는 다른 객체를 필요로하는 경우 클래스를 재사용할 수 없게 된다.
뭔가 애매하다.
코드를 보면서 다시 이해해보자!
public class PetOwner{
private AnimalType animal;
public PetOwner(){
this.animal = new Dog();
}
}
PetOwner 객체는 AnimalType 객체에 의존한다. 이럴 경우 많은 문제점이 있다.
문제점을 살펴보자.
아래 처럼
한 개를 변경하면 연쇄작용하여 와르르 부서질 수 있다!!
그래서 다음과 같이, IOC가 중재한다.
구체적인 과정을 살펴보자!
Container가 로드되면, Bean에 해당하는 객체들을 scan 하여, 해당 Bean들을 생성하려고 한다.
이 과정에서 의존성 주입이 이루어지게 되는데, 만약 순환 참조가 있다면 예외가 발생하여 Application은 종료된다.
이제 Bean들이 생성되려고 하는데, 사용자가 지정한 init method가 존재한다면, 객체가 생성되고 init이 실행되게 된다.
그 뒤에 사용자가 지정한 utility method(afterPropertiesSet)과 같은 메서드가 존재한다면, 해당 메서드가 실행되게 된다.(콜백 함수)
프로그램이 종료되기 전에 이제 Container도 같이 종료되려고 하는데, 이 과정에서 destory 메서드가 존재한다면, 실행하고 정상적으로 종료 됩니다.
package core.di.factory;
import java.util.List;
public interface BeanFactory {
void initialize();
void registerBeanDefinition(Class<?> clazz, BeanDefinition beanDefinition);
<T> T getBean(Class<T> requiredType);
List<Class<?>> getBeanClasses();
List<Object> getBeans();
}
ApplicationContext는 BeanFactory를 상속받아 구현하고 있다. bean의 생성, 등록, 조회를 구현한다.
ApplicaitonContext 전체 코드 예시
public class ApplicationContext {
private final BeanFactory beanFactory;
public ApplicationContext(Object... basePackages) {
if (ArrayUtils.isEmpty(basePackages)) {
final ComponentBasePackageScanner componentBasePackageScanner = new ComponentBasePackageScanner();
basePackages = componentBasePackageScanner.scan().toArray();
}
beanFactory = new DefaultBeanFactory();
final BeanScanners beanScanners = new BeanScanners(beanFactory);
beanScanners.scan(basePackages);
beanFactory.initialize();
}
public Set<Object> getBeansAnnotatedWith(Class<? extends Annotation> annotation) {
return this.beanFactory.getBeans().stream()
.filter(bean -> bean.getClass().isAnnotationPresent(annotation))
.collect(Collectors.toSet());
}
public <T> T getBean(Class<T> requiredType) {
return beanFactory.getBean(requiredType);
}
public List<Class<?>> getBeanClasses() {
return beanFactory.getBeanClasses();
}
public List<Object> getBeans() {
return beanFactory.getBeans();
}
}
이제 Bean을 스캔하는 것을 보자.
interface Scanner.java
package core.di;
import java.util.Set;
public interface Scanner<T> {
Set<T> scan(Object... basePackage);
}
interface Scanner.java 구현
@Override
public Set<Object> scan(Object... object) {
final AnnotationScanner annotationScanner = new AnnotationScanner();
final Set<Class<? extends Annotation>> scannedAnnotations = annotationScanner.scan(COMPONENT_SCAN_ANNOTATION);
final Set<Class<?>> classesAnnotatedComponentScan = allReflections.getTypesAnnotatedWith(COMPONENT_SCAN_ANNOTATION, true);
registerBasePackageOfComponentScan(classesAnnotatedComponentScan);
registerPackageOfClassesWithOutBasePackage(classesAnnotatedComponentScan);
registerBasePackageOfAnnotations(scannedAnnotations);
return new HashSet<>(this.basePackages);
}
Bean을 스캔하는 과정은 scan 인터페이스를 구현한다.
Reflection을 통해 Base Package에 지정한 하위 패키지에 있는 모든 Class들을 가져와서 "@Component"라는 어노테이션이 달라진 Class들을 scan 하는 책임을 가지고 있는 메서드다.
위 과정을 통해서 만들어 놓은 Bean Factory에 Map<Class<?> Object>로 객체들을 가지고 있게 된다.
private Object registerBean(Class<?> preInstantiateBean) {
if (beanInitializeHistory.contains(preInstantiateBean)) {
throw new CircularReferenceException("Circular Reference can't add to Bean Factory: " + preInstantiateBean.getSimpleName());
}
if (beans.containsKey(preInstantiateBean)) {
return beans.get(preInstantiateBean);
}
this.beanInitializeHistory.push(preInstantiateBean);
final Object instance = registerBeanWithInstantiating(preInstantiateBean);
this.beanInitializeHistory.pop();
return instance;
}
특정 객체에서부터 참조하고 있는 객체들을 registerBean을 시행할 때, beanIntializeHistory를 통해 객체들이 이미 등록되어 있는지 체크해서 순환 참조에 대한 유효성 검사✅를 실시한다.
각종 어노테이션에 Bean, Scan, 순환 검사 등등 갖가지의 일이 포함되어 있다!
그러니까 Bean은 개발자가 IoC에 등록한 객체들이다. IoC Container는 Bean의 관리를 도와주는 컨테이너이다. Bean을 생성해서 Container에 등록하여 IoC 방식으로 운영하는 것이 많은 이점이 있기 때문에 존재한다.