본문 바로가기

java/이펙티브 자바

[item 65] 리프렉션보다는 인터페이스를 사용하라

item 65 리프렉션보다는 인터페이스를 사용하라

 

핵심 정리

리플렉션은 복잡한 특수 시스템을 개발할 때 필요한 강력한 기능이지만, 단점도 많다.
컴파일 타임에는 알 수 없는 클래스를 사용하는 프로그램을 작성한다면 리플렉션을 사용해야 한다.
단, 되도록 객체 생성에만 사용하고, 생성한 객체를 이용할 때는 적절한 인터페이스나 컴파일타임에 알 수 있는 상위 클래스로 형변환해 사용해야 한다.

 

리플렉션

실행할떄마다 클래스에 대한 정보를 얻을 수 있다.

java의 고유한 기능이다. 

 

 

 

 

리플랙션(java.lang.reflect)를 이용하면 컴파일 타임에 알 수 없는 임의의 클래스까지 접근을 할 수 있다. 

생성자 메서드 가져올 수 있으며 맴버 이름, 필드 타입도 가져올 수 있다.


Class 객체가 주어지면 클래스 정보를 통해 아래와 같은 인스턴스를 가져올 수 있다.

  • Constructor
    • 생성자 시그니처를 가져올 수 있다.
    • 생성자 인스턴스를 통해 객체를 생성할 수 있다.
  • Method
    • Method 시그니처를 가져올 수 있다.
    • Method 인스턴스를 통해 Method를 실행시킬 수 있다. (Method.invoke)
  • Field
    • 필드타입, 멤버필드 이름등을 가져올 수 있다.

예를 들어 Method.invoke()를 이용해서 블러들인 메소드를 사용할 수 있다.

 

참조: https://calgaryhomeless.tistory.com/25

 

 

 

 

리플렉션의 단점

리플렉션을 이용하면 컴파일 당시에 존재하지 않던 클래스도 이용할 수 있다.
(예를들면.. 외부 라이브러리의 클래스를 리플렉션으로 인스턴스를 생성한다든지…)

 

컴파일타임 타입 검사가 주는 이점을 누릴 수 없다.

  • 존재하지 않는 클래스, 메소드를 호출해도 컴파일타임 검사에서는 발견하지 못해 런타임 오류가 발생한다.
  • 예외 검사, 컴파일 타임 에러를 잡아낼 수 없다.
  • 프로그램이 리플렉션 기능을 써서 존재하지 않는 혹은 접근 불가능한 (private 메서드)를 호출하려 하면 런타임 오류가 발생한다.

리플렉션을 이용하면 코드가 지저분하고 장황해진다.

  • 지루한 일이고 읽기도 어렵다.

성능이 떨어진다.

  • 리플렉션을 이용한 메서드 호출은 일반 메서드 호출보다 훨씬 느리다.
  • 매개변수가 없이 int를 반환하는 메소드도 리플렉션을 이용하면 11배나 느려졌다.
  • 고려해야 하는 요소가 많아 정확한 차이는 이야기하기 어렵다

 

리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피할 수 있다

  • 컴파일타임에 이용할 수 없는 클래스를 사용해야만 하는 프로그램은 비록 컴파일타임이라도 적절한 인터페이스나 상위 클래스를 이용할 수는 있을것이다.
  • 리플렉션은 인스턴스 생성에만 쓰고 이렇게 만든 인터페이스나 상위 클래스로 참조해 사용하자

 

 

 

리플렉션의 취약한 예시를 통한 리플렉션의 두가지 단점

public static void main(String[] args) {
    
    // 클래스 이름을 Class 객체로 변환
    Class<? extends Set<String>> cl = null;
    try {
        cl = (Class<? extends Set<String>>) Class.forName(args[0]); //비검사 형변환
    //forName()에 String 형태의 클래스 파일명을 넣어주면 해당하는 클래스를 반환해준다. 
    //단, 실제 클래스가 만들어진 것은 아니라서 메모리에 올라오지 않았다.
    } catch (ClassNotFoundException e) {
        fatalError("클래스를 찾을 수 없습니다.");
    }
    
    // 생성자를 얻는다.
    Constructor<? extends Set<String>> cons = null;
    try {
        cons = cl.getDeclaredConstructor();//생성자 생성
    } catch (NoSuchMethodException e) {
        fatalError("매개변수 없는 생성자를 찾을 수 없습니다.");
    }
     
    //집합의 인스턴스를 만든다.
    Set<String> s = null;
    try {
        s = cons.newInstance();
        //newInstance() 를 사용해서 클래스의 인스턴스를 만든다
        
    } catch (IllegalAccessException e) {
        fatalError("생성자에 접근할 수 없습니다.");
    } catch (InstantiationException e) {
        fatalError("클래스를 인스턴스화할 수 없습니다.");
    } catch (InvocationTargetException e) {
        fatalError("생성자가 예외를 던졌습니다: " + e.getCause());
    } catch (ClassCastException e) {
        fatalError("Set을 구현하지 않은 클래스입니다.");
    }
    
    //생성한 집합을 사용한다.
    s.addAll(Arrays.asList(args).subList(1, args.length));
    System.out.println(s);
    //java.io.reflect가 제공해준 생성자로 인스턴스를 생성한 이후 부터는 
    //reflect의 기능을 쓰지 않고 인터페이스나 상위 클래스를 참조해서 사용한다.
}

private static void fatalError(String msg) {
    System.err.println(msg);
    System.exit(1);
}

런타임에 총 6가지의 예외를 던진다.

  • 위에서 발생하는 예외는 모두 컴파일타임에 체크할 수 있는 예외들이다.

25줄 <<< 1줄

  • 클래스 이름만으로 인스턴스를 생성해내기 위해 무려 25줄이나 되는 코드를 작성했지만, 그게 아닌 경우에는 생성자 1줄이면 끝난다.
  • 리플렉션 예외를 각각 잡는 대신(위의 예외 6개).... 상위 클래스인 ReflectiveOperationException을 사용해 코드량을 줄일 수 있다. (ReflectiveOperationException은 Java 7부터 지원한다.)

 

 

리플렉션으로 생성자 생성시 비검사 형변환 경고가 뜬다.

클래스의 인스턴스를 생성하려고 할때 ClassCastException을 throw(던지게) 한다. 경고문을 숨기는 방법은 item 27.

 

 

 

 

 

리플렉션을 사용하기 적절한 상황

  • 드물지만 리플렉션은 런타임에 존재하지 않을 수도 있는 다른 클래스, 메서드, 필드와의 의존성을 관리할 때 적합하다.
  • 이 기법은 버전이 여러 개 존재하는 외부 패키지를 다룰 때 유용하다. 가동할 수 있는 최소한의 환경, 즉 주로 가장 오래된 버전만을 지원하도록 컴파일한 후, 이후 버전의 클래스와 메소드 등은 리플렉션으로 접근하는 방식이다.
  • 이런 경우 접근하려는 새로운 클래스나 메서드가 런타임에 존재하지 않을 수 있다는 사실을 감안해서 대체 수단을 만들어 둬야한다.

 

리플렉션은 무조건 쓰지 말아야 한다?

  • Spring MVC, Serialize/Deserialize, BeanUtils.copyProperties등 실무에서 사용하는 코드에 리플렉션이 적용된 예는 굉장히 많다.
  • 단점이 많다고는 하지만 공통적인 기능을 설계하거나, 재사용 가능한 코드를 설계할 경우에는 오히려 리플렉션이 적합할 수 있다.
  • 그렇기 때문에 Java 1.3 이후부터 리플렉션에 대한 성능향상을 발전시켜왔다고 한다.
  • 이러한 발전으로 리플렉션은 우려할 만큼 성능이 떨어지지는 않는다고 한다.
  • 리플렉션을 남발하는 것이 아닌 필요한 상황에 적시적소에 사용한다면 오히려 서비스 개발을 더 단순화 시킬수 있다.

참고