65. 리플렉션보다는 인터페이스를 사용하라
주제: 리플렉션보다는 인터페이스를 사용하라
(핵심)
java.lang.reflect
패키지를 통해 런타임에 클래스 정보를 얻고 조작할 수 있지만, 단점이 많아 매우 제한적으로 사용해야 하며, 가능하다면 인터페이스 기반 접근을 우선해야 한다.
리플렉션이란?
프로그램 실행 중(런타임)에 임의의 클래스에 접근 가능.
Class
,Constructor
,Method
,Field
인스턴스.가능한 작업
클래스/멤버 정보 조회
실제 멤버 조작:
인스턴스 생성 (
Constructor.newInstance()
).메서드 호출 (
Method.invoke()
).필드 값 접근/수정 (
Field.get()
,Field.set()
).
컴파일 시점에는 존재하지 않거나 알 수 없었던 클래스도 런타임에 로드하여 사용 가능.
리플렉션의 단점
컴파일타임 검사 이점 상실 (Loss of Compile-time Checking Benefits)
타입 안전성 X
잘못된 타입의 객체나 메서드 사용 시 런타임 오류 발생.
예외 검사 X
리플렉션 API는 확인된 예외(Checked Exception)를 제대로 검사하지 못함.
존재하지 않거나 접근 불가한 멤버 호출 시
RuntimeException
또는 관련 리플렉션 예외 발생.
지저분하고 장황한 코드
단순한 생성/호출에도 많은 리플렉션 API 호출 및 광범위한
try-catch
블록 필요. 가독성 저하.
성능 저하
일반적인 메서드 호출이나 필드 접근보다 '훨씬' 느림. 분석 및 해석 오버헤드 발생.
(참고) 실제 테스트 시 메서드 호출 속도 11배 차이.
권장 사용 패턴
graph TD
A[애플리케이션 코드] -- "컴파일 타임 의존" --> B(사용할 인터페이스);
subgraph "런타임 처리 (Runtime Processing)"
C(리플렉션 API) -- "1.클래스 로드, 생성" --> D(알 수 없는 구현 클래스 인스턴스);
D -- "2.인터페이스로 형변환/참조" --> B;
end
A -- "3.인터페이스 통해 사용" --> B;
style C fill:#egg,stroke:#333,stroke-width:2px
style D fill:#eff,stroke:#333,stroke-width:1px
아주 제한적 사용
핵심
생성은 리플렉션, 사용은 인터페이스/상위 클래스
컴파일 시점에는 구체 클래스를 알 수 없지만, 사용할 인터페이스나 상위 클래스는 아는 경우.
과정
리플렉션 API를 사용해 인스턴스를 생성.
Class.forName()
,getDeclaredConstructor()
,newInstance()
생성된 인스턴스를 컴파일 시점에 아는 인터페이스나 상위 클래스 타입으로 형변환하여 변수에 할당.
이후 코드에서는 해당 인터페이스/상위 클래스에 정의된 메서드만 사용하여 객체 처리. (리플렉션 코드와 분리)
예시 분석 (Code 65-1: 명령줄 인수로 Set 구현체 동적 생성)
//code 65-1 리플렉션으로 생성하고 인터페이스로 참조해 활용한다.
public static void main(String[] args) {
Class<? extends Set<String>> cl = null;
Constructor<? extends Set<String>> cons = null;
Set<String> s = null;
try {
// 1. 클래스 로드 (문자열 -> Class 객체)
cl = (Class<? extends Set<String>>) Class.forName(args[0]); // 비검사 형변환 주의
// 2. 생성자 얻기 (매개변수 없는 기본 생성자)
cons = cl.getDeclaredConstructor();
// 3. 인스턴스 생성 (실제 객체 만들기)
s = cons.newInstance();
} catch (ClassNotFoundException e) {
fatalError("클래스 찾기 실패: " + args[0]);
} catch (NoSuchMethodException e) {
fatalError("매개변수 없는 생성자 없음");
} catch (IllegalAccessException e) { // private 생성자 등 접근 불가
fatalError("생성자 접근 불가");
} catch (InstantiationException e) { // 추상 클래스, 인터페이스 등 인스턴스화 불가
fatalError("클래스 인스턴스화 불가");
} catch (InvocationTargetException e) { // 생성자 내부에서 예외 발생 시
fatalError("생성자 내부 예외 발생: " + e.getCause());
} catch (ClassCastException e) { // 로드된 클래스가 Set 구현체가 아닐 경우 등
fatalError("Set 인터페이스를 구현하지 않음");
} catch (ExceptionInInitializerError e) { // 정적 초기화 블록 실패
fatalError("클래스 초기화 실패: " + e);
}
// ... (더 많은 잠재적 예외 처리 가능) ...
if (s != null) {
s.addAll(Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
}
목표 : 프로그램 실행 시 주어진 클래스 이름으로
Set<String>
인스턴스를 동적으로 생성하고 사용하는 것.예:
java.util.HashSet
,java.util.LinkedHashSet
리플렉션 과정
Class.forName(args[0])
: 문자열 이름으로Class
객체 로드.cl.getDeclaredConstructor()
: 기본 생성자(Constructor
) 객체 얻기.cons.newInstance()
: 생성자를 호출하여 실제Set
인스턴스 생성.이 과정에서
ClassNotFoundException
,NoSuchMethodException
,IllegalAccessException
,InstantiationException
,InvocationTargetException
,ClassCastException
등 다양한 예외 처리 필요.
인터페이스 사용
Set<String> s = ...
: 생성된 객체를Set
인터페이스 타입 변수s
로 받음.s.addAll(...)
,System.out.println(s)
: 이후s
변수를 통해Set
인터페이스의 표준 메서드만 사용.이 부분은 리플렉션과 무관하게 타입 안전하고 명확함
예시가 보여주는 단점
6가지 잠재적 런타임 예외 발생 가능성 (컴파일 시점 확인 불가).
단 한 줄(
new HashSet<>()
)이면 될 인스턴스 생성을 위해 25줄의 복잡한 코드 필요.
고급/특수 사용 사례 (Advanced/Special Use Case)
다중 버전 외부 라이브러리 지원 (Supporting Multi-version External Libraries)
호환성을 위해 가장 오래된 버전(최소 요구사항)의 라이브러리로 컴파일.
런타임에 더 최신 버전의 라이브러리가 존재할 경우, 해당 버전에만 있는 추가 클래스나 메서드를 리플렉션으로 감지하고 접근 시도.
(필수 조건) 해당 최신 기능이 런타임에 없을 경우를 반드시 대비해야 함
예: 대체 로직 실행, 기능 비활성화
최종 요약 (Key Takeaway)
"리플렉션은 특정 문제(컴파일 시점에 알 수 없는 클래스 사용 등) 해결에 강력하지만, 코드 가독성/타입 안전성/성능 저하라는 명확한 단점이 있다.
필요하다면 사용하되, 가급적 인스턴스 생성에만 국한하고, 생성된 객체는 반드시 인터페이스나 상위 클래스로 형변환하여 참조하고 사용함으로써 리플렉션의 단점을 코드 전체로 확산시키지 않도록 격리하라."
Last updated