핵심 정리
리플렉션은 동적인 프로그래밍을 지원하는 강력한 기능이지만, 단점이 많다. 따라서 객체 생성에만 사용하고, 생성한 객체를 이용할 때는 적절한 인터페이스나 컴파일타임에 알 수 있는 상위 클래스로 형변환해 사용해야 한다.
리플렉션(java.lang.reflect) 개념
- 자바에서 동적인 프로그래밍을 가능하게 해주는 강력한 기능이다.
- 프로그램이 실행 중에 자신의 구조를 동적으로 탐색하거나 조작하는 능력이다.
- 프로그램이 자신을 조사하고 수정할 수 있게 하는 기능이며, 이는 컴파일 시점에는 알 수 없는 클래스나 메소드에 대해 런타임에 동적으로 접근하는 것을 가능하게 한다.
- 클래스의 정보를 가져올 수 있다.
- 자바의 Class 객체를 통해 임의의 클래스에 접근할 수 있다.
- Class 객체는 해당 클래스의 메타데이터를 나타내며, 이를 통해 클래스의 이름, 슈퍼 클래스, 인터페이스 등의 정보를 얻을 수 있다.
- Class 객체는 Class.forName() 메소드나 .class 리터럴 등을 이용해 얻을 수 있다.
- 프로그램 실행 중인 동안 어떤 클래스의 정보를 가져올 수 있다.
- Class 객체에서 getConstructors(), getMethods(), getFields() 등의 메소드를 호출하면, 해당 클래스의 생성자, 메소드, 필드에 대한 정보를 Constructor[], Method[], Field[] 등의 형태로 가져올 수 있다.
- 이 정보는 생성자, 메소드 필드 등에 대한 정보를 포함하며, 이들의 이름, 필드의 타입, 메소드의 시그니처(파라미터 타입과 반환 타입) 등을 포함한다.
- 각각에 연결된 실제 생성자, 필드, 메소드에 접근해 조작할 수 있다.
- 예
- Constructor.newInstance() 메소드를 호출하면 해당 생성자를 통해 새 인스턴스를 생성할 수 있다.
- Method 객체의 invoke() 메소드를 사용하면, 해당 Method 객체가 나타나는 메소드를 실제로 호출할 수 있다.
- Field.set 또는 Field.get()을 통해 필드 값을 설정하거나 가져올 수도 있다.
- 예
컴파일 당시에는 존재하지 않는, 즉 런타임에 동적으로 로드되는 클래스를 이용할 수 있다.
리플렉션 단점
- 컴파일타임 검사가 주는 이점을 사라진다.
- 일반적인 자바 코드는 컴파일러에 의해 타입 검사가 이루어진다.
- 이는 프로그래머가 잘못된 타입의 객체를 메소드에 전달하거나, 존재하지 않는 메소드를 호출하려고 시도하는 경우 같은 실수를 사전에 잡아준다.
- 그러나 리플렉션을 사용하면 이러한 타입 검사는 런타임에 이루어진다.
- 따라서 리플렉션을 사용한 코드는 존재하지 않는 메소드를 호출하거나, 잘못된 타입의 인수를 전달하는 등의 오류를 컴파일 시점에는 감지할 수 없으며, 이는 런타임 오류를 초래할 수 있다.
- 리플렉션을 이용하면 코드가 지저분하고 장황해진다.
- 일반적인 메소드 호출은 간결하지만, 리플렉션을 통한 메소드 호출은 해당 메소드의 이름, 파라미터 타입, 실제 인수 등을 모두 처리해야 하므로 코드가 복잡해진다.
- 성능이 떨어진다.
- 일반적인 메소드 호출에 비해 상당히 느리다.
- 이는 메소드를 찾고, 접근 제한자를 확인하고, 인수를 언박싱하고 다시 박싱하는 등의 작업 때문이다.
리플렉션 주의해야 할 점
리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피하고 이점만 취할 수 있다.
리플렉션은 인스턴스 생성에만 사용해야 한다.
- 컴파일 타임에 사용할 수 없는 클래스를 사용해야만 하는 경우라도, 일반적으로 해당 클래스는 어떤 인터페이스나 상위 클래스를 구현하거나 상속받은 경우가 많다.
- 이런 경우, 리플렉션은 해당 클래스의 인스턴스를 생성하는 데에만 사용하고, 그 생성된 인스턴스는 그 클래스가 구현하거나 상속받은 인터페이스나 상위 클래스를 통해 참조하고 사용하는 것이 좋다.(item 64)
예시 1 - 리플렉션을 사용해 명령줄 인자로 받은 클래스의 이름에 해당하는 Set<String> 인스턴스를 동적으로 생성하고 이를 사용
// 리플렉션으로 생성하고 인터페이스로 참조해 활용한다.
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) {
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(args).subList(1, args.length));
System.out.println(s);
}
private static void fatalError(String msg) {
System.err.println(msg);
System.exit(1);
}
위 코드는 주어진 클래스 이름에 해당하는 Set<String>의 인스턴스를 동적으로 생성하는 작업을 수행한다. 이는 런타임에 클래스를 결정하고 인스턴스화하는 능력이 필요할 때 매우 유용하다.
◾ 코드 설명
더보기
- Class.forName(args[0])을 통해 해당 클래스의 Class 객체를 가져온다. 이 클래스는 Set<String> 인터페이스를 구현해야 한다.
- cl.getDeclaredConstructor()를 통해 해당 클래스의 기본 생성자에 해당하는 Constructor 객체를 가져온다.
- cons.newInstance()를 호출하여 생성자를 통해 해당 클래스의 새 인스턴스를 생성한다. 이 인스턴스는 Set<String> 인터페이스를 구현하므로, Set<String> 타입의 참조변수 s에 할당될 수 있다.
- s.addAll(Arrays.asList(args).subList(1, args.length))를 통해 명령줄 인자의 두 번째 인자부터 마지막 인자까지리스트로 변환하고, 이 리스트를 s에 추가한다.
◾ 활용
더보기
- 제네릭 집합 테스터로의 활용
예를 들어, HashSet과 TreeSet이 Set 인터페이스의 규약을 잘 따르는지 확인하고 싶다고 가정하자. 명령줄 인자로 java.util.HashSet과 java.util.TreeSet을 각각 제공하고, 프로그램이 생성한 Set 인스턴스에 대해 다양한 연산을 수행하면서 이들이 Set 인터페이스의 규약을 따르는지 확인할 수 있다.
- 제네릭 집합 성능 분석 도구로의 활용
예를 들어, HashSet과 TreeSet 중 어느 것이 더 빠른지 비교하고 싶다고 가정하자. 명령줄 인자로 java.util.HashSet과 java.util.TreeSet을 각각 제공하고, 프로그램이 생성한 Set 인스턴스에 대해 다양한 연산을 수행하면서 이들의 실행 시간을 측정하고 비교할 수 있다.
- 서비스 제공자 프레임워크(item 1)로의 활용
서비스 제공자 프레임워크는 서비스 인터페이스를 정의하고, 이를 구현한 제공자 클래스를 등록하고, 사용자가 이를 사용할 수 있도록 하는 방식으로 동작하는데, 리플렉션을 이용하면 이를 효과적으로 구현할 수 있다.
예를 들어, 데이터베이스 접속을 관리하는 서비스를 제공하는 프레임워크를 구축하고 있다고 가정하자. DatabaseService라는 인터페이스를 정의하고, 이를 구현한 OracleDatabaseService, MySqlDatabaseService와 같은 클래스를 제공할 수 있다. 이때, 사용자는 DatabaseService 인터페이스를 통해 데이터베이스 작업을 수행할 수 있으며, 실제로 어떤 데이터베이스가 사용되는지는 프로그램 실행 시에 결정하고, 이를 위해 리플렉션을 사용할 수 있따.
◾ 예시 1에서 리플렉션의 단점
더보기
1. 런타임에 여섯 가지나 되는 예외를 던질 수 있다.
: 리플렉션을 사용하지 않았다면 컴파일타임에 잡을 수 있다. 이는 ReflectiveOperationException을 이용하여 예외들을 일괄적으로 처리할 수 있으므로 코드를 단순화하고 줄일 수 있다. ReflectiveOperationException은 Java 7부터 도입된 예외 클래스로, 리플렉션 연산 중 발생할 수 있는 예외 상황들을 포괄하는 최상위 예외 클래스이다.
2. 클래스 이름만으로 인스턴스를 생성하기 위해 긴 코드를 작성해야 한다.
: 리플렉션을 사용하지 않았다면 생성자 호출 한 줄로 구현할 수 있다.
위 단점은 모두 인스턴스를 생성하는 부분에서만 발생하므로, 이런 제약에 영향받는 코드는 일부에 지나지 않는다.
또한, 위 코드는 컴파일러가 비검사 형변환 경고가 발생시킨다. (Class<? extends Set<String>>) Class.forName(args[0])라는 비검사 형변환은 런타임에 args[0]에 해당하는 클래스가 Set<String> 인터페이스를 구현하고 있는지 검증하지 않는다. 즉, Set<String>을 구현하지 않은 클래스에 대해서도 형변환이 성공하게 된다. Set<String> 인터페이스를 구현하지 않는 클래스의 인스턴스를 생성하려고 시도하면, 그때 ClassCastException이 발생한다.
위 문제 또한 인스턴스를 생성하는 부분에서만 문제가 발생한다.
리플렉션과 의존성 관리
- 리플렉션은 런타임에 다른 클래스, 메소드, 필드에 접근하게 해준다.
- 이 기능은 특히 런타임에 존재하지 않을 수 있는 코드와의 의존성을 관리해야 할 때 유용하다.
- 예를 들어, 프로그램이 특정 라이브러리의 최신 기능을 이용하려 할 때, 이 최신 기능이 오래된 버전에서는 존재하지 않을 수 있다. 이런 경우 리플렉션을 이용하여 최신 기능이 존재하는지 런타임에 확인하고, 존재한다면 이를 이용하게 된다.
- 주의해야 할 점은 런타임에 존재하지 않는 코드에 접근하는 것이 실패할 때 대체 방법을 준비해야 한다.
- 예를 들어, 최신 기능을 이용할 수 없을 때 수행할 기능을 작성해야 한다.
'Language > Java' 카테고리의 다른 글
[effective java] 아이템 67. 최적화는 신중히 하라. (0) | 2023.07.31 |
---|---|
[effective java] 아이템 66. 네이티브 메서드는 신중히 사용하라. (0) | 2023.07.27 |
[effective java] 아이템 64. 객체는 인터페이스를 사용해 참조하라. (0) | 2023.07.27 |
[effective java] 아이템 63. 문자열 연결은 느리니 주의하라. (0) | 2023.07.27 |
[effective java] 아이템 62. 다른 타입이 적절하다면 문자열 사용을 피하라. (0) | 2023.07.27 |