[Project-Winter/#4] 애노테이션 기반 싱글톤 빈 동적 생성

djawnstj·2023년 6월 24일
0

Project Winter

목록 보기
4/4

블로그 이전

원래 애노테이션 기반으로 싱글톤 빈을 런타임시에 생성/관리 해주는 기능은 프로젝트 막바지에 개발하려 했다. 하지만 지난번 컨트롤러 기능 개발 중 컨트롤러 매핑 문제가 있었고, 문제 해결 방법으로 @Configuration 애노테이션이 붙은 설정 클래스에서 컨트롤러를 매핑해주기로 했다.

이 기능을 구현하기 위해 애노테이션 기반 빈 객체 생성 기능을 먼저 개발하였다.

Annotation

생성한 애노테이션
우선 애노테이션은 위와 같이 다섯개를 미리 만들어 뒀다.

@Component

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}

애플리케이션이 실행하면서 생성할 빈을 찾을 때, @Component 애노테이션이 붙은 클래스를 대상으로 검색을 하게끔 하였다. 따라서 @Controller, @Configuration 등 빈으로 관리할 애노테이션들은 이 애노테이션을 모두 가지고 있어야 한다.

Configuration

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Configuration {
}

설정 클래스들에 붙여줄 애노테이션이다. 후에 설명할 @Bean 애노테이션을 붙인 메서드를 만들어 빈으로 관리할 객체를 지정할수도 있다.

Controller

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Controller {
}

컨트롤러 역할을 할 클래스들에 붙여줄 애노테이션이다. 이 애노테이션을 기반으로 컨트롤러를 생성, 후에 설명할 @RequestMapping 애노테이션과 함께 핸들러 매핑을 하게 된다.

Bean

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Bean {

    String name() default "";

}

메서드 레벨에 붙일 수 있으며 위에 설명한 @Configuration 클래스 안의 메서드에 붙으면 해당 메서드의 리턴값은 싱글톤 빈으로 우선 관리된다.
빈의 이름을 받을 수 있게 name 이라는 프로퍼티를 추가해줬다.

RequestMapping

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
}

위의 설명한 @Controller 클래스, 혹은 클래스 안의 메서드에 붙으며 핸들러 매핑을 할 때 사용된다.

이제 위 애노테이션들이 붙은 클래스를 동적으로 생성해주면 된다.

BeanInfo

public class BeanInfo {
    private final String beanName;
    private final Class<?> clazz;

    public BeanInfo(String beanName, Class<?> clazz) {
        this.beanName = beanName;
        this.clazz = clazz;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        BeanInfo beanInfo = (BeanInfo) o;
        return Objects.equals(clazz, beanInfo.clazz);
    }

    @Override
    public int hashCode() {
        return Objects.hash(beanName, clazz);
    }
}

생성한 Bean의 키로 사용할 객체이다. 빈 이름과 클래스 리터럴을 이용해서 객체를 구성한다.
키의 비교를 위해 equals를 오버라이딩 하여 클래스 리터럴이 같은지를 비교하게 구성했다.

BeanFactory

해당 객체에서 리플렉션을 이용해 특정 패키지 내의 애노테이션이 붙은 클래스들을 가져와 싱글톤으로 빈 관리를 해준다.

변수

private static String packagePrefix = "com.project.winter";
private static Reflections reflections;
private static final Map<BeanInfo, Object> beans = new HashMap<>();
private static final List<Object> configurations = new ArrayList<>();

이 객체에서 사용하는 변수들이다.
패키지는 일단 지금은 이 프로젝트를 이용하는 패키지가 확정적이므로 고정값을 두었지만, 추후에 서버가 실행되는 패키지의 하위로 설정되도록 기능을 추가할 예정이다.

초기화

public static void initialize() {
    reflections = new Reflections(packagePrefix);
    Set<Class<?>> preInstantiatedClazz = getClassTypeAnnotatedWith(Component.class);

    createBeansByConfiguration();
    createBeansByClass(preInstantiatedClazz);
}

private static Set<Class<?>> getClassTypeAnnotatedWith(Class<? extends Annotation> annotation) {
    Set<Class<?>> types = new HashSet<>();

    reflections.getTypesAnnotatedWith(annotation).forEach(type -> {
        if (!type.isAnnotation() && !type.isInterface()) {
            if (type.isAnnotationPresent(Configuration.class)) configurations.add(createInstance(type));
            else types.add(type);
        }
    });

    return types;
}

초기화 단계이다. 설정한 패키지 하위에서 @Component 가 붙은 클래스 리터럴을 가져온다.
@Component 가 붙은 애노테이션, 인터페이스도 모두 가져와지기 때문에 클래스가 아니면 클래스 리터럴을 반환하지 않게끔 했다.

빈 생성 로직

private static Object createInstance(Class<?> clazz) {
    Constructor<?> constructor = findConstructor(clazz);

    try {
        return constructor.newInstance();
    } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }
}

private static Constructor<?> findConstructor(Class<?> clazz) {

    Set<Constructor> allConstructors = ReflectionUtils.getAllConstructors(clazz);

    Constructor<?> foundConstructor = null;
    for (Constructor constructor : allConstructors) {
        int parameterCount = constructor.getParameterCount();
        if (parameterCount == 0) {
            foundConstructor = constructor;
            break;
        }
    }

    return foundConstructor;
}

리플렉션을 통해 찾은 클래스 리터럴에서 생성자를 찾고 해당 생성자를 이용해 인스턴스 객체를 생성해주는 로직이다.
DI 와 관련된 기능은 추후에 업데이트 예정이므로 파라미터가 없는 생성자만 반환하게끔 하였다.

@Bean 을 이용한 빈 생성

private static void createBeansByConfiguration() {
    configurations.forEach(BeanFactory::createBeanInConfigurationAnnotatedClass);
}

private static void createBeanInConfigurationAnnotatedClass(Object configuration) {
    Class<?> subclass = configuration.getClass();
    Map<Class<?>, Method> beanMethodNames = BeanFactoryUtils.getBeanAnnotatedMethodInConfiguration(subclass);

    beanMethodNames.forEach((clazz, method) -> {
        List<Object> parameters = new ArrayList<>();

        Arrays.stream(method.getParameters()).forEach(parameter -> {
            Class<?> parameterType = parameter.getType();
            String parameterName = parameter.getName();
            if (isBeanInitialized(parameterName, clazz)) parameters.add(getBean(parameterName, clazz));
            else parameters.add(createInstance(parameterType));
        });

        try {
            Object object = method.invoke(configuration, parameters.toArray());
            Bean anno = method.getAnnotation(Bean.class);
            String beanName = (anno.name().isEmpty()) ? method.getName() : anno.name();
            putBean(beanName, clazz, object);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    });
}

스프링과 마찬가지로 @Bean 애노테이션으로 선언한 빈 정보들을 우선적으로 생성하게끔 하였다.
만약 파라미터가 있다면 클래스 생성된 빈들에서 클래스 리터럴이 일치하는 빈을 주입해준다. 만약 생성된 빈이 없다면 빈을 생성해준다.

@Component 를 이용한 빈 생성

private static void createBeansByClass(Set<Class<?>> preInstantiatedClazz) {
    for (Class<?> clazz : preInstantiatedClazz) {
        if (isBeanInitialized(clazz)) continue;

        Object instance = createInstance(clazz);
        putBean(clazz.getName(), clazz, instance);
    }
}

리플렉션으로 가져온 @Component 애노테이션이 붙은 클래스 리터럴을 이용해 빈을 생성하는 로직이다.

해당 기능을 기반으로 컨트롤러를 생성해 요청을 수행하는 로직을 만들 차례이다.

profile
이용자가 아닌 개발자가 되자!

0개의 댓글