...
자바의 Class 클래스 (Java.lang.Class)
자바 프로그래밍을 할때 우리는 보통 변수나 클래스를 직접 선언하고 만들어 사용하여 왔다. 그런데 어떤 경우에는 애플리케이션 실행 중에서 클래스를 동적으로 불러와 다루어야 할 경우가 생긴다. 즉, 코드를 실행하기전 컴파일 단에서 개발자가 직접 폴더를 뒤져가며 클래스 정의문을 찾아 클래스 정보를 얻는 것이 아닌, 코드 상에서 호출 로직을 통해 클래스 정보를 얻어와 다룸으로써 런타임 단에서 다이나믹하게 클래스를 핸들링 하는 것이다. 이때 사용되는 것이 바로 Class 클래스 객체이다.
Class 클래스는 java.lang.Class 패키지에 별도로 존재하는 독립형 클래스로서, 자신이 속한 클래스의 모든 멤버 정보를 담고 있기 때문에 런타임 환경에서 동적으로 저장된 클래스나 인터페이스 정보를 가져오는데 사용된다. 여기서 오해하지 말아야 할 것이 클래스 자료형을 말하는게 아니라 클래스 이름이 "Class" 인 클래스를 말하는 것이다.
자바의 모든 클래스와 인터페이스는 컴파일 후 .java → .class 파일로 변환된다. 이 .class 파일에는 멤버변수, 메서드, 생성자 등 객체의 정보들이 들어 있는데, JVM의 클래스 로더(ClassLoader)에 의해서 클래스 파일이 메모리에 올라갈 때, Class 클래스는 이 .class 파일의 클래스 정보들을 가져와 힙 영역에 자동으로 객체화가 되게 된다. 그래서 따로 new 인스턴스화 없이 바로 가져와 사용하면 된다.
JVM의 클래스 로더(class loader)는 실행 시에 필요한 클래스를 동적으로 메모리에 로드하는 역할을 한다.
먼저 기존에 생성된 클래스 객체가 메모리에 존재하는지 확인하고 있으면 객체의 참조를 반환하고, 없으면 classpath에 지정된 경로를 따라서 클래스 파일을 찾아 해당 클래스 파일을 읽어서 Class 객체로 변환한다.
만일 못 찾으면 우리가 익히아는 ClassNotFoundException 예외를 띄우게 된다.
Class 클래스 객체 얻기
자바에서 Class 객체를 가져오는 방법은 총 3가지가 존재한다. 객체를 가져오는 것을 똑같지만 각기 방식에 따라 차이가 존재하니 모두 알아둘 필요가 있다.
다음은 우리가 익히아는 String 클래스 객체를 생성하고, 세가지 방법으로 String 클래스의 Class 클래스 정보를 얻는 예시이다.
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
}
.class 리터럴로 얻기
- 인스턴스가 존재하지 않고, 컴파일된 클래스 파일만 있다면 리터럴로 Class 객체를 곧바로 얻을 수 있다.
- 가장 심플하게 Class 객체를 가져오는 방법이다.
public static void main(String[] args) {
// 클래스 리터럴(*.class)로 얻기
Class<? extends String> cls2 = String.class;
System.out.println(cls2); // class java.lang.String
}
Class.forName() 으로 얻기
- 위의 리터럴 방식과 같이 컴파일된 클래스 파일이 있다면 클래스 이름만으로 Class 객체를 반환 받을 수 있다.
- 단, 이때는 클래스의 도메인을 상세히 적어주어야 한다. 그래서 클래스 파일 경로에 오타가 있으면 에러가 발생할 수 있기 때문에 주의해야한다. (대소문자 실수 등)
- 만일 Class 객체를 찾지 못한다면
ClassNotFoundException를 발생 시키기 때문에 예외처리가 강제된다. - 그러나 다른 두가지 방법보다 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) {}
}
Class 클래스 객체를forName()메서드를 통해 가져오는 방법을 '동적 로딩'이라고 부른다. 보통 다른 클래스 파일을 불러올때는 컴파일 시 JVM의 Method Area에 클래스 파일이 같이 바인딩(binding)이 되지만,forName()으로 .class파일을 불러올 때는 컴파일에 바인딩이 되지않고 런타임때 불러오게 되기 때문에 동적 로딩이라고 부른다.
그래서 컴파일 타입에 체크 할 수 없기 때문에 클래스 유무가 확인되지 않아 예외 처리를 해주어야 하는 이유이기도 하다.
대부분의 클래스 정보는 프로그램이 로딩될 때 이미 Method Area 메모리에 적재된다.
그런데 어떤 시스템은 오라클, MySQL, SQL Server 등 여러 종류의 데이터베이스와 연동할 수 있다고 하는데, 그렇다고 이 시스템을 컴파일할 때 모든 데이터베이스의 라이브러리(드라이버)를 같이 컴파일할 필요는 없다. 시스템을 구동할 때 어떤 DB와 연결할지 결정되면 해당 드라이버만 로딩하면 되기 때문이다. 이럴때forName()을 통해 메모리적으로 아끼면서 클래스를 동적으로 로딩하면 좋다.
Class 클래스 메서드 종류
String getName(): 클래스의 이름을 리턴한다.Package getPackage(): 클래스의 패키지 정보를 패키지 클래스 타입으로 리턴한다.Field[] getFields(): public으로 선언된 변수 목록을 Field 클래스 배열 타입으로 리턴한다.Field getField(String name): public으로 선언된 변수를 Field 클래스 타입으로 리턴한다.Field[] getDeclaredFields(): 해당 클래스에서 정의된 변수 목록을 field 클래스 배열 타입으로 리턴한다.Field getDeclaredField(String name): name과 동일한 이름으로 정의된 변수를 Field 클래스 타입으로 리턴한다.Method[] getMethods(): public으로 선언된 모든 메소드 목록을 Method 클래스 배열 타입으로 리턴한다. 해당 클래스에서 사용 가능한 상속받은 메소드도 포함된다.Method getMethod(String name, Class... parameterTypes): 지정된 이름과 매개변수 타입을 갖는 메소드를 Method 클래스 타입으로 리턴한다.Method[] getDeclaredMethods(): 해당 클래스에서 선언된 모든 메소드 정보를 리턴한다.Method getDeclaredMethod(String name, Class... parameterTypes): 지정된 이름과 매개변수 타입을 갖는 해당 클래스에서 선언된 메소드를 Method 클래스 타입으로 리턴한다.Constructor[] getConstructors(): 해당 클래스에 선언된 모든 public 생성자의 정보를 Constructor 배열 타입으로 리턴한다.Constructor[] getDeclaredConstructors(): 해당 클래스에서 선언된 모든 생성자의 정보를 Constructor 배열 타입으로 리턴한다.int getModifiers(): 해당 클래스의 접근자(modifier) 정보를 int 타입으로 리턴한다.String toString(): 해당 클래스 객체를 문자열로 리턴한다.
클래스 정보 출력하기
Class 클래스 객체에는 자신이 속한 클래스의 각종 멤버 정보들이 들어있다고 했었다. 이렇게 받아온 Class 객체를 이용하여 자신이 속한 클래스의 정보를 출력할 수 있다.
public static void main(String[] args) {
// String 객체로부터 클래스 정보를 얻는다
Class<? extends String> cls = String.class;
// 클래스의 이름만 호출한다.
System.out.println("1. " + cls.getSimpleName()); // 1. String
// 패키지의 이름을 호출한다.
System.out.println("2. " + cls.getPackage()); // 2. package java.lang
// 패키지와 이름을 호출한다.
System.out.println("3. " + cls.getName()); // 3. java.lang.String
// 클래스와 패키지 이름을 호출한다.
System.out.println("4. " + cls.toString()); // 4. class java.lang.String
// 제어자부터 패키지 이름 모두다 호출한다.
System.out.println("5. " + cls.toGenericString()); // 5. public final class java.lang.String
}
클래스 구성 출력하기
public static void main(String[] args) {
Class<? extends String> cls = String.class;
// 가지고 있는 필드를 모두 출력한다.
System.out.println("6. " + Arrays.toString(cls.getFields())); // 6. [public static final java.util.Comparator java.lang.String.CASE_INSENSITIVE_ORDER]
// 가지고 있는 메서드를 모두 호출한다.
System.out.println("7. " + Arrays.toString(cls.getMethods())); // 7. [public boolean java.lang.String.equals(java.lang.Object), public java.lang.String java.lang.String.toString() ...]
// 인터페이스 목록을 모두 가져온다.
System.out.println("8. " + Arrays.toString(cls.getInterfaces())); // 8. [interface java.io.Serializable, interface java.lang.Comparable, interface java.lang.CharSequence]
// super 클래스를 가져온다
System.out.println("9. " + cls.getSuperclass()); // 9. class java.lang.Object
}
Reflection API 기법
바로 위에서 봤듯이, Class 객체를 이용하면 클래스에 대한 모든 정보(클래스의 정의된 멤버의 이름이나 개수 등)를 런타임 단에서 코드 로직으로 얻을 수 있다는 것을 알았다.
클래스 정보들을 실행부에서 얻을 수 있는 점은 꽤나 매력적인데, 이러한 정보들을 이용하여 오로지 Class 객체만으로 본 클래스를 인스턴스화 할 수 있고, 메서드를 호출 할 수 있는 ..등 보다 동적인 코드를 작성할 수 있게 된다. 이처럼 구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(메소드, 타입, 변수, ...)에 접근할 수 있게 해주는 자바 기법을 Reflection API 라고 부른다.
자바 리플렉션(Reflection - 사전적 의미 : 거울 등에 비친, 반사)은 객체를 통해 클래스의 정보를 분석하여 런타임에 클래스의 동작을 검사하거나 조작하는 프로그램 기법이다. 클래스 파일의 위치나 이름만 있다면 해당 클래스의 정보를 얻어내고, 객체를 생성하는 것 또한 가능하게 해주어 유연한 프로그래밍을 가능케 해준다.
리플렉션은 애플리케이션 개발에서보다는 프레임워크, 라이브러리에서 많이 사용된다. 왜냐하면 프레임워크, 라이브러리는 사용하는 사람이 어떤 클래스명과 멤버들을 구성할지 모르는데, 이러한 사용자 클래스들을 기존의 기능과 동적으로 연결 시키기 위하여 리플렉션을 사용한다고 보면 된다. 대표적인 예로는 스프링의 DI(dpendency injection), Proxy, ModelMapper 등이 있다. 이미 Spring, Hibernate, Lombok 등 많은 프레임워크에서 Reflection 기능을 사용하고 있다.
자바 리플렉션 사용법
다음은 리플렉션 예제에 쓰일 준비물 클래스이다. Person 클래스에는 public과 private, static한 필드와 메소드를 가지고 있으며, 생성자도 아규먼트에 따라 두가지로 준비하였다. 이제 이 복잡하게 구성되어 있는 Person 클래스의 멤버 정보들을 리플렉션으로 동적으로 불러와 어떻게 다이나믹하게 다룰수 있는지 체험해보자.
// 필요 패키지 로드
import java.lang.reflect.*;
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;
}
}
동적으로 생성자 가져와 초기화하기
getConstructor()를 호출할때 인자로 생성자의 매개변수 타입을 바인딩 해주어야 한다.- 만일 어떠한 매개변수 타입을 지정해주지 않으면 기본 생성자가 호출되게 된다.
- 만약 해당하는 생성자를 찾지 못하면 NoSuchMethodException이 발생된다.
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
}
동적으로 메서드 가져와 실행하기
getMethod()를 호출할때 인자로 생성자의 매개변수 타입을 바인딩 해주어야 한다.- 만약 매개변수가 없는 메소드라면 메소드 명만 입력해주면 된다.
- 실행은 Method 타입에서 제공하는
invoke()를 호출하여 실행하면 된다.- instance 메소드 - 매개변수로 인스턴스 필요
- static 메소드 - 매개변수 필요 없음
- private 메소드 - invoke 하기전에 공개화 할 필요있음
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
}
동적으로 필드 가져와 조작하기
getField()를 통해 클래스의 필드를 얻을 수 있다.- 필드 값 변경은
set()메서드를 호출하면 된다. - 필드는 클래스가 인스턴스가 되어야 Heap 메모리에 적재됨으로 인스턴스가 필요하다.
- 다만, static 필드라면 Method Area에 이미 적재되어 있으므로 인스턴스가 필요없다.
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
}
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
}
이렇게 리플렉션을 사용하면 클래스와 메서드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다. 하지만 리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다는 단점이 존재한다.
만일 리플렉션으로 getMethod("메서드명") 를 통해 클래스의 메서드를 가져온다고 가정했을 때 인자로 존재하지도 않는 메서드명을 기재할 경우, 해당 코드를 직접 실행해야 오류가 발생해 미리 대비할수 없게 된다.
가장 좋은 오류는 개발자가 즉시 확인할 수 있는 컴파일 오류이고, 가장 무서운 오류는 사용자가 직접 실행할 때 발생하는 런타임 오류다. 따라서 리플렉션은 초보자가 일반적으로 사용하는데는 유의해야한다.
# 참고자료
https://www.youtube.com/watch?v=Mc6OaicCZVA
https://www.youtube.com/watch?v=NI6QZy6juc8
https://12bme.tistory.com/129
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.