리플렉션이란
•
리플렉션(Reflection)은 구체적인 클래스 타입을 알지 못하더라도 해당 클래스와 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API이다.
•
이런 리플렉션은 컴파일타임이 아닌 런타임에 동적으로 클래스의 정보를 추출할 수 있는 프로그래밍 기법이다.
•
어플리케이션 개발보다는 주로 프레임워크나 라이브러리 작성 시 많이 사용되는데, 작성 시점에는 사용자가 어떤 이름으로 클래스를 만들고 어떤 메서드와 필드들이 있는지 모르지만 클래스 파일의 위치나 이름으로 해당 클래스를 다룰 수 있게 한다.
•
이러한 방식으로 동적으로 클래스 파일을 사용하여, 클래스의 정보를 얻어내고 객체를 생성하는 것을 가능하게 해 유연한 프로그래밍을 가능케 해준다.
•
Intellij의 자동 완성 기능과 Spring 프레임워크의 DI, Proxy, ModelMapper 등이 있고, Spring의 애노테이션 중 Lombok, Hibernate 등 많은 프레임워크도 리플렉션 기능을 사용하여 구현하고 있다.
•
리플렉션으로는 Class, Constructor, Method, Field 정보를 가져올 수 있다.
Java의 Class 클래스
•
자바로 프로그래밍 할 때 보통 클래스나 변수를 직접 선언하고 만들어 사용한다. 하지만 코드를 실행하기 전에 개발자가 직접 클래스를 찾아 클래스 정보를 얻는게 아니라, 코드 상에서 클래스 정보를 얻어와 런타임 시에 동적으로 클래스를 변경하고 다루는 일이 필요할 수 있다. 이 때 사용되는 것이 Class 클래스 객체이다.
•
Class 클래스는 java.lang.Class 패키지에 존재하는 독립형 클래스로서, 자신이 속한 클래스의 모든 멤버 정보를 담고 있어 런타임 환경에서 동적으로 저장된 클래스나 인터페이스 정보를 가져오는데 사용된다.
•
자바의 모든 클래스와 인터페이스는 컴파일을 통해 .java 파일에서 .class 파일로 변환되고, class 파일에는 멤버 변수, 메서드, 생성자 등의 객체 정보들이 들어있다.
•
JVM의 클래스 로더(ClassLoader)에 의해 클래스 파일에서 클래스 정보들을 가져와 메모리의 힙 영역에 Class 객체화하여 저장한다.
리플렉션으로 클래스 가져오기
•
Class 객체를 사용하기 위해 가져오는 방법은 3가지가 있다.
◦
Object.getClass()
▪
최상위 클래스인 Object 클래스에서 제공하는 getClass() 메서드를 통해 가져오는 방법으로, 해당 클래스가 인스턴스화 되어 있을 때만 가능하다는 제약이 있다.
public static void main(String[] args) {
// 스트링 클래스 인스턴스화
String str = new String("Class클래스 테스트");
// getClass() 메서드로 얻기
Class<? extends String> cls = str.getClass();
System.out.println(cls); // class java.lang.String
}
JavaScript
복사
◦
.class 리터럴
▪
가장 심플하게 Class 객체를 가져오는 방법으로, 인스턴스가 생성되지 않고 컴파일된 클래스 파일만 있다면 리터럴로 Class 객체를 가져올 수 있다.
public static void main(String[] args) {
// 클래스 리터럴(*.class)로 얻기
Class<? extends String> cls2 = String.class;
System.out.println(cls2); // class java.lang.String
}
JavaScript
복사
◦
Class.forName()
▪
클래스 이름만으로 Class 객체를 가져오는 방법으로, 클래스의 도메인을 상세히 적어야하고 클래스 파일 경로에 오타가 있으면 Class 객체를 찾지 못해 ClassNotFoundException 에러가 발생할 수 있다.
▪
다른 클래스 파일을 불러올 때는 JVM의 Method Area에 클래스 파일이 바인딩(binding) 되지만, 이 방법은 바인딩이 되지 않고 런타임 때 불러오기 때문에 Class 객체를 forName() 메서드로 가져오는 방법을 동적 로딩이라 부른다.
▪
위의 두 방법보다 메모리를 절약하며 동적으로 로딩 할 수 있기 때문에 성능이 좋다.
public static void main(String[] args) {
try {
// 도메인.클래스명으로 얻기
Class<?> cls3 = Class.forName("java.lang.String");
System.out.println(cls3); // class java.lang.String
} catch (ClassNotFoundException e) {}
}
JavaScript
복사
리플렉션으로 생성자 가져오기
class Person {
public String name; // public 필드
private int age; // private 필드
public static int height = 180; // static 필드
// 이름, 나이를 입력받는 생성자
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 기본 생성자
public Person() {
}
public void getField() {
System.out.printf("이름 : %s, 나이 : %d\n", name, age);
}
// public 메소드
public int sum(int left, int right) {
return left + right;
}
// static 메소드
public static int staticSum(int left, int right) {
return left + right;
}
// private 메소드
private int privateSum(int left, int right) {
return left + right;
}
}
Java
복사
•
이러한 클래스가 있다고 하면, 리플렉션을 통해 동적으로 생성자를 가져와서 초기화 하는 과정은 다음과 같다.
public static void main(String[] args) throws Exception {
// 클래스 객체 가져오기 (forName 메소드 방식)
Class<Person> personClass = (Class<Person>) Class.forName("Person");
// 생성자 가져오기 - Person(String name, int age)
Constructor<Person> constructor = personClass.getConstructor(String.class, int.class); // getConstructor 인자로 생성자의 매개변수 타입을 바인딩 해주어야 한다.
// 가져온 생성자로 인스턴스 만들기
Person person1 = constructor.newInstance("홍길동", 55);
person1.getField(); // 이름 : 홍길동, 나이 : 55
}
Java
복사
•
getConstructor()를 호출할때 인자로 생성자의 매개변수 타입을 바인딩 해주어야 하고, 매개변수 타입을 지정해주지 않으면 기본 생성자가 호출된다.
•
해당하는 생성자를 찾지 못하면 NoSuchMethodException이 발생한다.
리플렉션으로 메서드 가져오기
•
리플렉션을 통해 동적으로 메서드를 가져와서 실행 하는 과정은 다음과 같다.
public static void main(String[] args) throws Exception {
Class<Person> personClass = (Class<Person>) Class.forName("Person");
// 특정 public 메서드 가져와 실행
// getMethod("메서드명", 매개변수타입들)
Method sum = personClass.getMethod("sum", int.class, int.class);
int result = (int) sum.invoke(new Person(), 10, 20);
System.out.println("result = " + result); // 30
// 특정 static 메서드 가져와 실행
Method staticSum = personClass.getMethod("staticSum", int.class, int.class);
int staticResult = (int) staticSum.invoke(null, 100, 200);
System.out.println("staticResult = " + staticResult); // 300
// 특정 private 메서드 가져와 실행
Method privateSum = personClass.getDeclaredMethod("privateSum", int.class, int.class);
privateSum.setAccessible(true); // private 이기 때문에 외부에서 access 할 수 있도록 설정
int privateResult = (int) privateSum.invoke(new Person(), 1000, 2000);
System.out.println("privateResult = " + privateResult); // 3000
}
Java
복사
•
getMethod() 를 호출할때 인자로 생성자의 매개변수 타입을 바인딩 해주어야 한다.
•
Method 타입에서 제공하는 invoke()를 호출하여 해당 메서드를 실행할 수 있다.
◦
nstance 메소드 - 매개변수로 인스턴스 필요
◦
static 메소드 - 매개변수 필요 없음
◦
private 메소드 - invoke 하기전에 공개화 할 필요있음
리플렉션으로 필드 변경하기
•
리플렉션을 통해 동적으로 필드를 가져와서 조작 하는 과정은 다음과 같다.
public static void main(String[] args) throws Exception {
Class<Person> personClass = (Class<Person>) Class.forName("Person");
// static 필드를 가져와 조작하고 출력하기
Field height_field = personClass.getField("height");
height_field.set(null, 200);
System.out.println(height_field.get(null)); // 200
}
Java
복사
public static void main(String[] args) throws Exception {
Person person = new Person("홍길동", 55);
// 클래스 객체 가져오기
Class<Person> personClass = (Class<Person>) Class.forName("Person");
// public 필드를 가져온다.
Field name_field = personClass.getField("name");
// private 필드를 가져온다.
Field age_field = personClass.getDeclaredField("age");
age_field.setAccessible(true); // private 이기 때문에 외부에서 access 할 수 있도록 설정
// 필드 조작하기
name_field.set(person, "임꺽정");
age_field.set(person, 88);
System.out.println(name_field.get(person)); // 임꺽정
System.out.println(age_field.get(person)); // 200
}
Java
복사
•
getField() 를 통해 클래스의 필드를 얻을 수 있고, set() 메서드를 통해 필드 값을 변경할 수 있다.
•
필드는 클래스가 인스턴스가 되어야 Heap 메모리에 적재됨으로 인스턴스가 필요하다.
•
다만, static 필드라면 Method Area에 이미 적재되어 있으므로 인스턴스가 필요없다.
리플렉션의 단점
•
일반적인 클래스는 컴파일 타임에 분석되고 저장되지만, 리플렉션은 런타임에 클래스를 분석하기 때문에 속도가 느리고, 컴파일 타임에 타입 체크가 불가능하다. 그리고 오류도 컴파일 오류가 아닌 런타임 오류가 발생한다.
•
또한 객체의 추상화가 깨진다는 단점도 존재한다.
•
이런 단점들 때문에 리플렉션은 정말 필요한 곳에만 한정적으로 사용해야 한다.