자바 프로그래밍을 할 때 우리는 보통 변수나 클래스를 직접 만들어서 사용해 왔다. 그런데 어떤 경우에는 애플리케이션 실행 중에 클래스를 동적으로 불러와 다루어야 할 때가 생긴다. 즉 코드를 실행하기 전 컴파일 단에서 개발자가 직접 폴더를 뒤져 가며 클래스 정의문을 찾아 클래스 정보를 얻는 것이 아닌, 코드 상에서 호출 로직을 통해 클래스 정보를 얻어와 다루는 방식이다. 이는 런타임 시에 다이나믹하게 동적으로 클래스를 핸들링하는 기법이다. 이때 사용되는 것이 바로 Class 객체라고 말할 수 있다.
Class 클래스는 java.lang,Class 패키지에 별도로 존재하는 독립형 클래스이다. 이는 자신이 속한 클래스의 모든 멤버 정보를 가지고 있다. 따라서 런타임 환경에서 동적으로 저장된 클래스나 인터페이스의 정보를 불러올 수 있다. 여기서 헷갈릴 수 있는 부분은 클래스 자료형을 말하는 것이 아니라, 이름이 "Class"인 클래스를 말하는 것이다.
public final class Class<T> implements java.io.Serializable, java.lang.reflect.GenericDeclaration, java.lang.reflect.Type, java.lang.reflect.AnnotatedElement {
private static final int ANNOTATION= 0x00002000;
private static final int ENUM = 0x00004000;
private static final int SYNTHETIC = 0x00001000;
private static native void registerNatives();
static {
registerNatives();
}
}
자바의 모든 클래스와 인터페이스는 컴파일 후에 .class 파일로 바뀌게 된다. 이 파일에는 멤버 변수, 메서드, 생성자 등등 객체에 대한 메타데이터가 들어 있는데, JVM이 이 파일을 읽어서 Class Loader에 업로드한 뒤에 사용한다. 즉 JVM의 ClassLoader가 이 .class 파일의 클래스 정보를 가져와 Heap 영역에 넣고 클래스에 대한 정보를 가지고 있기 때문에, 우리는 Java에서 객체화된 클래스를 만나 볼 수 있다.
JVM의 ClassLoader는 실행 시에 필요한 클래스를 동적으로 메모리에 올리는 역할을 한다. 먼저 클래스 객체가 이미 만들어져 있지 않은지 확인한다. 이미 만들어진 객체가 있다면 그 객체의 참조를 반환하고, 없다면 classpath에 지정된 경로를 따라서 클래스 파일을 찾아 Class 객체로 변환한 뒤 반환한다. 만약 찾지 못한다면 ClassNotFoundException이 터지게 된다.
자바에서 Class 객체를 가져오는 방법은 총 3가지이다. 객체를 가져오는 것을 똑같지만 각자 다른 방식으로 처리하기 때문에, 이를 모두 잘 알아 둘 필요가 있다.
예시로 String 클래스 객체를 생성하고 세 가지의 방법으로 얻어 보겠다.
public static void main(String[] args) {
// String 클래스 인스턴스화
String str = new String("Class 클래스 테스트");
// getClass()로 메서드 읽기
Class<? extends String> exClass = str.getClass();
// class java.lang.String
System.out.println(exClass);
}
public static void main(String[] args) {
// 클래스 리터럴 (*.class)로 얻기
Class<? extends String> exClass = String.class;
// class java.lang.String
System.out.println(exClass);
}
public static void main(String[] args) {
try {
// 도메인.클래스명으로 얻기
Class<?> exClass = Class.forName("java.lang.String");
// class java.lang.String
System.out.println(exClass);
} catch (ClassNotFoundException e) {
}
}
Class 클래스 객체를 forName() 메서드를 통해 가져오는 방법을 동적 로딩이라고 부른다. 보통 다른 클래스 파일을 불러올 때는 컴파일 시 JVM의 Method Area에 클래스 파일이 같이 바인딩된다. 하지만 forName()으로 .class 파일을 불러올 때는 컴파일에 바인딩되지 않고 런타임 때 불러오기 때문에 동적 로딩이라고 말한다. 그래서 컴파일 타입에 체크할 수 없기 때문에 클래스 유무가 확인되지 않아 예외 처리도 해 줘야 한다.
대부분의 클래스 정보는 프로그램이 로딩될 때 이미 Method Area 메모리에 쌓인다. 그런데 어떤 시스템은 오라클, MySQL, SQL Server 등 여러 종류의 데이터베이스와 연동할 수 있다고도 한다. 그렇다고 이 시스템을 컴파일할 때 모든 데이터베이스의 라이브러리나 드라이버를 같이 컴파일할 필요는 없다. 시스템을 구동할 때 어떤 데이터베이스와 연결할지 결정되면 해당 드라이버만 로딩하면 되기 때문이다. 이럴 때 forName()을 통해 메모리적으로 아끼면서 클래스를 동적으로 로딩하면 좋다.
Class 클래스 객체에는 자신이 속한 클래스의 각종 멤버 정보들이 존재한다. 이렇게 가져온 Class 객체를 이용하여 자신이 속한 클래스가 어떤 클래스인지, 그 정보를 출력해 볼 수 있다.
public static void main(String[] args) {
// String 객체로부터 클래스 정보를 얻는다
Class<? extends String> exClass = String.class;
// 클래스의 이름만 호출
System.out.println(exClass.getSimpleName());
// 패키지의 이름을 호출
System.out.println(exClass.getPackage());
// 패키지와 이름을 호출
System.out.println((exClass.getName());
// 클래스와 패키지 이름을 호출
System.out.println(exClass.toString());
// 제어자부터 패키지 이름 모두 다 호출
System.out.println(exClass.toGenericString());
}
String
package.java.lang. Java Platform API Specification, version1.8
java.lang.String
class java.lang.String
public final class java.lang.String
위에서 본 것과 같이 Class 객체를 이용하면 클래스에 대한 모든 정보를 런타임 환경에서 코드 로직으로 확인해 볼 수 있다.
클래스 정보를 애플리케이션 실행 시점에 알 수 있다는 것을 잘 활용한다면 아주 매력적인 부분이다.
이러한 정보를 이용해 오로지 Class 객체로만 클래스를 인스턴스화할 수 있고, 메서드를 호출하는 등 굉장히 동적인 처리들을 가능하게 해 준다. 이처럼 구체적인 클래스 타입을 알지 못해도 그 클래스의 정보에 접근할 수 있게 해 주는 자바 기법을 Reflection API라고 부른다.
자바 리플렉션(Reflection - 사전적 의미: 거울 등에 비친, 반사)는 객체를 통해 클래스의 정보를 분석하여 런타임에 클래스의 동작을 검사하거나 조작하는 기법이다. 클래스 파일의 위치나 이름만 있다면 해당 클래스의 정보를 얻어내고, 객체를 생성하는 것또한 가능하게 해 줘서 유연한 프로그래밍을 가능하게 해 준다.
리플렉션은 애플리케이션 개발에서보다는 프레임워크나 라이브러리 개발에서 많이 사용된다. 그 이유는 프레임워크나 라이브러리를 사용하는 사람이 어떤 클래스명과 멤버를 구성할지 모르는데, 이러한 사용자 클래스들을 기존의 기능과 동적으로 연결시키기 위해 리플렉션을 사용한다고 생각하면 된다. 대표적인 예로는 스프링의 DI(dpendency injection), Proxy, ModelMapper 등이 있다. 이미 Spring, Hibernate, Lombok 등 많은 프레임워크에서 Reflection 기능을 사용하고 있다.
자바 리플렉션 예제에 쓰일 준비물 클래스를 다음와 같이 작성해 보겠다.
public class Person {
public String name;
private int age;
public static int height = 100;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person() {
}
public void getFeild() {
System.out.printf("이름: %s, 나이: %d\n", name, age);
}
public int sum(int left, int right) {
return (left + right);
}
public static int staticSum(int left, int right) {
return (left + right);
}
private int privateSum(int left, int right) {
return (left + right);
}
}
public static void main(String[] args) {
// 클래스 객체 가져오기
Class<Person> personClass = (Class<Person>) Class.forName("Person");
// 생성자 가져오기
Constructor<Person> constructor = personClass.getConstructor(String.class, int.class);
// 가져온 생성자로 인스턴스 만들기
Person person = constructor.newInstance("홍길동", 25);
}
Class라는 클래스가 따로 있다는 사실은 처음 알았네요!! 좋은 글 잘 읽고 갑니다 🫡