리플렉션(Reflection) Feat. Di 만들기

Mugeon Kim·2023년 8월 12일
0

서론


  • 최근 면접을 진행하면서 DI의 질문을 받았습니다.
  • 당시 DI의 개념과 @component에 대한 설명을 하였지만 스프링이 어떻게 컴포넌트를 스캔을 하는지 대답을 하지 못하였다.
  • 이후 면접이 끝나고 학습을 하면서 리플렉션의 개념이 필요했고 DI를 직접 만들었습니다.
  • 이번 게시글에서는 ReflectionIOC DI에 대하여 다시 한번 학습하고 Di Container를 직접 만들고 정리해본다.

본론


1-1. 리플랙션(Reflection)

  • JVM은 클래스 정보를 클래스 로더를 통해 읽어와서 해당 정보를 JVM 메모리에 저장한다. 그렇게 저장된 클래스에 대한 정보가 마치 거울에 투영된 모습과 닮아있어 리플랙션이라 불린다.

  • 리플랙션을 이용하면 생성자, 메소드, 필드 등 클래스에 대한 정보를 알 수 있습니다.

  • 리플랙션은 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API이며 컴파일 시간이 아닌 실행 시간에 동적으로 특정 클래스의 정보를 추출할 수 있는 프로그래밍 기법이다.

리플렉션을 언제 사용할까?
1. 동적으로 클래스를 사용해야할 때
2. 작성을 할때에는 어떤 클래스를 사용할지 모르지만 런타임 시점에서 가져와 사용할 때

1-2. 리플랙션 코드 설명

  • 클래스의 객체를 흭득하는 방법은 3가지가 있습니다.
  1. 클래스의 class를 통하여 프로퍼티를 흭득한다.
  2. 인스턴스에 getClass()를 통하여 흭득한다.
  3. 클래스의 forName()을 통하여 FQCN(Fully Qualified Class Name)를 전달하여 해당 경로와 대응하는 클래스에 대한 Class 클래스의 인스턴스를 얻는 방법이다.
        Class<UserDomain> userDomainClass = UserDomain.class;
        System.out.println(" 방법 1 = " + userDomainClass);


        UserDomain userDomain = new UserDomain();
        //Class<? extends UserDomain> user = UserDomain.class;
        Class<? extends UserDomain> user = userDomain.getClass();
        System.out.println(" 방법 2 = " + user);

        Class<?> aClass = null;
        try {
            aClass = Class.forName("com.example.dipractice.test.UserDomain");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        System.out.println(" 방법 = 3 " + aClass);
  • 결과
 방법 1 = class com.example.dipractice.test.UserDomain
 방법 2 = class com.example.dipractice.test.UserDomain
 방법 3 = class com.example.dipractice.test.UserDomain

2-1. IOC

Spring Bean

  • 스프링은 제어의 역전(IOC)로 객체를 관리한다. 이때 스프링이 제어 권한을 가져 직접 생성하고 관계를 부여하는 개체를 bEAN이라고 한다.
  • @Bean을 사용해 등록하거나 xml을 통하여 등록하고 등록된 객체는 쉽게 주입하여 사용이 가능하다.

Bean 생명주기
스프링 컨테이너 생성
-> 스프링 빈 생성
-> 의존 관계 주입
-> 초기화 콜백
-> 사용
-> 소멸전 콜백
-> 스프링 종료

  • 생성과 의존관계 주입과 초기화 분리
    • 생성자 주입은 필수정보를 받고 메모리 할당을 통해 객체 생성한다.
    • 초기화는 생성된 값들을 활용해 외부 커넥션을 연결하는 등 무거운 작업
    • 명확하게 분리하는게 유지보수 관점에서 좋다.

IOC

출처 : Spring 공식문서

  • Spring IOC Container는 스프링 핵심 중 하나이다. 공식문서를 살펴보면 Bean의 인스턴스화, 구성, 조립을 담당합니다. 응용 프로그램을 구성하는 개체와 해당 개체간의 풍부한 상호 종속성을 표현할 수 있다고 작성이 되어져있다.

  • 즉 스프링 컨테이너는 객체의 라이프 사이클을 관리하며 DI를 통해서 의존성을 관리할 수 있다.

  • Spring IOC 컨테이너로 BeanFactory, ApplicationContext가 사용이 되며 BeanFactory는 프레임워크와 기본 기능을 제공하고ApplicationContext는 더 많은 기능을 제공을 한다.

2-2. DI

  • DI는 스프링 프레임워크에서 지원하는 IOC의 형태이다. 클래스 사이의 의존 고나계를 빈 설정 정보를 바탕으로 컨테이너가 자동으로 연결을 해준다.

장점

  1. 스프링 자체에서 설정을 통해 연관 관계를 맺어줌으로써 객체간 결합도를 낮춰준다.
  2. 클래스의 재사용성을 높이고, 유지보수가 편리해진다.
  3. 의존성 주입으로 인해 stub, mock 객체를 사용해 unit 테스트의 이점이 생긴다.

단점

  1. 의존성 주입을 위한 선행 작업이 필요해 간단한 프로그램에서는 번거롭다.
  2. 코드 추적이 어렵다.

스프링

  • Controller, Service, Repository 어노테이션을 살펴보면 @componet가 등록되어 있다. @compoent는 Bean으로 등록하고 싶은 클래스에 명시를 해준다. 위 어노테이션이 분으면 스프링 프레임워크가 Scan을 하여 Bean을 찾고 등록한다.

  • 이때 Scan하는 방식으로는 Reflection을 이용해서 빈을 Find, 추가를 한다.

https://www.baeldung.com/spring-component-scanning

@SpringBootApplication

  • @SpringBootApplication 은 3개의 주석의 조합입니다.
@Configuration
@EnableAutoConfiguration
@ComponentScan
  • @ComponentScan은 지정된 패키지 및 하위 패키지에서 Spring의 컴포넌트들을 스캔하도록 지시합니다. 컴포넌트 스캔은 @Component 어노테이션 뿐만 아니라, @Controller, @Service, @Repository 등 Spring에서 제공하는 다양한 어노테이션들도 스캔 대상에 포함합니다. 이 어노테이션을 사용하면 스캔된 컴포넌트들이 Spring 컨테이너에 자동으로 등록됩니다.

  • 이때 하위 패키지에 등록하는 방식이 Reflection을 사용을 했다고 이해하면 된다.

2-3. DI 리플랙션 코드

Github 소스 - https://github.com/KMGeon/SpringPlayGround/tree/main/di-practice

Custom 어노테이션

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

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

@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
  • Target : 애너테이션이 적용가능한 대상을 지정하는데 사용한다.
    • ElementType.TYPE : 아래 사진을 살펴보면 TYPE을
public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,
    ...
}
  • Retention : 애너테이션이 유지되는 범위를 지정하는데 사용한다.
    • RetentionPolicy.RUNTIME은 런타임 유지기간을 설정을 합니다.

Custom BeanFactory


public class BeanFactory {
    //class 타입 객체
    private Set<Class<?>> preInstantiatedBeans;

    //class 타입을 키 인스턴스를 value
    private Map<Class<?>, Object> beans = new HashMap<>();

    public BeanFactory(Set<Class<?>> preInstantiatedBeans) {
        this.preInstantiatedBeans = preInstantiatedBeans;
        initialize();
    }

    // class 타입 객체를 가지고 인스턴스를 가지고 초기화
    public void initialize() {
        for (Class<?> clazz : preInstantiatedBeans) {
            Object instance = createInstance(clazz);
            beans.put(clazz, instance);
        }
    }

    private Object createInstance(Class<?> concreteClass) {
        //생성자
        Constructor<?> constructor = findConstructor(concreteClass);

        //파라미터
        List<Object> parameters = new ArrayList<>();
        for (Class<?> typeClass : Objects.requireNonNull(constructor).getParameterTypes()) {
            parameters.add(getBean(typeClass));
        }

        //인스턴스 생성
        try {
            return constructor.newInstance(parameters.toArray());
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    //inject가 붙은 어노테이션을 가져온다.
    private Constructor<?> findConstructor(Class<?> concreteClass) {
        Constructor<?> constructor = getInjectedConstructor(concreteClass);

        if (Objects.nonNull(constructor)) {
            return constructor;
        }

        return concreteClass.getConstructors()[0];
    }

    public <T> T getBean(Class<T> requiredType) {
        return (T) beans.get(requiredType);
    }

    public  Constructor<?> getInjectedConstructor(Class<?> clazz) {
        Set<Constructor> injectedConstructors = getAllConstructors(clazz, withAnnotation(Inject.class));
        if (injectedConstructors.isEmpty()) {
            return null;
        }
        return injectedConstructors.iterator().next();
    }
}

테스트 코드

class BeanFactoryTest {
    private Reflections reflections;
    private BeanFactory beanFactory;

    @BeforeEach
    void setUp() {
        //reflection 대상
        reflections = new Reflections("com.example.dipractice");
        // controller, service 조회
        Set<Class<?>> typesAnnotatedWith = getTypesAnnotatedWith(Controller.class, Service.class);
        beanFactory = new BeanFactory(typesAnnotatedWith);
    }

    private Set<Class<?>> getTypesAnnotatedWith(Class<? extends Annotation>... annotations) {
        Set<Class<?>> beans = new HashSet<>();
        for (Class<? extends Annotation> annotation : annotations) {
            beans.addAll(reflections.getTypesAnnotatedWith(annotation));
        }

        return beans;
    }

    @Test
    @DisplayName("diTest")
    public void diTest() throws Exception {
        //given
        UserController userController = beanFactory.getBean(UserController.class);
        //when
        //Then
        Assertions.assertThat(userController).isNotNull();
    }

}
  • 해당 코드를 살펴보면 package를 작성하여 하위 클래스의 Controller service를 조회를 합니다.
  • 이후 BeanFactory에 클래스와 인스턴스를 초기화를 하는 작업을 수행을 했습니다.

참고


https://hudi.blog/java-reflection/

https://www.youtube.com/watch?v=Q-8FC09OSYg

https://www.youtube.com/watch?v=67YdHbPZJn4

https://docs.spring.io/spring-framework/reference/core/beans/introduction.html

profile
빠르게 실패하고 자세하게 학습하기

1개의 댓글

comment-user-thumbnail
2023년 8월 12일

많은 도움이 되었습니다, 감사합니다.

답글 달기