DEV ℧ Developer Diary

[EffectiveJava] item65 - 리플렉션보다는 인터페이스를 사용하라

리플렉션 기능(java.lang.reflect)을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있다.

리플렉션 API(Reflect API)

Class 객체가 주어지면 그 클래스의 생성자(Constructor), 메서드(Method), 필드(Field)에 해당하는 인스턴스를 가져 올 수 있고, 이 인스턴스들로는 그 클래스의 멤버 이름, 필드 타입, 메서드 시그니처등을 가져올 수 있다.

나아가 Constructor, Method, Field 인스턴스를 이용해 각각에 연결된 실제 생성자, 메서드, 플드를 조작할 수도 있다.

Method.invoke

Method.invoke는 어떤 클래스의 어떤 객체가 가진 어떤 메서드라도 호출 할 수 있게 해준다. 리플렉션을 이용하면 컴파일 당시에 존재하지 않던 클래스도 이용할 수 있다.

리플렉션의 단점

  • 컴파일타임 타입 검사가 주는 이점을 하나도 누릴 수 없다. 존재하지 않거나, 접근할 수 없는 메서드를 호출하려 시도하면 런타임 오류가 발생한다.
  • 리플렉션을 이용하면 코드가 지저분하고 장황해진다.
  • 성능이 떨어진다. 리플렉션을 통한 메서드 호출은 일반 메서드 호출보다 훨씬 느리다.

코드 분석 도구나 의존관계 주입 프레임워크처럼 리플렉션을 쓰는 복잡한 애플리케이션이 있지만, 이런 도구마저저 리플렉션 사용을 점차 줄이고 있다.

리플렉션의 활용법

리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피하고 이점만 취할 수 있다. 컴파일 타임에 이용할 수 없는 클래스 사용 해야만 하는 프로그램은 컴파일타임 이라도 적절한 인터페이스나 상위 클래스를 이용할 수 있을 것이다.

이런 경우라면 리플렉션은 인스턴스 생성에만 쓰고, 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자.

예시

다음 프로그램은 Set 인터페이스의 인스턴스를 생성하고, `java.util.HashSet`이나 `java.util.TreeSet`과 같은 지정한 클래스가 무엇이냐에 따라 달라진다.

public class ReflectTest {
    public static void main(String[] args) {
        /** 클래스 이름을 Class 객체로 변환 */
        Class<? extends Set<String>> cl = null;
        try {
            cl = (Class<? extends Set<String>>) /** 비검사 형변환 */
                    Class.forName(args[0]);
        } catch (ClassNotFoundException e) {
            fetalError("클래스를 찾을 수 없습니다.");
        }

        /** 생성자를 얻는다. */
        Constructor<? extends Set<String>> cons = null;
        try {
            cons = cl.getDeclaredConstructor();
        } catch (NoSuchMethodException e) {
            fetalError("매개변수 없는 생성자를 찾을 수 없습니다.");
        }

        /** 집합의 인스턴스를 만든다. */
        Set<String> s = null;
        try {
            s = cons.newInstance();
        } catch (IllegalAccessException e) {
            fetalError("생성자에 접근할 수 없습니다.");
        } catch (InstantiationException e) {
            fetalError("클래스를 인스턴스화할 수 없습니다.");
        } catch (InvocationTargetException e) {
            fetalError("생성자가 예외를 던졌습니다:" + e.getCause());
        } catch (ClassCastException e) {
            fetalError("Set을 구현하지 않은 클래스입니다.");
        }

        /** 생성한 집합을 사용한다. */
        s.addAll(Arrays.asList(args).subList(1, args.length));
        System.out.println(s);
    }

    private static void fetalError(String msg) {
        System.out.println(msg);
        System.exit(1);
    }
}

간단한 프로그램이지만, 이 프로그램은 손쉽게 Set 규약을 잘 지키는지 검사하는 제네릭 집합 테스터로 변신할 수있다.
대부분의 경우 리플렉션 기능은 이정도만 사용해도 충분하다.

예시의 단점

이 예는 리플렉션의 단점 두 가지를 보여준다.

  • 첫 번째, 런타임에 총 여섯가지나 되는 예외를 던질 수 있다. 인스턴스를 리플렉션 없이 생성했다면 컴파일 타임에는 잡아낼 수 있었을 예외들이다.
  • 두 번째, 클래스 이름만으로 인스턴스를 생성해내기 위해 무려 25줄이나 되는 코드를 작성했다. 리플렉션이 아니면 한줄로 끝났을 것이다.

자바 7 이후에는 ReflectiveOperationException을 통해 모든 리플렉션 예외를 한줄로 잡을 수 있다.

이 프로그램은 컴파일 하면 비검사 형변환 경고가 뜬다. 하지만 Class<? extends Set<String>> 으로의 형변환은 명시한 클래스가 Set을 구현하지 않았더라도 성공할 것이라, 실제 문제로 이어지지는 않는다.
단 그 클래스의 인스턴스를 생성할 때 ClassCastException을 던질테니 경고를 숨겨줘야 한다.

리플렉션이 적합할 때

리플렉션은 드물긴 하지만, 런타임에 존재하지 않을 수도 있는 다른 클래스, 메서드, 필드와의 의존성을 관리할 때 적합하다. 이기법은 버전이 여러 개 존재하는 외부 패키지를 다룰 때 유용하다.

가동할 수 있는 최소한의 환경, 즉 주로 가장 오래된 버전만을 지원하도록 컴파일한 후, 이후 버전의 클래스와 메서드 등은 리플렉션으로 접근하는 방식이다.