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주 전에 올렸던 게시글에도 올렸듯 프레임워크를 만드는건 옛날부터 목표였다. 저 글을 올릴때도 백엔드 프레임워크를 염두해두고 있었는데 이렇게 마무리하니 감개가 무량하다.
이젠 부족한 공부를 좀 더 하면서 제대로 된 사이드 프로젝트에 들어가고 배포까지 계획했던대로 진행하고자 한다.
반응형