Spring의 DI 컨테이너 BeanFactory 만들어보기
BeanFactory는 스프링이 빈을 생성하고 의존관계를 설정하는 기능을 담당하는 가장 기본적인
IOC 컨테이너를 말한다. 이 BeanFacotry를 구현한 많은 컨테이너를 제공하고 있고
개발자는 원하는 컨테이너를 선택해 빈을 관리하면 된다.
그 중 많이 선택하는 방식이 @Annotation기반에 @Controller, @Service, @Inject 등을 붙인 방식이다.
여기서는 Reflections를 이용해 @Controller, @Service, @Inject 가 붙은 클래스들을 DI 방식을 통해
빈 등록하는 걸 구현해본다. DI는 생성자 DI만 생각한다.
스프링에서처럼 다양한 기능을 제공하는 것이 아닌 단순히 빈을 생성하는 기능만 구현한다.
완성된 프로젝트는 https://github.com/gks930620/study_provide 의 di_practice를 다운받으세요
준비
@을 적용하기 위해 @Controller, @Service, @Inject 을 만든다.
@Target({ElementType.TYPE}) //class, interface,enum에 @을 붙일 수 있다.
@Retention(RetentionPolicy.RUNTIME ) //@이 RUNTIME중에도 남아있다. 자세한건 RetentionPliciy 검색
public @interface Controller {
}
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
}
@을 적용한 UserService와 UserController
@Inject는 생성자방식만 적용한다.
@Service
public class UserService {
}
@Controller
public class UserController {
private final UserService userService;
@Inject
public UserController(UserService userService){
this.userService=userService;
}
//@Inject가 제대로 동작한다면 userService는 null이 아닐 것이다.
public UserService getUserService() {
return userService;
}
}
이제 UserController와 UserService의 객체를 가지게 될 BeanFactory를 Reflection을 이용해 만들거다.
이 BeanFactory 클래스는 다음과 같은 테스트를 통과해야 할 것이다.
@Test
void diTest() {
UserController userController = beanFactory.getBean(UserController.class);
assertThat(userController).isNotNull(); //@Controller가 붙은건 빈으로 등록함
assertThat(userController.getUserService()).isNotNull(); //@Inject 된 Service도 빈으로 등록되야함
}
BeanFacotry
1.BeanFactory는 getBean(Class<?> clazz) 메소드를 통해 빈을 제공할 수 있어야한다.
여기서는 @이 붙은 클래스들에 대해 객체를 1개씩만 만들기 때문에 Map의<key, value>는
Map<Class<?>, Object>가 된다.
public class BeanFactory {
private Map<Class<?>, Object> beans = new HashMap<>();
public <T> T getBean(Class<T> type) {
return (T) beans.get(type);
}
}
2. BeanFactory는 생성자에 여러 클래스들에 대한 정보를 받아, 그 클래스들에 대한 객체를 1개씩 만든다.
이미 해당 클래스에 대한 객체를 생성했을 경우 if문을 통과한다.
먼저 UserController를 생성하려고 하는 경우, UserService가 필요하다.
이때는 UserController 전에 UserService를 먼저 생성한다.
public BeanFactory(Set<Class<?>> annotatedClazzes) throws ReflectiveOperationException {
for (Class<?> clazz : annotatedClazzes) {
if (getBean(clazz) == null) { //주입객체가 먼저 생성됐다면 조건이 false가 된다.
createInstanceAndPut(clazz);
}
}
}
3. 객체를 만들고 beans에 저장하는 createInstanceAndPut 메소드
빈을 만들기 위해서 먼저 주입이 필요한지 판단해야 한다.
@Inject가 붙은 생성자가 있거나, 기본생성자가 있거나 둘 중 하나의 경우일것이다.
이 2개의 경우에 맞는 Consturctor<?>를 찾아야 한다.
private void createInstanceAndPut(Class<?> clazz) throws ReflectiveOperationException {
Constructor<?> constructor = findConstructor(clazz); //생성자를 조사한다. 생성자는 1개.(주입이 있거나 없거나)
findConstructor 메소드
private Constructor<?> findConstructor(Class<?> clazz){
//@Inject가 붙은 생성자가 있는지 확인
Set<Constructor> injectedConstructors = ReflectionUtils.getAllConstructors(clazz, ReflectionUtils.withAnnotation(Inject.class));
if (injectedConstructors.isEmpty()){ //@Inject붙은 생성자가 없다면 기본생성자만 있는거다.
return clazz.getConstructors()[0];
}else{ //
return injectedConstructors.iterator().next(); //@Inject붙은 생성자도 1개라고 가정
}
}
4. 객체를 만들고 beans에 저장하는 createInstanceAndPut 메소드 전체.
private void createInstanceAndPut(Class<?> clazz) throws ReflectiveOperationException {
Constructor<?> constructor = findConstructor(clazz); //생성자를 조사한다. 생성자는 1개.(주입이 있거나 없거나)
//파라미터가 없다면 기본 생성자일 것이다.
if (constructor.getParameterTypes().length == 0) {
Object instance = constructor.newInstance();
beans.put(clazz, instance);
return ;
}
//파라미터가 있다면 주입객체가 필요한 생성자.
List<Object> parameters = new ArrayList<>();
for (Class<?> typeClass : constructor.getParameterTypes()) { //주입객체가 여러개일수도있으니까..
Object instanceBean = getBean(typeClass);
if(instanceBean==null) {//주입해야 할 객체를 아직 생성하지않음
if( ! this.annotatedClazzes.contains( typeClass)){//만약 주입해야할 클래스에 @이 없다면
throw new RuntimeException(
clazz.getSimpleName()+"빈 만드려는데 " + typeClass.getSimpleName()+"객체가 없어서 에러.");
}
createInstanceAndPut(typeClass); //재귀. 객체를 만들고 bean에 넣음.
Object instanceBean2 = getBean(typeClass); //이제는 bean에 있으니까 parameter에 추가.
parameters.add(instanceBean2);
} else { //주입해야할 객체를 이미 만들어놓음
parameters.add(instanceBean);
}
}
Object instance = constructor.newInstance(parameters.toArray());
beans.put(clazz, instance);
}
BeanFactoryTest
class BeanFactoryTest {
private Reflections reflections;
private BeanFactory beanFactory;
@BeforeEach
void setUp() throws ReflectiveOperationException {
//reflection을 통해 @이 붙은 클래스 clazz들을 얻고 beanfactory 생성자로 전달.
reflections = new Reflections("com.study");
Set<Class<?>> preInstantiactedClazz = getTypesAnnotatedWith(Controller.class, Service.class);
beanFactory = new BeanFactory(preInstantiactedClazz);
}
private Set<Class<?>> getTypesAnnotatedWith(Class<? extends Annotation>... annotations) {
Set<Class<?>> beans = new HashSet<>();
for (Class<? extends Annotation> annotation : annotations) {
beans.addAll(reflections.getTypesAnnotatedWith(annotation)); //@Controller가붙은 모든 클래스 + Service가 붙은 모든 클래스 com.study 스캔
}
return beans;
}
@Test
void diTest() {
UserController userController = beanFactory.getBean(UserController.class);
assertThat(userController).isNotNull();
assertThat(userController.getUserService()).isNotNull();
}
}
테스트가 잘 통과한다.