본문 바로가기
Spring Framework

Java Reflection을 이용한 Bean Autowiring 구현

by kellis 2020. 10. 12.

지난 2019년 4월 2주차 뉴스클리핑에서 @Autowired는 어떻게 동작하는가에 대한 내용을 다룬 바가 있습니다. @Autowired는 내부적으로 Reflection을 이용하여 Dependency Injection을 수행하며, Spring의 동작 중 많은 것들이 Reflection을 이용하여 구현되어 있습니다. 그래서 이번 뉴스 클리핑에서는 Spring의 Bean 생성 및  DI를, 간단하게 Java를 이용하여 구현해 보도록 하겠습니다. 

  • Load : 스프링은 @Bean이나 @Configuration과 같이 빈으로 생성되어야 하는 객체들을 전부 읽어들입니다(이미지 상에서는 XML에서 읽어 들이는 것만 표현되어 있으나 앞서 말했듯 어노테이션 등을 통해서도 읽어 들입니다). 
  • Merge & Validation : 읽어들인 컨텍스트 정보들을 합치고, 검증 작업을 거칩니다.
  • Dependency tree : 빈 간의 의존성 관계를 포함하여 생성해야 할 빈들을 트리 형태로 목록화합니다. 
  • Dependency Injection : Final Bean Defnition을 가지고 빈을 생성합니다. 만약 이때, Final Bean Definition에 컨텍스트 파일을 읽어 들여 통합한 빈 목록에 없는 빈이 존재한다면 스프링은 Exception을 뿌리고 구동이 중지됩니다. 

 

스프링은 이렇게 빈을 생성하는 과정이 복잡합니다. spEL이나 lazy loading 등 빈의 생성과 주입에 영향을 미치는 요소가 많으나, 어떠한 방식으로 동작하는가에 초점을 맞출 것이기 때문에 훨씬 단순하게 구현하겠습니다. 

 

프로젝트 구조는 아래와 같습니다. 

먼저 Autowired와 유사하게 동작할 CustomAutowired 어노테이션은 Runtime Retension을 가지며 Field를 타깃으로 합니다.

@Retention(RUNTIME)
@Target({ FIELD })
public @interface CustomAutowired {
     
}

스프링의 Autowired 어노테이션을 살펴보면 CustomAutowired보다 많은 타깃을 제공하는 것을 볼 수 있습니다. 

@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    boolean required() default true;
}

(동작 방식을 보기 위해 간단하게 구현할 것이기 때문에 Field에만 적용되도록 하였습니다.)

 

그림상의 XML 정의, Application Context를 구동하는 역할은 Main 클래스가 수행합니다. 

public class Main {
    public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        List<String> requiredBeans = Arrays.asList("kr.sys4u.reflection.controller.UserController","kr.sys4u.reflection.repo.UserRepository","kr.sys4u.reflection.service.UserService");
         
        CustomAnnotationContainer container = new CustomAnnotationContainer(requiredBeans) ;
        container.printAllBeans();
         
        System.out.println();
        UserController userController = (UserController) container.get("kr.sys4u.reflection.controller.UserController");
        userController.print();
    }
}

스프링의 컨텍스트 파일은 클래스명으로 빈을 생성하므로, 여기에서는 문자열의 리스트 형태로 생성해야 할 빈의 목록을 제공하겠습니다. 

그리고 이 프로젝트에서 가장 중요한, Application Context의 역할을 수행하는 CustomAnnotationContainer는 아래와 같이 구현하였습니다. 

public class CustomAnnotationContainer {
    private Map<String, Object> instances;
    public CustomAnnotationContainer(List<String> beanNames) {
        DependencyListGenerator dependencyListGenerator = new DependencyListGenerator(beanNames);
        List<String> dependencyList = dependencyListGenerator.generate();
         
        BeanInstantiator beanInstantiator = new BeanInstantiator(beanNames, dependencyList);
        this.instances = beanInstantiator.instantiate();
    }
    public Object get(String beanName){
        return instances.get(beanName);
    }
    public void printAllBeans() {
        instances.forEach((key, value) -> {
            System.out.print("key: " + key);
            System.out.println(", Value: " + value);
        });
    }
}

CustomAnnotationContainer는 생성된 빈 객체들의 목록을 Map으로 가지고 있으며, 클래스명으로 get 요청이 왔을 때 이 객체를 꺼내 주는 역할을 합니다. 

빈 간의 의존성 관계를 확인하여 생성해야 하는 빈의 목록을 만드는 것은 DependencyListGenerator가 수행하며, 빈의 생성과 주입은 BeanInitiator가 수행합니다.

public class DependencyListGenerator {
    private List<String> requiredClasses;
    private List<String> dependencyList;
     
    public DependencyListGenerator(List<String> requiredClasses) {
        this.requiredClasses = requiredClasses;
        this.dependencyList = new ArrayList<>();
    }
     
    public List<String> generate() {
        requiredClasses.forEach(this::generateDependency);
        return dependencyList;
    }
     
    private void generateDependency(String beanName) {
        if(dependencyList.contains(beanName)) {
            return;
        }
         
        dependencyList.add(beanName);
        try {
            Arrays.asList(Class.forName(beanName).getDeclaredFields())
                .stream().sequential()
                .filter(f -> f.isAnnotationPresent(CustomAutowired.class))
                .forEach(field->generateDependency(field.getType().getName()));
        }catch (Exception e) {
            throw new BeanCreationException(e);
        }
    }
}

생성해야 하는 빈의 목록을 생성자로 받아서, 각각 의존성 주입해야 할 객체가 있는지 확인하고 목록에 추가합니다. 이때 만들어진 dependencyList는 결론적으로  입력받은 requiredClasses와 동일한 빈의 목록을 가지고 있으며, 만약 더 많은 빈의 목록을 가지게 된다면 BeanCreationException이 발생하게 됩니다. 이에 대한 확인은 아래 BeanInitiator에서 수행하며, DependencyListGenerator에서 수행하지 않는 이유는 스프링이 그렇게 동작하기 때문입니다. 스프링의 경우 @Configuration과 같은 객체 내에서 또 @Bean 등의 어노테이션을 사용해 빈을 등록할 수 있기 때문에 빈을 생성하는 시점에서 이 것을 확인합니다. 

public class BeanInstantiator {
    private List<String> requiredClasses;
    private List<String> dependencyList;
    private Map<String, Object> instances;
     
    public BeanInstantiator(List<String> requiredClasses, List<String> dependencyList) {
        this.requiredClasses = requiredClasses;
        this.dependencyList = dependencyList;
        instances = new HashMap<>();
    }
     
    public Map<String, Object> instantiate(){
        dependencyList.forEach(this::instantiate);
        return instances;
    }
     
    private Object instantiate(String beanName) {
        Object instance = instances.get(beanName);
        if (instance != null) {
            return instance;
        }
         
        try {
            Class<?> bean = Class.forName(beanName);
            Object newBean = bean.newInstance();
             
            Arrays.asList(bean.getDeclaredFields())
                    .stream().sequential()
                    .filter(f -> f.isAnnotationPresent(CustomAutowired.class))
                    .map(field -> {
                        field.setAccessible(true);
                        return field;
                    })
                    .forEach(field -> {
                        if(!requiredClasses.contains(field.getType().getName())) {
                            throw new BeanCreationException();
                        }
                        Object autowiredField = instances.get(field.getType().getName());
                        Object innerField = autowiredField == null ? instantiate(field.getType().getName()) : autowiredField;
                        try {
                            field.set(newBean, innerField);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    });
            instances.put(beanName, newBean);
            return newBean;
        } catch (Exception e) {
            throw new BeanCreationException(e);
        }
    }
}

BeanInitiator는 dependencyList와 requiredClasses를 생성자로 받아, 빈을 생성하고, 빈을 생성하는 경우에 의존성 주입이 필요하다면 그것 역시 수행합니다. 

 

Controller와 Service, Repository는 빈의 생성과 주입이 제대로 되었는지 확인하기 위해 아래와 같이 간결하게 작성하였습니다. 

//Controller
public class UserController {
    @CustomAutowired
    private UserService userServiceInjectedInField;
     
    public void print() {
        userServiceInjectedInField.printAll();
    }
}
  
//Service
public class UserService {
    @CustomAutowired
    private UserRepository userRepository;
     
    public void printAll() {
        Arrays.stream(userRepository.getAll()).forEach(e->System.out.print(e+" "));
    }
}
  
//Repository
public class UserRepository {
    private String[] data = {
        "Jay","Lee","Kim","Choi","Park","Yoon"
    };
     
    public String[] getAll() {
        return data.clone();
    }
}

메인의 결과는 아래와 같습니다. 

Annotation의 Target 의 ElementType과 Retesion의 RetensionPolicy에 대한 자세한 내용은 각각 링크를 참고하시기 바랍니다. 

'Spring Framework' 카테고리의 다른 글

@Autowired는 어떻게 동작하는 걸까?  (0) 2020.10.12

댓글