Project

[Project Winter/#9] Dependency Injection 기능 개발(IoC)

djawnstj 2023. 8. 2. 17:22

Project Winter

 

DI 기능을 구현하며 드디어 계획했던 기능을 모두 구현했다.

 

스프링의 핵심이라고 할 수도 있는 IoC 역할을 구현해서 결합도도 낮추고 개발 편의성을 낮출 수 있도록 했다.

@Autowired

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}

@Autowired 애노테이션을 개발해 의존성 주입을 해줄 생성자, 필드, 메서드에 애노테이션을 명시할 수 있도록 했다.
이 애노테이션이 붙은 생성자, 필드, 메서드는 백엔드 컨테이너에서 빈 주입을 해주면 된다.

BeanFactory 수정

private Object createInstance(Class<?> clazz) {
    if (!isDeclaredBean(clazz)) throw new IllegalArgumentException("Not declared bean: " + clazz.getSimpleName());
    Constructor<?> constructor = findConstructor(clazz);
    Set<Method> methods = getAutowiredMethodsInClass(clazz);
    Set<Field> fields = getAutowiredFieldsInClass(clazz);
    try {
        final Object[] parameters = createParameters(constructor.getParameterTypes());
        constructor.setAccessible(true);
        final Object instance = constructor.newInstance(parameters);
        injectAutowiredMethod(methods, instance);
        injectAutowiredFields(fields, instance);
        putBean(clazz.getName(), clazz, instance);
        return instance;
    } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }
}

private boolean isDeclaredBean(final Class<?> clazz) {
    return (preInstantiatedClazz != null && preInstantiatedClazz.contains(clazz));
}

private void injectAutowiredFields(final Set<Field> fields, final Object instance) throws IllegalAccessException {
    for (final Field field : fields) {
        field.setAccessible(true);
        if (!isBeanInitialized(field.getType().getSimpleName(), field.getType())) createInstance(field.getType());
        field.set(instance, getBean(field.getType()));
    }
}

private void injectAutowiredMethod(final Set<Method> methods, final Object instance) throws IllegalAccessException, InvocationTargetException {
    for (final Method method : methods) {
        method.setAccessible(true);
        final Object[] parameters = createParameters(method.getParameterTypes());
        method.invoke(instance, parameters);
    }
}

private Set<Field> getAutowiredFieldsInClass(Class<?> clazz) {
    Set<Field> fields = new HashSet<>();
    fields.addAll(filterAutowiredFields(clazz.getFields()));
    fields.addAll(filterAutowiredFields(clazz.getDeclaredFields()));
    return fields;
}

private List<Field> filterAutowiredFields(Field[] clazz) {
    return Arrays.stream(clazz).filter(field -> field.isAnnotationPresent(Autowired.class)).toList();
}

private Set<Method> getAutowiredMethodsInClass(Class<?> clazz) {
    Set<Method> methods = new HashSet<>();
    methods.addAll(filterAutowiredMethods(clazz.getMethods()));
    methods.addAll(filterAutowiredMethods(clazz.getDeclaredMethods()));
    return methods;
}

private List<Method> filterAutowiredMethods(Method[] clazz) {
    return Arrays.stream(clazz).filter(method -> method.isAnnotationPresent(Autowired.class)).toList();
}

private Constructor<?> findConstructor(Class<?> clazz) {
    Set<Constructor<?>> allConstructors = getAllConstructors(clazz);
    if (allConstructors.size() == 1) return allConstructors.stream().findFirst().get();
    return findAutowiredConstructor(allConstructors);
}

private Set<Constructor<?>> getAllConstructors(Class<?> clazz) {
    Set<Constructor<?>> allConstructors = new HashSet<>();
    allConstructors.addAll(List.of(clazz.getDeclaredConstructors()));
    allConstructors.addAll(List.of(clazz.getConstructors()));
    return allConstructors;
}

private Constructor<?> findAutowiredConstructor(Set<Constructor<?>> allConstructors) {
    for (Constructor<?> constructor : allConstructors) {
        final boolean isAutowiredConstructor = constructor.isAnnotationPresent(Autowired.class);
        if (isAutowiredConstructor) return constructor;
    }
    return null;
}

createInstance() 메서드가 호출되면 인스턴스를 만들기 전 @Autowired 가 붙은 생성자, 필드, 메서드를 모두 찾는다.
이때 생성자가 한개만 존재하는 경우 애노테이션이 없어도 의존성 주입을 해주도록 했다.

테스트

import static org.junit.jupiter.api.Assertions.*;

@WinterServerTest
public class DITest {

    public abstract static class AbstractDiTestBean {
        DiTestComponent5 component5;

        abstract void setComponent5(DiTestComponent5 component5);
    }

    @Component
    public static class DiTestBean extends AbstractDiTestBean {

        private final DiTestComponent1 component1;
        private DiTestComponent2 component2;
        private DiTestComponent3 component3;

        private DiTestBean(DiTestComponent1 component1) {
            this.component1 = component1;
        }

        @Autowired
        private DiTestBean(DiTestComponent1 component1, DiTestComponent2 component2) {
            this(component1);
            this.component2 = component2;
        }

        public DiTestBean(DiTestComponent1 component1, DiTestComponent2 component2, DiTestComponent3 component3) {
            this(component1, component2);
            this.component3 = component3;
        }

        @Autowired
        private DiTestComponent4 component4;

        @Override
        @Autowired
        void setComponent5(final DiTestComponent5 component5) {
            this.component5 = component5;
        }

    }

    @Component
    public static class SingleConstructorDiTestBean {
        private final DiTestBean diTestBean;

        SingleConstructorDiTestBean(DiTestBean diTestBean) {
            this.diTestBean = diTestBean;
        }
    }

    @Component
    public static class DiTestComponent1 {}

    @Component
    public static class DiTestComponent2 {}

    @Component
    public static class DiTestComponent3 {}

    @Component
    public static class DiTestComponent4 {}

    @Component
    public static class DiTestComponent5 {}

    @Test
    public void diTest() throws Exception {
        final DiTestBean bean = BeanFactory.getInstance().getBean(DiTestBean.class);

        assertNotNull(bean);
        assertNotNull(bean.component1);
        assertNotNull(bean.component2);
        assertNull(bean.component3);
        assertNotNull(bean.component4);
        assertNotNull(bean.component5);
    }

    @Test
    public void singleConstructorDiTest() throws Exception {
        final SingleConstructorDiTestBean bean = BeanFactory.getInstance().getBean(SingleConstructorDiTestBean.class);

        assertNotNull(bean);
        assertNotNull(bean.diTestBean);
    }

}

여러 경우에서 빈을 제대로 생성하는지 검증하는 테스트코드이다.

 

여러개의 생성자 중 애노테이션이 붙은 생성자와 애노테이션이 붙은 필드, 메서드를 통한 빈 주입에 대한 테스트와 애노테이션이 없는 단일 생성자에 대한 테스트를 수행했다.

다음으로


정확한 날짜는 모르지만 49주 전에 올렸던 게시글에도 올렸듯 프레임워크를 만드는건 옛날부터 목표였다. 저 글을 올릴때도 백엔드 프레임워크를 염두해두고 있었는데 이렇게 마무리하니 감개가 무량하다.

 

이젠 부족한 공부를 좀 더 하면서 제대로 된 사이드 프로젝트에 들어가고 배포까지 계획했던대로 진행하고자 한다.

반응형