작년 이맘때 궁금하지만 어려워서 넘어갔던 기술들이 있었습니다. 대표적으로 엘라스틱 서치와 리플렉션이 있었는데 최근에 하나씩 공부하면서 지난번과 달리 비교적 쉽게 체득할 수 있었는데요. 그 이유는 학습 방법의 변화 덕분입니다.

작년에 리플렉션을 공부할 때는 무작정 개념과 사용방법만 익히려고 했는데 이번에는 Why? 에 초점을 맞춰서 공부했더니 더 쉽게 이해하고 활용을 잘할 수 있게 되었습니다.

'이 기술은 왜 나왔을까? 왜 사용할까?' 를 이해하는 것은 중요합니다. 그래서 작년의 저와 같은 사람에게 도움이 되었으면 해서 리플렉션이 무엇인지 정리를 해보기로 했습니다.

리플렉션의 사용방법은 다루지 않습니다. 사용 방법은 아래 링크를 추천합니다

리플렉션이란 ?

리플렉션은 구체적인 클래스 타입을 알지 못해도, 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API

여기서 구체적인 클래스 타입을 알지 못해도 가 무슨 말일까요? 저는 이 부분에서 이해하기가 정말 어려웠습니다.
'내가 작성한 코드인데 왜 몰라? 당연히 아는 거 아냐?' 이런 생각이 들었지만, 나중에서야 접근방법이 잘못된 것을 알 수 있었습니다. 이 문장을 이해하려면 리플렉션을 언제, 왜 사용해야 하는지 알아야 합니다.

Why?

자바는 정적인 언어라 부족한 부분이 많은데 이 동적인 문제를 해결하기 위해서 리플렉션을 사용합니다.

정적 언어, 동적 언어 ?

  • 정적 언어: 컴파일 시점에 타입을 결정 ex) Java, C, C++ 등..
  • 동적 언어: 런타임 시점에 타입을 결정 ex) Javascript, Python, Ruby 등..

리플렉션은 애플리케이션 개발에서보다는 프레임워크, 라이브러리에서 많이 사용됩니다.
프레임워크, 라이브러리는 사용하는 사람이 어떤 클래스를 만들지 모릅니다. 이럴 때 동적으로 해결해주기 위해서 리플렉션을 사용합니다.
대표적인 사용 예로는 스프링의 DI(dpendency injection), Proxy, ModelMapper 등이 있습니다.

코드로 예를 볼까요?

@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) {
       ...
    }
}

스프링을 사용할 때 @Controller 를 넣어주면 인스턴스를 생성 하지 않아도 스프링이 알아서 생성해서 빈으로 관리해줍니다.

  • 스프링은 ArticleController의 존재를 어떻게 알고 만들어주는 것일까요?
  • ArticleService 라는 필드는 어떻게 주입해주는 걸까요?
  • 모든 메소드의 파라미터 개수, 타입이 다른데 어떻게 알고 해당하는 값을 바인딩 해줄까요?

ArticleController을 작성한 개발자는 클래스의 정보를 알겠지만, 스프링은 모릅니다.
이 문제를 해결하기 위해서 리플렉션을 사용합니다. (스프링이 ArticleController의 정보를 알아내기 위해서)

대략 흐름을 보자면

  • @Controller 를 갖고있는 클래스를 스캔
  • 해당하는 클래스의 인스턴스 생성 및 필드 DI

How?

간단한 사용법

그럼 어떻게 사용하는지 간단하게 보겠습니다.

class Article {
   private int id;
   private String title;
   private LocalDateTime date;
}

Article이라는 클래스가 있습니다.
이 클래스의 필드 정보를 출력하겠습니다.

Class<?> clazz = Article.class;  // Article의 Class를 가져온다.
Field[] fields = clazz.getDeclaredFields(); // Article의 모든 필드를 가져온다.

for (final Field field : fields) { // field의 type, name 출력
    System.out.printf("%s %s\n", field.getType(), field.getName());
}

출력 결과

int id
class java.lang.String title
class java.time.LocalDateTime date

이런 방식으로 사용하는 데요. 사실 이 방식은 Article.class 을 직접 명시해줬으므로 처음에 말했던 '내가 작성한 코드인데 왜 몰라? 당연히 아는 거 아냐?' 와 일치합니다. 그래서 한 번 프레임워크 기준에서 작성해보겠습니다.

간단한 DI 프레임워크

이번 예시로는 간단하게 스프링처럼 DI를 구현 하겠습니다.

아래 코드는 백기선님의 리플렉션 강의를 참고해서 작성했습니다.

먼저 AutoWried 어노테이션을 만들어주고

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

ContainerService (인스턴스를 생성하고 필드를 DI 해주는 역할)

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);
    }
  }
}

이로써 간단한 DI 프레임워크가 만들어졌습니다.

이제는 이 프레임워크를 사용하는 애플리케이션 개발자 관점에서 코드를 작성하겠습니다.

ArticleController, ArticleService

public class ArticleController {
  @AutoWired
  private ArticleService articleService;

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


public class ArticleService {

  public void foo() {
    System.out.println("call foo");
  }
}

프레임워크를 이용해서 ArticleController 인스턴스 생성

public static void main(String[] args){
      ContainerService containerService = new ContainerService();

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

      articleController.foo();
}

실행결과

call foo

여기까지 DI 프레임워크를 구현해 봤는데요. 여기서 ArticleController.class 를 직접 명시해줬지만 ArticleService에 대해서는 직접 명시해주지 않았는데 알아서 생성해서 주입해줬습니다.
조금 더 한다면 @Controller 어노테이션을 만들어서 해당하는 클래스를 가져와서
containerService.getObject()ArticleController.class 를 직접 명시하지 않고도 할 수 있습니다. 현재는 어떠한 흐름이다만 보여주기 위해서 이 부분은 생략했습니다.

추가로 리플렉션을 사용하면서 주의해야할 점이 몇가지 있습니다.

  • 지나친 사용은 성능 이슈를 야기할 수 있다. 반드시 필요한 경우에만 사용할 것
    • 이미 인스턴스를 만들었음에도 불구하고 굳이 필드와 리플렉션을 이용해서 접근하거나 사용할 경우
  • 컴파일 타임에 확인되지 않고 런타임 시에만 발생하는 문제를 만들 가능성이 있다.
  • 접근 지시자를 무시할 수 있다.

리플렉션은..

컴파일한 클래스 정보를 활용해 동적으로 프로그래밍이 가능하도록 지원하는 API

Refereneces