Reflection

Huisu·2025년 6월 1일
1

ETC

목록 보기
7/8
post-thumbnail

자바의 Class 클래스 (Java.lang.Class)

자바 프로그래밍을 할 때 우리는 보통 변수나 클래스를 직접 만들어서 사용해 왔다. 그런데 어떤 경우에는 애플리케이션 실행 중에 클래스를 동적으로 불러와 다루어야 할 때가 생긴다. 즉 코드를 실행하기 전 컴파일 단에서 개발자가 직접 폴더를 뒤져 가며 클래스 정의문을 찾아 클래스 정보를 얻는 것이 아닌, 코드 상에서 호출 로직을 통해 클래스 정보를 얻어와 다루는 방식이다. 이는 런타임 시에 다이나믹하게 동적으로 클래스를 핸들링하는 기법이다. 이때 사용되는 것이 바로 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 클래스 객체 얻기

자바에서 Class 객체를 가져오는 방법은 총 3가지이다. 객체를 가져오는 것을 똑같지만 각자 다른 방식으로 처리하기 때문에, 이를 모두 잘 알아 둘 필요가 있다.

예시로 String 클래스 객체를 생성하고 세 가지의 방법으로 얻어 보겠다.

Object.getClass()로 얻기

  • 모든 클래스는 최상의 클래스인 Object 클래스에서 제공하는 getClass() 메서드를 통해 가져온다.
  • 단 해당 클래스가 인스턴스화된 상태여야 한다는 제약이 있다.
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);
}

.class 리터럴로 얻기

  • 인스턴스가 존재하지 않고 컴파일된 클래스만 있을 경우에는 리터럴로 Class 객체를 얻을 수 있다.
  • 가장 심플하게 Class 객체를 가져오는 방법이다.
public static void main(String[] args) {
	// 클래스 리터럴 (*.class)로 얻기
	Class<? extends String> exClass = String.class;
	// class java.lang.String
	System.out.println(exClass);
}

Class.forName()으로 얻기

  • 위의 리터럴 방식과 같이 컴파일된 클래스 파일이 있다면 클래스 이름만으로 Class 객체를 반환받을 수 있다.
  • 단 이때는 클래스의 도메인을 상세히 적어 주어야 한다. 그래서 클래스 파일 경로에 오타가 있으면 에러가 발생할 수 있다.
  • 만약 Class 객체를 찾지 못한다면 ClassNotFoundException을 발생시키기 때문에 예외처리가 강제된다.
  • 그러나 다른 두 가지 방법보다 forName을 통해 얻게 되면 메모리를 절약하며 동적 로딩을 할 수 있기 때문에 가장 성능이 좋다
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 클래스 메서드 종류

  • String getName(): 클래스의 이름을 리턴
  • Package getPackage(): 클래스의 패키지 정보를 패키지 클래스 타입으로 리턴
  • Field[] getFields(): public으로 선언된 변수를 Field 클래스 타입으로 리턴
  • Field[] getDeclaredFields(String name): name과 동일한 이름으로 정의된 변수를 Field 클래스 타입으로 리턴
  • Method[] getMethods(): public으로 선언된 모든 메소드 목록을 Method 클래스 배열 타입으로 리턴
  • Method getMethod(String name, Class ... parameterTypes): 지정된 이름과 매개변수 타입을 갖는 메소드를 Method 클래스 타입으로 리턴
  • Method[] getDeclaredMethods(): 해당 클래스에서 선언된 모든 메소드 정보를 리턴
  • Method getDeclaredMethod(String name, Class ... parameterTypes): 지정된 이름과 매개변수 타입을 갖는 해당 클래스에서 선언된 메소드를 Method 클래스 타입으로 리턴ㅣㅐㄴㅅㄴㅅㅁㄱㄴ005
  • Constructor[] getDeclaredConstructors(): 해당 클래스에서 선언된 모든 생성자의 정보를 Constructor 배열 타입으로 리턴
  • int getModifiers(): 해당 클래스의 접근자 정보를 int 타입으로 리턴
  • String toString(): 해당 클래스 객체를 문자열로 리턴

클래스 정보 출력하기

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

Reflection API 기법

위에서 본 것과 같이 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);
	}

}

동적으로 생성자 가져와 초기화하기

  • getConstructor()를 호출할 때 인자로 생성자의 매개변수 타입을 바인딩해 줘야 한다.
  • 만일 어떤 매개변수 타입을 지정해 주지 않으면 기본 생성자가 호출되게 된다.
  • 만약 해당하는 생성자를 찾지 못한다면 NoSuchMethodException이 발생한다.
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);
}

3개의 댓글

comment-user-thumbnail
2025년 6월 1일

Class라는 클래스가 따로 있다는 사실은 처음 알았네요!! 좋은 글 잘 읽고 갑니다 🫡

1개의 답글