[Spring Framework] Java 리플렉션을 이용한 Dependency Injection(의존성 주입) 예제 로직 구현

스프링의 가장 중요한 개념인 DI객체라는 것이 있다.

이는 Dependency Injection이라는 용어의 약자로써, A 클래스가 멤버로써 다른 B 클래스를 가지고 있을 때 A클래스는 B클래스가 존재하지 않으면 안된다. -> 즉 의존하고 있다. 라는 의미로 "B 클래스는 A 클래스의 의존객체이다." 라고 합니다.

 

스프링은 이러한 의존성을 주입해주는 방법으로 new 키워드 생성자를 통한 인스턴스화가 아닌 스프링이 Bean이라는 이름으로 관리하는 객체들을 주로 @Autowired라는 어노테이션을 이용해서 주입시켜주게 됩니다.(혹은 생성자, Setter 주입) 이때, 스프링이 관리하는 Bean이라는 객체들은 싱글톤(singleton) 객체로써 어느 클래스에서 주입받던지 단 하나의 인스턴스입니다.

 

즉, 다음과 같은 코드는 유효하게 동작합니다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyServiceTest {
    @Autowired
    MyService myService;

    @Test
    public void di(){
        Assert.assertNotNull(myService);
    }
}

기존의 자바코딩으로 하던 방법이라면 myService라는 변수에는 그 어떤 초기화도 일어나지 않았습니다. 그런데 아래 테스트를 해보면 myService는 null이 아닌 실제 인스턴스를 참조하고 있습니다.

 

이런 것이 어떻게 가능했을 지, 자바의 리플렉션을 이용해서 구현해보려고 합니다. (실제로 스프링이 이렇게 구현되어있다는 뜻이 절!대!로! 아닙니다.!)

 

 

@Inject

- 예제에서 사용할 Dependency Injection을 구현할 어노테이션입니다. 실제로 @Autowired 처럼 인스턴스를 주입해줄 어노테이션입니다.

@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}

 

 

MyService, MyRepository 클래스

public class MyService {

    @Inject
    MyRepository myRepository;
}

public class MyRepository { }

- MyService에 MyRepository 클래스를 선언하여 주입하여 다음 테스트를 통과해보겠습니다.

 

 

테스트코드

import com.example.demo.di.ContainerService;
import org.junit.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;

public class ContainerServiceTest {
    @Test
    public void getObject_MyRepository(){
        MyRepository object = ContainerService.getObject(MyRepository.class);
        assertNotNull(object);
    }
    @Test
    public void getObject_MyService(){
        MyService object = ContainerService.getObject(MyService.class);
        assertNotNull(object);
        assertNotNull(object.myRepository);
    }
}

- 첫번째 테스트 코드는 Class 타입의 MyRepository 객체를 매개값으로 넣어 실제 MyRepository 인스턴스를 얻어보겠습니다.

- 두번째 테스트 코드는 Class 타입의 MyService 객체를 통해 MyService 인스턴스와 멤버변수로 선언되어 있는 MyRepository에도 인스턴스가 참조되어있는지 확인해보는 코드입니다.

 

테스트가 정상적으로 작동하기위한 코드를 보겠습니다.

 

ContainerService 클래스

import java.util.Arrays;
import java.util.Objects;

public class ContainerService {
	
    //매개값으로 들어온 타입의 인스턴스를 반환하는 메소드
    public static <T> T getObject(Class<T> classType){
        T instance = createInstance(classType);
        Arrays.stream(classType.getDeclaredFields()).forEach(f -> {
        
            if(Objects.nonNull(f.getAnnotation(Inject.class))){
                Object fieldInstance = createInstance(f.getType());
                f.setAccessible(true);
                try {
                    f.set(instance, fieldInstance);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        return instance;
    }
	
    //인스턴스를 만드는 메소드
    private static <T> T createInstance(Class<T> classType) {
        try {
            return classType.getConstructor(null).newInstance();
        } catch(Exception e){
            throw new RuntimeException(e);
        }
    }
}

- 일단 메소드를 두개로 분리하였습니다.

- createInstance() 메소드는 클래스타입의 매개값을 받아 해당 타입의 NoArgumentConstructor(getConstructor(null))를 통해 인스턴스를 생성하는 구문으로 인스턴스를 반환해주는 메소드입니다. 실제로 getObject 메소드 내부에 같이 로직이 있어도 되지만 따로 메소드로 구분하여 정의합니다. 다른 로직이 필요할 가능성이 존재하기 때문입니다.

- getObject() 메소드는 간단히 설명하자면 getDeclaredFields() 메소드는 private/public 상관 없이 모든 선언된 필드들을 배열로써 가져옵니다. 이를 stream api를 이용하여 forEach() 메소드를 통해 루프를 돌아줍니다. 그중에서 Annotation 중 Inject.class 타입을 가지는 필드가 있다면 if문으로 들어와서 해당 필드타입의 인스턴스를 생성해주고 필드의 setAccessible(true) 를 통해 private 접근자라도 접근하여 set() 해줄수 있도록 합니다. set() 메소드의 인자로는 (필드가 속한 인스턴스, 필드에 넣어줄 인스턴스) 입니다.

 

 

리플렉션이란...?

이러한 예제는 자바의 reflection이라는 기술을 이용한 것입니다. 리플렉션이란 실제 클래스로더에 의해 클래스가 로딩될 때 자바 바이트코드를 읽게 되는데 소스코드를 이용해서 이러한 부분을 제어할 수 있는 강력한 기술입니다. 따라서 바이트코드를 이용한 프로그래밍 라이브러리인 ASM이나 ByteBuddy를 이용해서 .java 파일에서 코딩하는 것이 아닌 클래스 바이트코드 단과 즉, 머신 우리의 컴퓨터와 보다 가까운 코딩을 접해볼 수 있습니다. 

 

 

댓글

Designed by JB FACTORY