본문 바로가기

Develop/JAVA

자바 reflection에 대하여

리플렉션(Reflection)은 이미 로딩이 완료된 클래스에서 동적으로 로딩하여 생성자, 멤버 필드 그리고 멤버 메서드 등을 사용할 수 있도록 도와준다. 따라서 컴파일 시간이 아니라 런타임에 동적으로 특정 클래스의 정보를 객체화하여 분석 및 추출해낼 수 있는 프로그래밍 기법이다.

public class Car {
	public void drive() {
    	//...
	}
}

public class Main {
	public static void main(String[] args) {
    	Object car = new Car();
        car.drive(); // 컴파일 에러 발생!
	}
}

위 코드에서 car 변수는 Object 타입으로 선언되었기 때문에 하위 타입인 Car 클래스의 인스턴스를 담을 수 있다. 하지만 car 변수를 통해 사용 가능한 메서드는 Object 타입의 것들이다. 따라서 Car 클래스에 선언된 메서드는 사용할 수 없다.

 

이는 자바가 컴파일 타임에 클래스 타입이 결정되고 실제 런타임에는 어떤 클래스 타입이 사용될 지 모를 수 있다는 걸 의미한다. 이러한 상황에 사용할 수 있는 방법이 리플렉션이다.

Class vectorClass = Class.forName("java.util.Vector");

  Method[] methods = vectorClass.getDeclaredMethods();

  /* 임의의 메서드 지정, 이름으로 확인 */
  Method method = methods[25];
  System.out.println("Class Name : " + method.getDeclaringClass());
  System.out.println("Method Name : " + method.getName());
  System.out.println("Return Type : " + method.getReturnType());

  /* Parameter Types */
  Class[] paramTypes = method.getParameterTypes();
  for(Class paramType : paramTypes) {
  	System.out.println("Param Type : " + paramType);
  }

  /* Exception Types */
  Class[] exceptionTypes = method.getExceptionTypes();
  for(Class exceptionType : exceptionTypes) {
    System.out.println("Exception Type : " + exceptionType);
}

구체적인 사용 방법은 위와 같다.

클래스 로더에 의해 로딩된 자바 코드는 컴파일 과정에서 바이트코드로 변경되어 JVM 메모리에 저장된다. 이때 클래스에 대한 정보는 Heap 영역, Metaspace에 저장된다. 자바는 런타임에 해당 영역에 접근하여 클래스에 대한 다양한 정보를 받아올 수 있다. 이것이 리플렉션의 기본적인 원리이다.

실제 사용 예시

대표적인 사용 예시는 스프링의 DI(dependency injection), Proxy, Model Mapper 등이 있다.

프레임워크 또는 라이브러리는 개발자가 어떤 클래스를 작성할지 알 수 없다. 이런 경우 동적으로 해결하기 위해 리플렉션을 사용한다.

 

@Controller
@RequestMapping("/articles")
public class ArticleController {    

    @Autowired    
    private ArticleService articleService;       
       ....

    @PostMapping
    public String write(UserSession userSession, ArticleDto.Request articleDto){
       ...
    }

    @GetMapping("/{id}")
    public String show(@PathVariable int id, Model model) {
       ...
    }
}

위 코드는 스프링 DI를 활용한 예시이다. 이때 스프링은 어떻게 ArticleService의 존재를 알고 의존관계를 넣어줄까?

코드를 직접 작성한 개발자는 ArticleService 클래스에 대한 정보를 알고 있지만 스프링을 그러지 못하다. 따라서 리플렉션을 활용한다!

 

아래부터는 직접 스프링 DI를 구현해보며 그 원리를 파악해본다.

@Retention(RetentionPolicy.RUNTIME)
public @interface AutoWired {
}
public class ContainerService {
  public static <T> T getObject(Class<T> classType) {
    // 기본생성자를 통해서 인스턴스를 만든다.
    T instance = createInstance(classType);

    // 클래스의 모든 필드를 불러온다.
    Stream.of(classType.getDeclaredFields())
      .filter(field -> field.isAnnotationPresent(AutoWired.class)) // 어노테이션에 AutoWired를 갖는 필드만 필터
      .forEach(field -> {
        try {
          // 필드의 인스턴스 생성
          Object fieldInstance = createInstance(field.getType());
          // 필드의 접근제어자가 private인 경우 수정가능하게 설정
          field.setAccessible(true);
          // 인스턴스에 생성된 필드 주입
          field.set(instance, fieldInstance);
        } catch (IllegalAccessException e) {
          throw new RuntimeException(e);
        }
      });
    return instance;
  }

  private static <T> T createInstance(final Class<T> classType) {
    try {
      // 해당 클래스 타입의 기본생성자로 인스턴스 생성
      return classType.getConstructor().newInstance();
    } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
      throw new RuntimeException(e);
    }
  }
}

먼저 @Autuwired 어노테이션을 만들어준다.

또한 ContainerService는 인스턴스를 생성하고 DI를 해주는 역할을 맡는다.

 

public class ArticleController {
  @AutoWired
  private ArticleService articleService;

  public void foo(){
    articleService.foo();
  }
}


public class ArticleService {

  public void foo() {
    System.out.println("call foo");
  }
}
public static void main(String[] args){
      ContainerService containerService = new ContainerService();

      ArticleController articleController = containerService.getObject(ArticleController.class);

      articleController.foo();
}
call foo

결과는 call foo 가 제대로 출력된다.

이때 getObject를 메서드를 호출하면서 ArticleController 클래스에 대한 정보는 명시해주었다.

하지만 ArticleController 클래스 내부에서 사용하는 ArticleService 클래스는 따로 명시해주지 않았는데도 불구하고 해당 클래스의 메서드를 정상적으로 사용할 수 있었다. 이유는 리플렉션을 활용해 @Autowired가 붙은 필드를 직접 주입해주었기 때문이다.

리플렉션의 단점

이러한 리플렉션도 단점이 존재한다.

 

- 컴파일 타임에 확인되지 않고 런타임에만 발생하는 문제를 야기할 수 있다.

- 접근 지시자(public, protected, private)을 무시할 수 있다.

- 저렴한 자원이 아니기 때문에 성능 이슈가 발생할 수 있다.