[교재 EffectiveJava] 아이템 65. 리플렉션보다는 인터페이스를 사용하라

반응형
728x90
반응형

리플렉션

리플렉션 기능(java.lang.reflect)을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있다. Class 객체가 주어지면 그 클래스의 생성자, 메서드 필드에 해당하는 Constructor, Method, Field 인스턴스를 가져올 수 있고, 이어서 이 인스턴스들로는 그 클래스의 멤버 이름, 필드 타입, 메서드 시그니처 등을 가져올 수 있다. 또한 각 인스턴스들을 사용하여 각각에 연결된 실제 생성자, 메서드, 필드를 조작할 수도 있다. 이 인스턴스들을 통해 해당 클래스의 인스턴스를 생성하거나, 메서드를 호출하거나, 필드에 접근할 수 있다. 

 

 

리플렉션 단점

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

리플렉션 기능을 써서 존재하지 않는 혹은 접근할 수 없는 메서드를 호출하려 시도하면 런타임 오류가 발생한다. 

 

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

읽기 어렵다.

 

3) 성능이 떨어진다.

일반 메서드 호출보다 훨씬 느리다. 

 

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

 

 

생성은 리플렉션, 참조는 인터페이스

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

 

다음 예제 코드를 보자.

1) Set<String> 인터페이스의 인스턴스를 생성한다. 정확한 클래스는 명령줄의 첫번째 인수로 확정한다.
2) 생성한 집합(Set)에 두번째 이후의 인수들을 추가한 다음 화면에 출력한다.
3) 첫번째 인수와 상관없이 이후의 인수들을 추가한 다음 화면에 출력한다.
4) 반면, 이 인수들이 출력되는 순서는 첫번째 인수로 지정한 클래스가 무엇이냐에 따라 달라진다.

HashSet 이라면 순서가 무작위일 것이고, TressSet을 지정하면 순서가 정렬되어 알파벳 순서로 출력될 것이다.

 

리플렉션으로 생성하고 인터페이스로 참조해 활용한다.
package com.java.effective.item65;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Set;

public class Main {
    // 코드 65-1 리플렉션으로 생성하고 인터페이스로 참조해 활용한다. (372-373쪽)
    public static void main(String[] args) {


        // 클래스 이름을 Class 객체로 변환
        Class<? extends Set<String>> cl = null;
        try {
            cl = (Class<? extends Set<String>>)  // 비검사 형변환!
                    Class.forName("java.util.HashSet");
        } 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();
        } catch (IllegalAccessException e) {
            fatalError("생성자에 접근할 수 없습니다.");
        } catch (InstantiationException e) {
            fatalError("클래스를 인스턴스화할 수 없습니다.");
        } catch (InvocationTargetException e) {
            fatalError("생성자가 예외를 던졌습니다: " + e.getCause());
        } catch (ClassCastException e) {
            fatalError("Set을 구현하지 않은 클래스입니다.");
        }

        // 생성한 집합을 사용한다.
        s.addAll(Arrays.asList("a","b","c").subList(1, 3));
        System.out.println(s);
    }

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

 

위 코드의 객체 생성 시점에, 단점이 2가지 존재한다. 

1) 런타임에 총 6가지나 되는 예외를 던질 수 있다.

그 모두가 인스턴스를 리플렉션 없이 생성했다면 컴파일 타임에 잡아낼 수 있었을 예외들이다.

 

2) 클래스 이름만으로 인스턴스를 생성해내기 위해 무려 25줄이나 되는 코드를 작성했다.

리플렉션이 아니라면 생성자 호출 한줄로 끝났을 것이다.

 

참고로 리플렉션 예외 각각을 잡는 대신 모든 리플렉션 예외의 상위 클래스인 ReflectiveOperationException 을 잡도록 하여 코드 길이를 줄일 수도 있다.

 

객체가 일단 만들어지면 그 후의 코드는 기본 Set 인스턴스를 사용하는 코드와 동일하다. 이 프로그램을 컴파일하면 비검사 형변환 경고가 뜬다. 하지만 Class<? extends Set<String>>으로의 형변환은 심지어 명시한 클래스가 Set을 구현하지 않았더라도 성공할 것이라, 실제 문제로 이어지지는 않는다. 이 경고는 숨길 수 있다.

 

(경고 숨기는 방법)

https://devfunny.tistory.com/573?category=895441 

 

[교재 EffectiveJava] 아이템 27. 비검사 경고를 제거하라

비검사 경고 제네릭을 사용하기 시작하면 수많은 컴파일러 경고를 보게 될 것이다. 비검사 형변환 경고 비검사 메서드 호출 경고 비검사 매개변수화 가변인수 타입 경고 비검사 변환 경고 오류

devfunny.tistory.com

 

 

리플렉션 사용 상황

드물긴 하지만, 런타임에 존재하지 않을 수도 있는 다른 클래스, 메서드, 필드와의 의존성을 관리할때 적합하다. 가동할 수 있는 최소한의 환경, 즉 주로 가장 오래된 버전만을 지원하도록 컴파일한 후, 이후 버전의 클래스와 메서드 등은 리플렉션으로 접근하는 방식을 사용할 수 있다. 이렇게 하려면 접근하려는 새로운 클래스나 메서드가 런타임에 존재하지 않을 수 있다는 사실을 반드시 감안해야한다. 

 

 

반응형

Designed by JB FACTORY