Java - 심화 : 리플렉션

Seok-Hyun Lee·2021년 7월 19일
0

JAVA

목록 보기
16/21
post-thumbnail

리플렉션, Reflection

개념 정리

자바에는 다른 언어들과 다르게 '리플렉션'이라는 것이 있다.

  • 리플렉션을 위한 클래스들은 java.lang.reflect 패키지에서 제공
  • 리플렉션을하면, 어떤 객체 또는 그 객체의 생성자,필드,메소드가 속한 클래스와 관련한 정보를 알 수 있다.
  • 리플렉션을하면, 런타임에 접근제어자와 상관없이 메서드를 호출할 수 있다.

쉽게 말하면 Reflection 이란, 런타임에 클래스의 정보를 활용하고 수정할 수 있게 해주는 API 이다.

그리고 위의 그림처럼 Relfection은 객체의 클래스의 어노테이션이나 부모 클래스, 구현한 인터페이스 등 거의 모든 정보를 알 수 있다. 아래는 그 중 자주 사용되는 것만 정리한 것이다.

  • 클래스 : getClass() 메소드를 사용하여 객체가 속한 클래스명을 반환
  • 생성자 :getConstructors() 메소드를 사용하여 객체가 속한 클래스의 생성자들을 반환
  • 메소드 : getMethods() 메소드를 사용하여 객체가 속한 클래스의 메소드들을 반환

Class 클래스

java.lang.Class 는 자바의 클래스와 인터페이스의 메타 데이터를 관리하는 클래스이다. 즉, 클래스의 메타 데이터(클래스명, 메소드 정보, 생성자 정보, 필드 정보)를 런타임에 알 수 있다. 아래는 Class 클래스가 하는 일을 요약한 것이다.

  • 런타임에 클래스의 메타데이터(클래스명, 메소드 정보, 생성자 정보, 필드 정보)를 제공
  • 클래스가 런타임에 제공하는 기능을 테스트/수정 할 수 있는 메소드 제공

아래는 간단한 예이다.

Class c1 = int.class;
Class c2 = boolean.class;
Class c3 = void.class;
Class c4 = Double.class;

System.out.println(c1.getName());
System.out.println(c2.getName());
System.out.println(c3.getName());
System.out.println(c4.getName());

결과는 아래와 같다.

int
boolean
void
java.lang.Double

여기서 주의할 점은 자바에서는 Primitive Type들도 Class 객체로 간주된다. 아래는 java.doc의 내용이다

The primitive Java types (boolean, byte, char, short, int, long, float, and double), and the keyword void are also represented as Class objects.

사용 예제 1 - 정보 확인

우선, 본문의 가독성을 위해 코드의 전문은 최하단에 위치하였다. 현재 Main 클래스와 Dog 클래스만 있고 어떤 메소드를 부르고 어떤 결과가 나오는지를 위주로 확인만 하면 된다. 참고로 패키지는 reflection.demo 로 설정하였고 Dog 클래스 코드는 아래와 같다.

[Dog.java]

package reflection.demo;

public class Dog {
    private String name;
    private int age;

    public Dog(){
        this.name = "Bob";
        this.age = 2;
    }

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
    public int getAge() {return age;}
    public void setAge(int age) {this.age = age;}
    private void walk() {System.out.println("WALK");}
}

참고로 아래의 Constructor, Method, Field 클래스들은 java.lang.reflect 패키지에 있는 클래스들이다.

클래스

Class dogClass = Dog.class;
System.out.println("Class Name: " + dogClass.getName());
System.out.println("Interface? " + dogClass.isInterface());
System.out.println("Array? " + dogClass.isArray());

아래는 결과이다

public reflection.demo.Dog(java.lang.String,int)
Class Name: reflection.demo.Dog
Interface? false
Array? false

생성자

System.out.println("\n---Constructors---");
        Constructor[] constructors = dogClass.getConstructors();
        
        for(Constructor constructor : constructors) {
            System.out.println("Constructor name : " + constructor.getName());
            System.out.println("---params---");
            
            if (constructor.getParameterCount() == 0) {
                System.out.println("no-arg");
            } else {
                Parameter[] parameters = constructor.getParameters();
                for(Parameter parameter : parameters) {
                    System.out.println(parameter.getName() + " : " + parameter.getType());
                }
            }
        }

아래는 결과이다

---Constructors---
Constructor name : reflection.demo.Dog
---params---
no-arg
Constructor name : reflection.demo.Dog
---params---
arg0 : class java.lang.String
arg1 : int

메소드

        System.out.println("\n---Methods---");
        Method[] methods = dogClass.getMethods();                   // 부모의 메소드까지 전부 반환
        for(int i = 0 ; i < methods.length;i++) {
            System.out.println(
                    "Method#"+(i+1)+ " " +
                    Modifier.toString(methods[i].getModifiers()) + " " +
                    methods[i].getReturnType().getName() + " " +
                    methods[i].getName() + " - " +
                    Arrays.toString(methods[i].getParameterTypes()));
        }

        System.out.println("\n---DeclaredMethods---");
        Method[] declaredMethods = dogClass.getDeclaredMethods();   // 자신에게 명시된 메소드만 반환
        for(int i = 0 ; i < declaredMethods.length;i++) {
            System.out.println(
                    "Method#"+(i+1)+ " " +
                    Modifier.toString(declaredMethods[i].getModifiers()) + " " +
                            declaredMethods[i].getReturnType().getName() + " " +
                            declaredMethods[i].getName() + " - " +
                    Arrays.toString(declaredMethods[i].getParameterTypes()));
        }

주의할 점은 methods() 와 declaredMethods() 메소드가 반환하는 메소드 배열이 다르다.

  • getMethods() : public 접근 제어자에 한해 부모 클래스의 메소드까지 전부 반환
  • declaredMethods() : 자신에게 명시된 모든 메소드만 반환,

아래는 그 결과이다.

---Methods---
Method#1 public java.lang.String getName - []
Method#2 public void setName - [class java.lang.String]
Method#3 public int getAge - []
Method#4 public void setAge - [int]
Method#5 public final native void wait - [long]
Method#6 public final void wait - [long, int]
Method#7 public final void wait - []
Method#8 public boolean equals - [class java.lang.Object]
Method#9 public java.lang.String toString - []
Method#10 public native int hashCode - []
Method#11 public final native java.lang.Class getClass - []
Method#12 public final native void notify - []
Method#13 public final native void notifyAll - []

---DeclaredMethods---
Method#1 public java.lang.String getName - []
Method#2 public void setName - [class java.lang.String]
Method#3 private void walk - []
Method#4 public int getAge - []
Method#5 public void setAge - [int]

필드

코드를 입력하세요

필드도 마찬가지로 fields() 와 declaredFields() 가 따로 있다.

  • fields() : public 한 필드만 반환
  • declaredFields() : 명시된 모든 필드 반환

아래는 그 결과이다.

---Fields---

---DeclaredFields---
private java.lang.String name
private int age

사용 예제 2 - 정보 수정/추가

아래는 리플렉션을 활용해 정보를 수정하거나 추가하는 작업을 하기 위해 새롭게 정의한 Job 클래스이다

package reflection.demo;

public class Job {

    private String title;
    private int wage;
    private int hours;

    public Job(String title, int wage, int hours) {
        this.title = title;
        this.wage = wage;
        this.hours = hours;
    }

    // Custom
    private void goToWork() {System.out.println("GO TO WORK");}
    public void commuteTime(int start, int end) { System.out.println("Start from : " + start + " End to " + end);}
    // Getter and Setter
    public String getTitle() {return title;}
    public void setTitle(String title) { this.title = title; }
    public int getWage() {return wage;}
    public void setWage(int wage) {this.wage = wage;}
    public int getHours() {return hours;}
    public void setHours(int hours) {this.hours = hours;}
}

객체 생성

Class jobClass = Job.class;

// 생성자에 public 접근 제어자 명시 필수
Constructor<Job> constructor = jobClass.getConstructor(String.class, int.class, int.class); 

// newInstance() : Object 타입 객체 생성 -> 캐스팅 필요
Job job = (Job) constructor.newInstance("Cleaning",40,30);                          
  • 리플렉션으로 클래스의 생성자 정보 가져오기
  • Constructor 타입의 레퍼런스과 함께 사용하고자 하는 생성자에 맞게 매개 인자로 Class 타입 선언
  • Constructor 레퍼런스에 newInstance()로 인스턴스 멤버 변수 초기화 및 객체 생성

여기서 주의할 점은 newInstance()로 만들어지는 객체는 Object 타입이라 따로 캐스팅이 필요하다.

메소드 사용

// 오직, public 접근 제어 메소드, 탐색 범위 : 부모 클래스까지
Method method1 = jobClass.getMethod("commuteTime", int.class, int.class);   
// 메소드를 호출할 객체를 인자로 전달
method1.invoke(job,9,18);        

// 모든 접근 제어 메소드, 탐색 범위 : 자기 자신
Method method2 = jobClass.getDeclaredMethod("goToWork");  
try {
    // private 메소드를 참조할 순 있지만 호출하면 예외 발생
    method2.invoke(job);
} catch (IllegalAccessException  e) {
    System.out.println(e);
}

이전 예제에서 다뤘듯이 get 과 getDeclared 는 메소드를 탐색하는 범위와 접근 제어 제약이 다르니까 이를 주의해서 사용해야 한다. 그리고 method2와 같은 private 메소드는 Method 레퍼런스로 참조하였어도 일반적으로는 호출 시 예외를 발생시킨다.

method2.setAccessible(true);

하지만 setAccessible()메소드로 private한 메소드에도 접근 가능하게끔 만들 수 있다. 여기서 주의할 점은 한 번 접근 제어를 변경시켜놓으면 계속 해당 상태가 유지되니 사용 후에는 복구시켜야 한다.

  • true : Java Language Access Control이 접근 제어 검사 막음, public화
  • false : Java Language Access Control이 접근 제어 검사 실행

필드 수정

// private 접근 제어 필드도 탐색 가능
Field jobTitle = jobClass.getDeclaredField("title");
// 접근 제어 검사 막기, private 필드를 수정가능하게 변경
jobTitle.setAccessible(true);
jobTitle.set(job,"Washing");
// 접근 제어 검사 실행
jobTitle.setAccessible(false);
System.out.println("Changed Job Title: " + job.getTitle());

클래스의 필드에 대한 리플렉션을 통해 객체의 필드를 수정하는 예시이다. 여기서도 private 접근 제어를 가진 필드를 수정하기 위해 setAccessible(true)를 설정한 것을 볼 수 있다. 물론, 이런 방식은 추천하지 않는다.

마무리

장점

  • 확장성 기능: 리플렉션을 활용해 확장성(기존 객체의 정보 변경없이 확장할 수 있는) 객체들의 인스턴스를 활용하는 외부 사용자 클래스로 가능
  • 디버깅과 테스팅: private 접근 제어 상관 없이 로직 전체에서 디버깅과 테스팅 지원
  • 클래스 브라우저와 비주얼 개발 환경: 클래스들을 열거형으로 관리 및 리플렉션을 통한 타입 정보를 활용해 개발 중 오류 방지

참고로 이러한 API가 제공됨으로써 자바가 가지게 된 특징이자 JVM을 기반으로하는 언어들이 가지는 특징은 동적 로딩이다. 동적 로딩에 대한 얘기는 다른 포스트에서 다루도록 하겠다.
-> 동적 로딩 관련 글 보러 가기

단점

  • 성능의 문제, 리플렉션은 런타임에 동적으로 이뤄지는 특징 때문에 JVM의 최적화 작업에 영향을 준다. 결과적으로 성능이 느려지기 때문에 성능에 민감한 애플리케이션인 경우, 코드 섹션에서 자주 호출될 때는 사용을 지양해야 한다.
  • 보안의 문제, 런타임에도 접근 제어와 상관없이 클래스 내부를 확인 및 수정이 가능한 문제

코드

정보 확인 - Main.java 전문

package reflection.demo;

import java.lang.reflect.*;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {

        Dog dog = new Dog("Doggy",5);


        Class dogClass = Dog.class;

        System.out.println(dogClass.getConstructors()[1]);

        System.out.println("Class Name: " + dogClass.getName());

        System.out.println("Interface? " + dogClass.isInterface());
        System.out.println("Array? " + dogClass.isArray());


        System.out.println("\n---Constructors---");
        Constructor[] constructors = dogClass.getConstructors();
        for(Constructor constructor : constructors) {
            System.out.println("Constructor name : " + constructor.getName());
            System.out.println("---params---");
            if (constructor.getParameterCount() == 0) {
                System.out.println("no-arg");
            } else {
                Parameter[] parameters = constructor.getParameters();
                for(Parameter parameter : parameters) {
                    System.out.println(parameter.getName() + " : " + parameter.getType());
                }
            }
        }

        System.out.println("\n---Methods---");
        Method[] methods = dogClass.getMethods();                   // 부모의 메소드까지 전부 반환
        for(int i = 0 ; i < methods.length;i++) {
            System.out.println(
                    "Method#"+(i+1)+ " " +
                    Modifier.toString(methods[i].getModifiers()) + " " +
                    methods[i].getReturnType().getName() + " " +
                    methods[i].getName() + " - " +
                    Arrays.toString(methods[i].getParameterTypes()));
        }

        System.out.println("\n---DeclaredMethods---");
        Method[] declaredMethods = dogClass.getDeclaredMethods();   // 자신에게 명시된 메소드만 반환
        for(int i = 0 ; i < declaredMethods.length;i++) {
            System.out.println(
                    "Method#"+(i+1)+ " " +
                    Modifier.toString(declaredMethods[i].getModifiers()) + " " +
                            declaredMethods[i].getReturnType().getName() + " " +
                            declaredMethods[i].getName() + " - " +
                    Arrays.toString(declaredMethods[i].getParameterTypes()));
        }

        System.out.println("\n---Fields---");
        Field[] fields = dogClass.getFields();      // public 으로 선언된 필드만 반환
        for(Field field : fields) {
            System.out.println(Modifier.toString(field.getModifiers()) + " " + field.getType().getName() + " " + field.getName());
        }

        System.out.println("\n---DeclaredFields---");
        Field[] declaredFields = dogClass.getDeclaredFields();      // 명시된 모든 필드 반환
        for(Field field : declaredFields) {
            System.out.println(Modifier.toString(field.getModifiers()) + " " + field.getType().getName() + " " + field.getName());
        }
    }
}

정보 수정/추가 - Main.java 전문

package reflection.demo;

import java.lang.reflect.*;

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        Class jobClass = Job.class;
        Constructor<Job> constructor = jobClass.getConstructor(String.class, int.class, int.class); // 생성자에 public 접근 제어자 명시 필수
        Job job = (Job) constructor.newInstance("Cleaning",40,30);                          // newInstance() : Object 타입 객체 생성 -> 캐스팅 필요

        Method method1 = jobClass.getMethod("commuteTime", int.class, int.class);   // 오직, public 접근 제어 메소드, 탐색 범위 : 부모 클래스까지
        method1.invoke(job,9,18);                                                   // 메소드를 호출할 객체를 인자로 전달
        Method method2 = jobClass.getDeclaredMethod("goToWork");                    // 모든 접근 제어 메소드, 탐색 범위 : 자기 자신
        try {
            method2.invoke(job);                                                          // private 메소드를 참조할 순 있지만 호출하면 예외 발생
        } catch (IllegalAccessException  e) {
            System.out.println(e);
        }



        Field jobTitle = jobClass.getDeclaredField("title");                        // private 접근 제어 필드도 탐색 가능
        jobTitle.setAccessible(true);                                                     // 접근 제어 검사 막기, private 필드를 수정가능하게 변경
        jobTitle.set(job,"Washing");
        jobTitle.setAccessible(false);                                                    // 접근 제어 검사 실행
        System.out.println("Changed Job Title: " + job.getTitle());
    }
}
profile
Arch-ITech

0개의 댓글