[F-Lab 모각코 챌린지] 49일차 - DI 구현하기

F-Lab 모각코 챌린지 - 49일차. DI 구현하기 입니다. 대부분 코드로 작성되어 있습니다.

[F-Lab 모각코 챌린지] 49일차 - DI 구현하기

코드

BeanFactory

  • Spring 에서 ApplicationContext 가 extands 한 BeanFactory 와 같다. 일부 필요 한 메서드는 추가하여 구현하였다
package wsffs.springframework.beans.factory;

import wsffs.springframework.beans.BeansException;

public interface BeanFactory {

    void refresh();
    Object getBean(String name);
    <T> T getBean(String name, Class<T> requiredType) throws BeansException;
    <T> T getBean(Class<T> requiredTpe, Object... args) throws BeansException;
    boolean containsBean(String name);
}

ApplicationContext

  • 실제로 Bean 을 저장하고 관리하는 구간으로 생각하였다
  • BeanFactory 를 바로 구현하고 싶었지만, BeanFactory 의 역할 만을 수행하기 위한 클래스는 아니기 때문에 ApplicationContext 라는 인터페이스를 두었다.
package wsffs.springframework.context;

import wsffs.springframework.beans.factory.BeanFactory;

public interface ApplicationContext extends BeanFactory {
}

ApplicationContextImpl

  • BeanDefinition : Bean 의 정보
  • beanRegister : Bean 의 저장소

ApplicationContext 의 구현체이다. DI 에서 주된 역할을 수행하는 클래스로, 여러 객체들에게 명령을 내려 실제 DI 를 동작하도록 만든다.

다만, 최적화는 수행하지 않았다

Bean 의 생성과 주입을 수행하는데, 생성은 객체가 필요 한 시점에 할 수 있도록 getBean 메서드를 호출하는 순간으로 결정하였다

따라서, 객체가 정말 필요 할 때에만 객체를 생성한다

객체가 소멸되어야 하는 타이밍은 따로 지정하지 않았다.

package wsffs.springframework.boot.web.servlet.context;

import wsffs.Application;
import wsffs.springframework.beans.*;
import wsffs.springframework.beans.annotation.Component;
import wsffs.springframework.beans.exception.BeanNotOfRequiredTypeException;
import wsffs.springframework.context.ApplicationContext;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

public class AnnotationConfigServletWebServerApplicationContext  implements ApplicationContext {

    private final Map<String, BeanDefinition> beanDefinitions;
    private final Map<String, Object> beanRegister;

    private BeanScanner beanScanner;

    public AnnotationConfigServletWebServerApplicationContext(Class scanStartPoint) {
        beanScanner = new BeanScanner(scanStartPoint);
        this.beanDefinitions = new HashMap<>();
        this.beanRegister = new HashMap<>();
    }

    public void refresh() {
        beanDefinitions.clear();
        beanRegister.clear();
        Class<? extends Annotation>[] annotations = new Class[]{Component.class};
        Set<Class<?>> scan = beanScanner.scan(annotations);
        System.out.println("scan = " + scan);

        for (Class<?> candidateBean : scan) {
            BeanDefinition beanDefinition = new DefaultBeanDefinition(candidateBean);
            beanDefinitions.put(beanDefinition.getBeanName(), beanDefinition);
        }
    }

    public Object getBean(String name) {
        String beanName = BeanNameUtils.getName(name);
        if(beanRegister.containsKey(beanName)) {
            return beanRegister.get(beanName);
        }
        BeanDefinition beanDefinition = beanDefinitions.get(beanName);
        if(Objects.isNull(beanDefinition)) {
            throw new BeansException("bean definition is not found");
        }
        Class<?>[] requiredDependencies = beanDefinition.getDependencies();
        Object[] constructorArguments = new Object[requiredDependencies.length];
        for (int i = 0; i < requiredDependencies.length; i++) {
            String argumentBeanName = BeanNameUtils.getName(requiredDependencies[i]);
            constructorArguments[i] = getBean(argumentBeanName);
        }
        return getBean(beanDefinition.getOriginalClass(), constructorArguments);

    }

    @Override
    public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
        String beanName = BeanNameUtils.getName(name);
        BeanDefinition beanDefinition = beanDefinitions.get(beanName);
        if(Objects.isNull(beanDefinition)) {
            throw new BeansException("bean definition is not found");
        }
        if(beanDefinition.getOriginalClass() != requiredType) {
            throw new BeanNotOfRequiredTypeException("bean type not matched");
        }
        Class<?>[] dependencies = beanDefinition.getDependencies();
        Object[] instanceOfDependency = new Object[dependencies.length];
        for (int i = 0; i < dependencies.length; i++) {
            String simpleName = dependencies[i].getSimpleName();
            String dependencyBeanName = BeanNameUtils.getName(simpleName);
            instanceOfDependency[i] = getBean(dependencyBeanName);
        }
        return getBean(requiredType, instanceOfDependency);
    }

    public <T> T getBean(Class<T> requiredTpe, Object... args) throws BeansException {
        String beanName = BeanNameUtils.getName(requiredTpe.getSimpleName());
        BeanDefinition beanDefinition = beanDefinitions.get(beanName);
        if(Objects.isNull(beanDefinition)) {
            throw new BeansException("bean definition is not found");
        }
        Constructor<?> beanConstructor = beanDefinition.getBeanConstructor();
        try {
            return (T) beanConstructor.newInstance(args);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    public boolean containsBean(String name) {
        String beanName = BeanNameUtils.getName(name);
        Object instance = beanRegister.get(beanName);
        return instance != null;
    }
}

BeanDefinition

객체의 정보를 가지고 있다.

Spring 의 구현 방식을 비슷하게 따라했지만 모두 비슷한것은 아니며 개인적인 판단으로 들어간 메서드도 있다.

package wsffs.springframework.beans;

import java.lang.reflect.Constructor;

public interface BeanDefinition {

    String getBeanName();
    Class<?> getOriginalClass();
    Constructor<?> getBeanConstructor();
    Class<?>[] getDependencies();
}

DefaultBeanDefinition

BeanDefinition 인터페이스를 구현한 구현체

BeanDefinition 의 함수를 수행 할 수 있도록 생성자에서 정보를 셋팅하고, 각 함수를 구현하고 있다.

package wsffs.springframework.beans;

import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Comparator;

public class DefaultBeanDefinition implements BeanDefinition {

    private final Class<?> originalClass;
    private final Constructor<?> constructor;
    private final Class<?>[] dependencies;

    public DefaultBeanDefinition(Class<?> clazz) {
        this.originalClass = clazz;
        Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
        this.constructor = Arrays.stream(declaredConstructors)
                .max(Comparator.comparingInt(Constructor::getParameterCount))
                .orElseThrow();
        this.dependencies = this.constructor.getParameterTypes();
    }

    @Override
    public String getBeanName() {
        String simpleName = originalClass.getSimpleName();
        return simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
    }

    @Override
    public Class<?> getOriginalClass() {
        return this.originalClass;
    }

    @Override
    public Constructor<?> getBeanConstructor() {
        return this.constructor;
    }

    @Override
    public Class<?>[] getDependencies() {
        return this.dependencies;
    }

    @Override
    public String toString() {
        return "DefaultBeanDefinition{" +
                "originalClass=" + originalClass +
                ", constructor=" + constructor +
                ", dependencies=" + Arrays.toString(dependencies) +
                '}';
    }
}

BeanScanner

BeanScan 을 담당하는 클래스이다

생성자로 Class 를 받고있는데, 이는 main 함수에서 추가되는 클래스이다

해당 클래스를 기준으로 하위 패키지에서 존재하는 특정 애노테이션들을 검색한다

그리고 그런 클래스들을 리턴하면, ApplicationContext 의 구현체에서 BeanDefinition 정보로 저장하도록 유도하였다.

이 위치에서 BeanDefinition 을 만들어서 넘길 수도 있지만, 여기서 BeanDefinition 을 알고있으면 안된다고 판단하였다.

클래스들의 의존성은 최대한 낮추는 방향으로.

package wsffs.springframework.beans;

import org.reflections.Reflections;
import org.reflections.scanners.Scanners;

import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.Set;

public class BeanScanner {

    private final Class beanScanStartPoint;

    public BeanScanner(Class beanScanStartPoint) {
        this.beanScanStartPoint = beanScanStartPoint;
    }

    public Set<Class<?>> scan(Class<? extends Annotation>[] annotations) {
        final Set<Class<?>> result = new HashSet<>();
        for (Class<? extends Annotation> annotation : annotations) {
            Set<Class<?>> scan = scan(annotation);
            result.addAll(scan);
        }
        return result;
    }

    public Set<Class<?>> scan(Class<? extends Annotation> annotation) {
        Reflections reflections = new Reflections(this.beanScanStartPoint.getPackageName(), Scanners.TypesAnnotated);
        return reflections.getTypesAnnotatedWith(annotation);
    }
}

BeanNameUtils

BeanName 을 담당하는 Util 이다

Bean 의 이름은 공통된 형태로 만들어져야 한다고 생각했고, 그것을 위한 클래스이다.

package wsffs.springframework.beans;

public class BeanNameUtils {

    public static String getName(Class<?> clazz) {
        String simpleName = clazz.getSimpleName();
        return getName(simpleName);
    }

    public static String getName(String name) {
        return name.substring(0, 1).toLowerCase() + name.substring(1);
    }
}

Main code

Main 코드에서는 위에서 구현한 구현체를 통해 실제로 DI 를 수행해본다

아래의 코드를 보면 DummyService 를 getBean 을 통해 가져오는데,

package wsffs;

import wsffs.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import wsffs.springframework.context.ApplicationContext;
import wsffs.web.DummyObject;
import wsffs.web.DummyRepository;
import wsffs.web.DummyService;

public class Application {

    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigServletWebServerApplicationContext(Application.class);
        applicationContext.refresh();

        DummyService dummyService = applicationContext.getBean("dummyService", DummyService.class);
        DummyObject dummy = dummyService.get();
        System.out.println("dummy = " + dummy);
    }
}

사용 코드

DummyObject

  • 객체 생성을 위한 더미 객체이다
package wsffs.web;

public class DummyObject {

    public Long id;
    public String name;
    public String email;

    public DummyObject(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    @Override
    public String toString() {
        return "DummyObject{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}

DummyRepository

  • 의존성 테스트를 위한 객체이다.
  • 이 객체가 필요로 하는 의존성은 없으며 주입되기 위한 객체이다.
package wsffs.web;

import wsffs.springframework.beans.annotation.Component;

@Component
public class DummyRepository {

    public DummyObject getDummy() {
        return new DummyObject(1L, "pollra", "pollra@gmail.com");
    }
}

DummyService

  • 의존성을 주입받을 객체이다
package wsffs.web;

import wsffs.springframework.beans.annotation.Component;

@Component
public class DummyService {

    private final DummyRepository dummyRepository;

    public DummyService(DummyRepository dummyRepository) {
        this.dummyRepository = dummyRepository;
    }

    public DummyObject get() {
        return dummyRepository.getDummy();
    }
}

동작

사실 위에 소개 한 객체들은 주된 객체들이며 나머지는 아래와 같이 구성되어있다

메인 함수를 동작시킨 결과