- 개념
- 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API
- 컴파일 시간이 아닌 실행 시간에 동적으로 특정 클래스의 정보를 추출할 수 있는 프로그래밍 기법
- JVM과 리플렉션
- JVM은 클래스 정보를 클래스 로더를 통해 읽어와서 해당 정보를 JVM 메모리에 저장함
- 리플렉션은 이 JVM의 메모리 영역에 저장된 클래스의 정보(멤버 변수, 메서드, 생성자)를 런타임 시에 꺼내와서 사용하는 기술로, 클래스의 구조와 동작을 동적으로 탐색하고 제어할 수 있게함
- 결국 클래스의 정보를 리플렉션이라는 기능을 통해 다시 가져올 수 있음
- 하지만 성능상의 오버헤드와 보안상의 이슈가 있으므로 신중하게 사용해야 함
- 런타임에 동적으로 클래스 정보에 접근하여 클래스를 사용할 때 필요함
- 작성 시점에는 어떠한 클래스를 사용해야 할 지 모르지만, 런타임 시점에서 클래스를 가져와서 실행해야 하는 경우
- private 접근 제어자로 선언한 필드나 메서드까지 조작이 가능함
- 하지만 이 때문에 캡슐화가 깨진다는 문제점 존재
- 프레임워크나 IDE에서 이러한 동적 바인딩을 이용한 기능을 제공함
- IDE의 자동완성 기능
- 인텔리제이와 같은 IDE에서 etter, Setter를 자동으로 생성해주는 기능도 리플렉션을 사용하여 필드 정보를 가져와 구현함
- 보통 프레임워크나 라이브러리에서 많이 사용함
- 이 둘은 컴파일 시점까지 개발자가 구현한 객체들의 타입을 모르기 때문에 이러한 문제를 동적으로 해결하기 위해 사용함
- 대표적으로 Spring 프레임워크의 애노테이션 같은 기능들이 리플렉션을 이용하여 프로그램 실행 도중에 동적으로 클래스의 정보를 가져와서 사용함
- IDE의 자동완성 기능
- 장점
- 유연성
- 구체적은 클래스를 알지 못해도 런타임 시점에 클래스를 만들어서 의존관계를 맺어줄 수 있음
- 개발 규모가 큰 스프링의 경우, 리플렉션을 이용한 Dynamic proxy를 통해서 DI 어노테이션(
@AutoWired
,@Service
,@Controller
등)을 활용함
- 접근 제한 상관없이 테스트 가능
- 접근 제어자와 관계 없이 필드와 메소드에 접근하여 필요한 작업을 수행할 수 있기 때문에 private 메서드도 테스트가 가능함
- 유연성
- 단점
- 캡슐화 저해
- private이나 protected로 선언된 멤버 변수, 메서드에도 접근할 수 있음
- 따라서 일반적인 접근 제어 규칙을 우회할 수 있는 가능성이 있으며, 클래스의 내부 구현 및 민감한 정보가 노출될 수 있음
- 디버깅의 어려움
- 런타임 시점에서 인스턴스를 생성하므로 컴파일 시점에서 해당 타입을 체크할 수 없고, 구체적인 동작 흐름을 파악하기 어려움
- 성능 이슈
- 단순히 필드 및 메소드를 접근할 때는 컴파일 시점에 분석된 클래스를 사용하지만, 리플렉션은 런타임에 클래스를 분석하기 때문에 성능저하가 발생함
- 캡슐화 저해
보통 일반적인 웹 애플리케이션 개발자는 리플렉션을 사용할 일이 거의 없다. 보통 라이브러리나 프레임워크를 개발할 때 사용된다. 따라서 리플렉션은 꼭 필요한 경우에만 한정적으로 사용해야 한다.
- 리플렉션의 가장 핵심은
Class
클래스로, 리플렉션을 사용하라면Class<T>
타입을 가져와야 함 Class<T>
타입을 통해Constructor
와Method
그리고Field
정보를 가져올 수 있음
Class<Member> aClass = Member.class; // (1) {클래스 타입}.class
Member member1 = new Member();
Class<? extends Member> bClass = member1.getClass(); // (2) {인스턴스}.getClass()
Class<?> cClass = Class.forName("org.example.Member"); // (3) Class.forName("{전체 도메인 네임}")
{클래스 타입}.class
: 클래스의class
프로퍼티를 통해 획득하는 방법{인스턴스}.getClass()
: 인스턴스 변수의 메서드인getClass()
사용Class.forName("{전체 도메인 네임}")
:Class
클래스의forName()
정적 메소드에 FQCN를 전달하여 해당 경로와 대응하는 클래스에 대한Class
클래스의 인스턴스를 얻는 방법- FQCN(Fully Qualified Class Name): 해당 클래스가 속한 패키지명을 모두 포함한 이름
- Class 객체의 메서드 중에서
getFields()
,getMethods()
와 같은 형태와getDeclaredFields()
,getDeclaredMethods()
와 같은 형태로 정의된 메서들이 존쟂함 getXXX()
: 상속받은 클래스와 인터페이스를 포함하여 모든 public인 요소들을 가져옴getDelcaredXXX()
: 상속받은 클래스와 인터페이스를 제외하고 해당 클레스에 직접 정의된 메서드들을 모두 가져옴- 접근 제어자와 상관없이 요소에 접근할 수 있음
Constructor
는java.lang.reflect
패키지에서 제공하는 클래스이며, 클래스 생성자에 대한 정보와 접근을 제공함Constructor<?> constructor = aClass.getDeclaredConstructor(); // 생성자 가져오기 Object object = constructor.newInstance(); // 인스턴스 생성 Member member = (Member) constructor.newInstance(); // 타입 캐스팅 // 파라미터가 존재하는 생성자의 경우 Constructor<?> allArgsConstructor = getDeclaredConstructor(String.class, int.class); noArgsConstructor.setAccessible(true); Member member = (Member) noArgsConstructor.newInstance();
- 리플렉션을 사용하여 Method 타입의 오브젝트를 획득하여 객체 메소드에 직접 접근할 수 있음
Class<Member> aClass = Member.class; Member member = new Member("길동", 25); Method sayMyName = aClass.getDeclaredMethod("sayMyName"); sayMyName.invoke(member); // 내 이름은 길동
Method
타입의invoke()
를 사용하여 메소드를 직접 호출할 수 있음
- 리플렉션을 사용하여 Field 타입의 오브젝트를 획득하여 객체 필드에 직접 접근할 수 있음
Class<Member> aClass = Member.class; Member member = new Member("길동", 25); for (Field field : aClass.getDeclaredFields()) { field.setAccessible(true); String fieldInfo = field.getType() + ", " + field.getName() + " = " + field.get(member); System.out.println(fieldInfo); } /* class java.lang.String, name = 길동 int, age = 25 */
- 모든 클래스에 기본 생성자가 필요한 이유는 Java Reflection API로 가져올 수 없는 정보 중 하나가 바로 생성자의 인자 정보이기 때문이다.
- 따라서 기본 생성자 없이 파라미터가 있는 생성자만 존재한다면 Reflection이 객체를 생성할 수 없게 되는 것이다.
- 하지만 지금은 생성자의 파라미터 정보를 가져올 수 있다. Java7까지는 파라미터 정보를 가져올 수 없었지만, java8에서 리플렉션의 Parameter가 추가되었다.
- 그럼에도 불구하고 기본생성자를 요구하는 이유는 기본 생성자로 객체를 생성하고 필드를 통해 값을 넣어주는 것이 가장 간단한 방법이기 때문이다.
- 다음 글을 참고해보자. 기본 생성자가 필요한 '진짜' 이유 (리플렉션 오해 바로 잡기!!!)