from Dictionary - Reflection

Reflection

java.lang.Class

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

Class 클래스는 java.lang.Class 패키지에 별도로 존재하는 독립형 클래스로서, 자신이 속한 클래스의 모든 멤버 정보를 담고 있기 때문에 런타임 환경에서 동적으로 저장된 클래스나 인터페이스 정보를 가져오는데 사용된다. 여기서 오해하지 말아야 할 것이 클래스 자료형을 말하는게 아니라 클래스 이름이 “Class” 인 클래스를 말하는 것이다.

JVM의 클래스 로더(class loader)는 실행 시에 필요한 클래스를 동적으로 메모리에 로드하는 역할을 한다. 먼저 기존에 생성된 클래스 객체가 메모리에 존재하는지 확인하고 있으면 객체의 참조를 반환하고, 없으면 classpath에 지정된 경로를 따라서 클래스 파일을 찾아 해당 클래스 파일을 읽어서 Class 객체로 변환한다. 만일 못 찾으면 우리가 익히아는 ClassNotFoundException 예외를 띄우게 된다.

Class 객체 얻기

Object.getClass()

  • 모든 클래스의 최상위 클래스인 Object 클래스에서 제공하는 getClass() 메서드를 통해 가져온다.
  • 해당 클래스가 인스턴스화 된 상태 이어야 한다는 제약이 있다.

java Class<? extends String> cls = str.getClass();

.class 리터럴로 얻기

  • 인스턴스가 존재하지 않고, 컴파일된 클래스 파일만 있다면 리터럴로 Class 객체를 곧바로 얻을 수 있다.
  • 가장 심플하게 Class 객체를 가져오는 방법이다.

java Class<? extends String> cls2 = String.class;

Class.forName() 으로 얻기

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

java Class<?> cls3 = Class.forName("java.lang.String");

Class 클래스 객체를 forName() 메서드를 통해 가져오는 방법을 ‘동적 로딩’이라고 부른다. 보통 다른 클래스 파일을 불러올때는 컴파일 시 JVM의 Method Area에 클래스 파일이 같이 바인딩(binding)이 되지만, forName()으로 .class파일을 불러올 때는 컴파일에 바인딩이 되지않고 런타임때 불러오게 되기 때문에 동적 로딩이라고 부른다. 그래서 컴파일 타입에 체크 할 수 없기 때문에 클래스 유무가 확인되지 않아 예외 처리를 해주어야 하는 이유이기도 하다.

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(): 해당 클래스 객체를 문자열로 리턴한다.

Reflection API

Class 객체를 이용하면 클래스에 대한 모든 정보(클래스의 정의된 멤버의 이름이나 개수 등)를 런타임 단에서 코드 로직으로 얻을 수 있다는 것을 알았다. 클래스 정보들을 실행부에서 얻을 수 있는 점은 꽤나 매력적인데, 이러한 정보들을 이용하여 오로지 Class 객체만으로 본 클래스를 인스턴스화 할 수 있고, 메서드를 호출 할 수 있는 ..등 보다 동적인 코드를 작성할 수 있게 된다. 이처럼 구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(메소드, 타입, 변수, …)에 접근할 수 있게 해주는 자바 기법을 Reflection API 라고 부른다. 자바 리플렉션(Reflection - 사전적 의미 : 거울 등에 비친, 반사)은 객체를 통해 클래스의 정보를 분석하여 런타임에 클래스의 동작을 검사하거나 조작하는 프로그램 기법이다. 클래스 파일의 위치나 이름만 있다면 해당 클래스의 정보를 얻어내고, 객체를 생성하는 것 또한 가능하게 해주어 유연한 프로그래밍을 가능케 해준다.

사용법

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

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
}
  • 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
}
  • getMethod() 를 호출할때 인자로 생성자의 매개변수 타입을 바인딩 해주어야 한다.
  • 만약 매개변수가 없는 메소드라면 메소드 명만 입력해주면 된다.
  • 실행은 Method 타입에서 제공하는 invoke()를 호출하여 실행하면 된다.
    • instance 메소드 - 매개변수로 인스턴스 필요
    • 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
}
  • getField() 를 통해 클래스의 필드를 얻을 수 있다.
  • 필드 값 변경은 set() 메서드를 호출하면 된다.
  • 필드는 클래스가 인스턴스가 되어야 Heap 메모리에 적재됨으로 인스턴스가 필요하다.
  • 다만, static 필드라면 Method Area에 이미 적재되어 있으므로 인스턴스가 필요없다.