Spring DI 적용하기

2024. 5. 5. 17:57Spring

 

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();
    }
}

 

 

 

테스트가 잘 통과한다.