[Spring 클론코딩] 1. BeanFactory

kingcjy·2020년 6월 30일
1

Spring 클론코딩

목록 보기
2/2
post-thumbnail

처음으로 만들것은 스프링 빈 컨테이너인 BeanFactory이다.

BeanUtils

가장 먼저 BeanUtils 클래스를 만든다.
Target클래스 혹은 생성자와 생성자 arguments를 이용해서 인스턴스를 생성하는 간단한 유틸리티 클래스다. 나중에 Bean을 생성할때 이 유틸 클래스를 사용한다.
org.myspring.beans.BeanUtils.java

public class BeanUtils {

    public static <T> T instantiateClass(Class<T> targetClass) {
        if(targetClass.isInterface()) {
            throw new BeanInstantiationException(targetClass, "Specified class is an interface");
        }

        try {
            return instantiateClass(targetClass.getConstructor());
        } catch (NoSuchMethodException e) {
            throw new BeanInstantiationException(targetClass, "No default constructor found", e);
        }
    }

    public static <T> T instantiateClass(Constructor<T> constructor, Object... args) {
        try {
            Class<?>[] parameterTypes = constructor.getParameterTypes();

            if(args.length != parameterTypes.length) {
                throw new IllegalArgumentException("argument length and parameter length is not match");
            }
            return constructor.newInstance(args);
        } catch (InstantiationException ex) {
            throw new BeanInstantiationException(constructor, "Is it an abstract class?", ex);
        } catch (IllegalAccessException ex) {
            throw new BeanInstantiationException(constructor, "Is the constructor accessible?", ex);
        } catch (IllegalArgumentException ex) {
            throw new BeanInstantiationException(constructor, "Illegal arguments for constructor", ex);
        } catch (InvocationTargetException ex) {
            throw new BeanInstantiationException(constructor, "Constructor threw exception", ex.getTargetException());
        }
    }
}

검증을 위해 테스트를 작성한다. 테스트는 JUnit5를 사용한다.
org.myspring.beans.BeanUtilsTest.java

public class BeanUtilsTest {

    @Test
    @DisplayName("인스턴스 생성 테스트")
    public void instantiateClassTest() throws Exception {
        String message = "Hello World";
        TestClass testClass = BeanUtils.instantiateClass(TestClass.class.getDeclaredConstructor(String.class), message);
        assertThat(testClass.getMessage()).isEqualTo(message);
    }

    @Test
    public void instantiateInterfaceTest() {
        assertThatThrownBy(() -> {
            BeanUtils.instantiateClass(List.class);
        }).isInstanceOf(BeanInstantiationException.class);
    }

    public static class TestClass {

        private final String message;

        public TestClass(String message) {
            this.message = message;
        }

        public String getMessage() {
            return message;
        }
    }
}

BeanDefinition

다음은 BeanDefinition 이다.
Bean을 생성하기 위한 정보를 가지고 있는 클래스다.
BeanFactory에서 이 BeanDefinition의 정보를 가지고 BeanUtils에 인스턴스 생성을 요청한다.
org.myspring.beans.BeanDefinition.java

public interface BeanDefinition {
    Class<?> getType();
    String getName();
}

ClassBeanDefinition은 기본적인 @Component, @Controller @Service 등의 어노테이션이 붙은 클래스의 정보를 넣어두는데 사용한다.

org.myspring.beans.definition.ClassBeanDefinition.java

public class ClassBeanDefinition implements BeanDefinition {
    private String name;
    private Class<?> type;
    public ClassBeanDefinition(Class<?> type) {
        this(type.getName(), type);
    }
    
    public ClassBeanDefinition(String name, Class<?> type) {
        this.name = name;
        this.type = type;
    }
    
    @Override
    public Class<?> getType() {
        return type;
    }
    
    @Override
    public String getName() {
        return name;
    }
}

BeanDefinitionRegistry인터페이스 를 만든다.
이 인터페이스의 구현체는 BeanDenition을 등록하고 관리한다.
org.myspring.beans.definition.BeanDefinitionRegistry.java

public interface BeanDefinitionRegistry {
    void registerBeanDefinition(BeanDefinition beanDefinition);
    Set<BeanDefinition> getBeanDefinitions();
    BeanDefinition getBeanDefinition(Class<?> targetClass);
}

BeanFactory

빈의 인스턴스를 생성, 관리하는 빈 컨테이너다.
org.myspring.beans.BeanFactory.java

public interface BeanFactory {
    Object getBean(String name);
    <T> T getBean(Class<T> type);
    <T> T getBean(String name, Class<T> type);
    Object[] getBeans();
}

마지막은 BeanFactoryBeanDefinitionRegistry의 구현제인 DefaultBeanFactory이다.
BeanDefinitionRegistry를 상속받아 BeanDefinition을 등록할 수 있고
BeanDefinition의 정보로 빈의 인스턴스를 생성하고 의존성을 주입한다.
org.myspring.beans.DefaultBeanFactory.java

public class DefaultBeanFactory implements BeanFactory, BeanDefinitionRegistry {
    private Set<BeanDefinition> beanDefinitions = new HashSet<>();
    private Map<String, Object> beans = new HashMap<>();
    
    public void instantiate() {
        beanDefinitions.forEach(beanDefinition -> instantiateBeanDefinition(beanDefinition));
    }
    
    private void instantiateBeanDefinition(BeanDefinition beanDefinition) {
        if(beans.get(beanDefinition.getName()) != null) {
            return;
        }
        
        Constructor cosntructor = beanDefinition.getType().getDeclaredConstructors()[0];
        Object[] parameters = getParameters(cosntructor.getParameterTypes());
        Object instance = BeanUtils.instantiateClass(cosntructor, parameters);
        beans.put(beanDefinition.getName(), instance);
    }
    
    private Object[] getParameters(Class<?>[] parameterTypes) {
        List<Object> instances = new ArrayList<>();
        for (Class<?> typeParameter : parameterTypes) {
            Object instance = getInstanceBean(typeParameter);
            instances.add(instance);
        }
        return instances.toArray();
    }
    
    private Object getInstanceBean(Class<?> typeParameter) {
        if(beans.get(typeParameter) == null) {
            instantiateBeanDefinition(getBeanDefinition(typeParameter));
        }
        return beans.get(typeParameter.getName());
    }
    
    @Override
    public void registerBeanDefinition(BeanDefinition beanDefinition) {
        this.beanDefinitions.add(beanDefinition);
    }
    
    @Override
    public Set<BeanDefinition> getBeanDefinitions() {
        return beanDefinitions;
    }
    
    @Override
    public BeanDefinition getBeanDefinition(Class<?> targetClass) {
        return this.beanDefinitions.stream()
                .filter(beanDefinition -> beanDefinition.getType().equals(targetClass))
                .findAny()
                .orElseGet(null);
    }
    
    @Override
    public Object getBean(String name) {
        return beans.get(name);
    }
    
    @Override
    public <T> T getBean(Class<T> type) {
        return (T) beans.get(type.getName());
    }
    @Override
    public <T> T getBean(String name, Class<T> type) {
        return (T) beans.get(name);
    }
    
    @Override
    public Object[] getBeans() {
        return beans.values().toArray(new Object[] {});
    }
}

이제 실제로 동작하는지 확인을 위해 테스트를 작성한다.
의존성이 없는 Bean 생성과 의존성이 있는 Bean 생성 두가지를 테스트한다.
org.myspring.beans.DefaultBeanFactoryTest.java

public class DefaultBeanFactoryTest {

    @Test
    public void instantiateTest() {
        DefaultBeanFactory beanFactory = new DefaultBeanFactory();

        beanFactory.registerBeanDefinition(new ClassBeanDefinition(TestClass.class));
        beanFactory.instantiate();

        TestClass testClass = beanFactory.getBean(TestClass.class);

        assertThat(testClass.getMessage()).isEqualTo("helloWorld");
    }

    @Test
    public void instantiateWithDependencyInjectionTest() {
        DefaultBeanFactory beanFactory = new DefaultBeanFactory();

        beanFactory.registerBeanDefinition(new ClassBeanDefinition(TestClass.class));
        beanFactory.registerBeanDefinition(new ClassBeanDefinition(TestClass2.class));
        beanFactory.instantiate();

        TestClass testClass = beanFactory.getBean(TestClass.class);
        TestClass2 testClass2 = beanFactory.getBean(TestClass2.class);

        assertThat(testClass.getMessage()).isEqualTo("helloWorld");
        assertThat(testClass2.getMessage()).isEqualTo("helloWorld");
    }


    static class TestClass {
        private String message = "helloWorld";

        public String getMessage() {
            return message;
        }
    }

    static class TestClass2 {
        private final TestClass testClass;

        public TestClass2(TestClass testClass) {
            this.testClass = testClass;
        }

        public String getMessage() {
            return testClass.getMessage();
        }
    }
}

여기까지 진행하면 간단한 빈 컨테이너가 완성된다. 필요한 클래스들을 등록하고 BeanFactory를 초기화하면 인스턴스 생성과 의존성 주입을 자동으로 알아서 진행해준다.

Source >> GITHUB

profile
아 개발잘하고싶다

1개의 댓글

comment-user-thumbnail
2020년 7월 1일

좋은 내용 잘봤습니다! 다음편도 기대할게요 :)

답글 달기