도당탕탕
Item65 : 리플렉션보다는 인터페이스를 사용하라 본문
리플렉션 기능을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있다. Class객체가 주어지면 그 클래스의 생성자, 메서드, 필드에 해당하는 인스턴스를 가져올 수 있다.
또한 이 인스턴스들로는 그 클래스의 멤버 이름, 필드 타입, 메소드 시그니처 등을 가져올 수 있고 조작도 가능하다. 예를 들어 Method.invoke
는 어떤 클래스의 어떤 객체가 가진 어떤 메소드라도 호출할 수 있게 해 준다.
이렇게 리플렉션을 이용하면 컴파일 당시에 존재하지 않던 클래스도 이용할 수 있는데, 다음과 같이 단점이 있다.
- 컴파일타임 타입 검사가 주는 이점을 하나도 누릴 수 없다.
예외 검사도 마찬가지로, 프로그램이 리플렉션 기능을 써서 존재하지 않는 혹은 접근할 수 없는 메서드를 호출하려 시도하면 런타임 오류가 발생한다.
- 리플렉션을 이용하면 코드가 지저분하고 장황해진다.
지루한 일이고, 읽기도 어렵다.
- 성능이 떨어진다.
리플렉션을 통한 메소드 호출은 일반 메소드 호출보다 훨씬 느리다. ( 이펙티브 자바에서 입력 매개변수가 없고 int를 반환하는 메서드로 실험해 보니 11배가 느렸다. )
이렇게 코드 분석 도구나 의존관계 주입 프레임워크처럼 리플렉션을 써야 하는 복잡한 애플리케이션이 있지만 다들 리플렉션 사용을 줄이고 있는 추세다.
리플렉션 사용 문제
리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피하고 이점만 취할 수 있다.
예를 들어 컴파일타임에 이용할 수 없는 클래스를 사용해야만 하는 프로그램은 비록 컴파일타임이라도 적절한 인터페이스나 상위 클래스를 이용할 수는 없을 것이다. 다행히 이런 경우라면 리플렉션은 인스턴스 생성에만 쏘고, 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자.
다음 예제는 Set 인터페이스의 인스턴스를 생성하는데, 정확한 클래스는 명령줄의 첫 번째 인수로 확정하는 프로그램이다.
- 리플렉션으로 생성하고 인터페이스로 참조해 활용한 예제
public static void main(String[] args) { // args : HashSet(0) a(1) b(2) b(3) c(4) ....
//클래스 이름을 Class 객체로 변환
Class<? extends Set<String>> cl = null;
try {
// args[0]을 통해 HashSet클래스를 가져온다.
cl = (Class<? extends Set<String>>) Class.forName(args[0]); // 비검사 형변환 -> 형변환 안해도 오류는 안나지만
} catch (ClassNotFoundException e) { // 인스턴스를 생성할때 ClassCastException이 발생
fatalError("클래스를 찾을 수 없습니다.");
}
Constructor<? extends Set<String>> cons = null;
try {
cons = cl.getDeclaredConstructor(); // Set 클래스를 통해 매개변수 없는 생성자를 찾아 얻는다.
} catch (NoSuchMethodException e) {
fatalError("매개변수 없는 생성자를 찾을 수 없습니다.");
}
// 집합의 인스턴스를 만든다.
Set<String> s = null;
try {
s = cons.newInstance(); // 생성자를 생성한다. (HashSet의 생성자)
} 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);
}
private static void fatalError(String msg) {
System.err.println(msg);
System.exit(1);
}
위 예제는 리플렉션의 단점을 두 가지를 보여준다.
- 런타임에 총 여섯 가지나 되는 예외를 던질 수 있다.
- 이 모두가 인스턴스를 리플렉션 없이 생성했다면 컴파일타임에 잡아낼 수 있었을 예외들이다.
- 클래스 이름만으로 인스턴스를 생성해 내기 위해 무려 25줄이나 되는 코드를 작성했다.
- 리플렉션이 아니라면 생성자 호출한 줄로 끝났을 것이다.
두 단점 모두 객체를 생성하는 부분에만 국한된다. 객체가 일단 만들어지면 그 후의 코드는 여타의 Set 인스턴스를 사용할 때와 똑같다. 그래서 실제 프로그램에서는 이런 제약에 영향받는 코드는 일부에 지나지 않는다.
리플렉션 사용
드물긴 하지만, 리플렉션은 런타임에 존재하지 않을 수도 있는 다른 클래스, 메서드, 필드와의 의존성을 관리할 때 적합하다. 이 기법은 버전이 여러 개 존재하는 외부 패키지를 다룰 때 유용하다. 가동할수 있는 최소한의 환경, 즉 주로 가장 오래된 버전만을 지원하도록 컴파일한 후, 이후 버전의 클래스와 메소드 등은 리플렉션으로 접근하는 방식이다.
이렇게 하려면 접근하려는 새로운 클래스나 메서드가 런타임에 존재하지 않을 수 있다는 사실을 반드시 감안해야 한다. 즉 같은 목적을 이룰 수 있는 대체 수단을 이용하거나 기능을 줄여 동작하는 등의 적절한 조치를 취해야 한다.
'JAVA' 카테고리의 다른 글
Item67 : 최적화는 신중히 하라 (0) | 2023.01.19 |
---|---|
Item66 : Native method를 현명하게 사용하라. (0) | 2023.01.18 |
Item64 : 인터페이스로 객체들을 참조하라. (0) | 2023.01.17 |
Item63 : 문자열 연결은 느리니 주의하라 (0) | 2023.01.17 |
Item62 : 다른 타입이 적절한곳에 String을 피하라. (0) | 2023.01.16 |