티스토리 뷰

728x90
반응형

리플렉션(Reflection)이란?


  • 리플렉션은 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API입니다.
  • 💡 컴파일 시간이 아닌 실행 시간에 동적으로 특정 클래스의 정보를 추출할 수 있는 프로그래밍 기법입니다.

 

리플렉션은 언제 사용될까?
  • 동적으로 클래스를 사용해야할 때 필요합니다.
  • 다시 말해 작성 시점에는 어떠한 클래스를 사용해야 할지 모르지만 런타임 시점에서 클래스를 가져와서 실행해야하는 경우 필요합니다.
  • 대표적으로는 Spring 프레임워크의 어노테이션 같은 기능들이 리플렉션을 이용하여 프로그램 실행 도중 동적으로 클래스의 정보를 가져와서 사용합니다.

 

리플렉션은 어떤 정보를 가져올 수 있을까?
  • 아래와 같은 정보들을 가져올 수 있으며 해당 정보들을 가져와서 객체를 생성하거나 메서드를 호출하거나 변수의 값을 변경할 수 있습니다.
  • Class
  • Constructor
  • Method
  • Field

 

예제

Step 01 - Animal Class

public class Animal {
    public String name = "myName ?";
    private String city = "myCity ?";

    public Animal() {
    }

    private void sleep(){
        System.out.println("sleep");
    }

    private void eat(String x){
        System.out.println("eat : " + x);
    }

    private void walk(){
        System.out.println("walk");
    }
}

 

Step 02 - Dog Class

public class Dog extends Animal{
    private String myName = "뽀삐";
    public  String myCity = "서울";

    public Dog() {
    }

    private Dog(String myName) {
        this.myName = myName;
    }

    private void myName(String name){
        System.out.println("myName : " + name);
    }

    private void myCity(String city){
        System.out.println("myCity : " + city);
    }
    
    private void hello(){
        System.out.println("hello~");
    }
}

 

Step 03 - Example Class

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Field;

public class Example {
    public static void main(String[] args) throws Exception{

    }
}

 

Class 찾기
  • 클래스 Class 객체는 클래스 또는 인터페이스를 가리킵니다. java.lang.Class이며 import를 하지 않고 사용할 수 있습니다.

 

Case 01 - class를 알고 있다는 전제

  • 아래 코드를 보시면 Dog.class 처럼 클래스 정보를 할당할 수 있습니다. Class객체는 여러 메서드를 제공하고 있으며 getName()은 클래스의 이름을 리턴합니다.
public class Example {
    public static void main(String[] args) throws Exception{
        Class cls = Dog.class;

        System.out.println("Class Name : " + cls.getName());
        // Class Name : test.Dog 출력
    }
}

 

Case 02 - class를 참조할 수 없고 이름만 알고 있는 상황이라면?

  • 아래 예제는 클래스 이름만으로 클래스의 정보를 가져옵니다.
  • Class,forName() 메서드에 클래스 이름을 인자로 전달하여 클래스 정보를 가져올 수 있습니다.
  • 패키지 네임이 포함된 클래스 이름으로 작성해야 합니다.
public class Example {
    public static void main(String[] args) throws Exception{
        Class cls = Class.forName("test.Dog");

        System.out.println("Class Name : " + cls.getName());
        // Class Name : test.Dog 출력
    }
}

 

Constructor 찾기

Case 01 - 인자가 없는 생성자 가져오기

  • getDeclaredConstructor() 메서드에 아무런 내용을 작성하지 않으면 인자가 없는 기본 생성자를 가져올 수 있습니다.
  • 기본 생성자가 없고 오버로딩된 생성자만 있다면 java.lang.NoSuchMethodException 예외를 발생시킵니다.
public class Example {
    public static void main(String[] args) throws Exception{
        Class cls = Class.forName("test.Dog");
        Constructor constructor = cls.getDeclaredConstructor();

        System.out.println("Constructor : " + constructor.getName());
        // Constructor : test.Dog 출력
    }
}

 

Case 02 - 인자가 있는 생성자 가져오기

  • getDeclaredConstructor(Param)에 인자를 넣으면 해당 타입과 일치하는 생성자를 찾습니다.
public class Example {
    public static void main(String[] args) throws Exception{
        Class cls = Class.forName("test.Dog");
        Constructor constructor = cls.getDeclaredConstructor(String.class);

        System.out.println("Constructor : " + constructor.getName());
        // Constructor : test.Dog 출력
    }
}

 

Case 03 - 모든 생성자 가져오기

  • getDeclaredConstructors() 메서드를 사용하면 클래스의 private, public 등의 모든 생성자를 리턴해줍니다.
public class Example {
    public static void main(String[] args) throws Exception{
        Class cls = Class.forName("test.Dog");
        Constructor constructors[] = cls.getDeclaredConstructors();

        for (Constructor item : constructors){
            System.out.println("Get constructors : " + item);
            // Get constructors : public test.Dog()
            // Get constructors : public test.Dog(java.lang.String)
        }
    }
}

 

Case 04 - public 생성자만 가져오기

  • getConstructors() 메서드를 사용하면 클래스의 public 생성자를 리턴할 수 있습니다.
public class Example {
    public static void main(String[] args) throws Exception{
        Class cls = Class.forName("test.Dog");
        Constructor constructors[] = cls.getConstructors();

        for (Constructor item : constructors){
            System.out.println("Get public constructors : " + item);
            // Get public constructors : public test.Dog()
        }
    }
}

 

Method 찾기
  • getDeclaredMethod() 메서드를 사용하여 인자로 메서드의 파라미터 정보를 넘겨주면 일치하는 것을 찾을 수 있습니다.

Case 01 - 인자가 있는 메서드 가져오는 방법

import java.lang.reflect.Method;

public class Example {
    public static void main(String[] args) throws Exception{
        Class cls = Class.forName("test.Dog");
        Method method = cls.getDeclaredMethod("myName", String.class);

        System.out.println("Method : " + method);
        // Method : private void test.Dog.myName(java.lang.String)
    }
}

 

Case 02 - 인자가 없는 메서드 가져오는 방법

import java.lang.reflect.Method;

public class Example {
    public static void main(String[] args) throws Exception{
        Class cls = Class.forName("test.Dog");
        Method method = cls.getDeclaredMethod("hello", null);

        System.out.println("Method : " + method);
        // Method : Method : private void test.Dog.hello()
    }
}

 

Case 03 - 모든 메서드를 가져오는 방법

import java.lang.reflect.Method;

public class Example {
    public static void main(String[] args) throws Exception{
        Class cls = Class.forName("test.Dog");
        Method methods[] = cls.getDeclaredMethods();

        for (Method item : methods) {
            System.out.println("Method : " + item);
            // Method : private void test.Dog.hello()
            // Method : private void test.Dog.myName(java.lang.String)
            // Method : private void test.Dog.myCity(java.lang.String)
        }
    }
}

 

Case 04 - 상속받은 메서드와 public 메서드만 가져오는 방법

  • getMethods() 메서드는 public 메서드를 리턴하며, 상속받은 메서드들도 모두 찾아줍니다.
import java.lang.reflect.Method;

public class Example {
    public static void main(String[] args) throws Exception{
        Class cls = Class.forName("test.Dog");
        Method methods[] = cls.getMethods();

        for (Method item : methods) {
            System.out.println("Method : " + item);
        }
    }
}

 

Output: 

Method : public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
Method : public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
Method : public final void java.lang.Object.wait() throws java.lang.InterruptedException
Method : public boolean java.lang.Object.equals(java.lang.Object)
Method : public java.lang.String java.lang.Object.toString()
Method : public native int java.lang.Object.hashCode()
Method : public final native java.lang.Class java.lang.Object.getClass()
Method : public final native void java.lang.Object.notify()
Method : public final native void java.lang.Object.notifyAll()

 

Field 찾기

Case 01 - getDeclaredField() 메서드를 사용하여 전달된 이름과 일치하는 필드를 찾아줍니다.

public class Example {
    public static void main(String[] args) throws Exception{
        Class cls = Class.forName("test.Dog");
        Field field = cls.getDeclaredField("myName");

        System.out.println(field);
        // private java.lang.String test.Dog.myName
    }
}

 

Case 02 - getDeclaredFields() 메서드를 사용하여 객체에 선언된 모든 필드를 찾아줍니다. 단 상속받은 객체의 정보는 찾아주지 않습니다.

public class Example {
    public static void main(String[] args) throws Exception{
        Class cls = Class.forName("test.Dog");
        Field fields[] = cls.getDeclaredFields();

        for (Field item : fields) {
            System.out.println(item);
            // private java.lang.String test.Dog.myName
            // public java.lang.String test.Dog.myCity
        }
    }
}

 

Case 03 - getFields() 메서드를 사용하면 상속받은 객체의 public 필드까지 찾아줍니다.

import java.lang.reflect.Field;

public class Example {
    public static void main(String[] args) throws Exception{
        Class cls = Class.forName("test.Dog");
        Field fields[] = cls.getFields();

        for (Field item : fields) {
            System.out.println(item);
            // public java.lang.String test.Dog.myCity
            // public java.lang.String com.kdg.bootStudy.Animal.name
        }
    }
}

 

Field 변경

- 클래스로부터 변수 정보를 가져와 객체의 변수의 값을 변경할 수 있습니다.

 

Case 01 - getField() 메서드를 사용하면 객체의 public 필드를 찾아서 값을 변경할 수 있습니다.

public class Example {
    public static void main(String[] args) throws Exception{
        Dog dog = new Dog();
        Class cls = Class.forName("test.Dog");
        Field field = cls.getField("myCity");

        System.out.println("default field : " + field.get(dog));
        // default field : 서울 출력

        field.set(dog, "제주도");
        System.out.println("update field : " + field.get(dog));
        // update field : 제주도 출력
    }
}

 

Case 02 - setAccessible() 메서드를 사용하면 private로 선언한 필드에 접근

  • setAccessible() 메서드를 사용하면 private 인스턴스 변수나 메서드는 해당 클래스의 외부에서는 접근할 수 없는데 setAccessible(true)를 사용하면 문제없이 접근을 할 수 있습니다.
public class Example {
    public static void main(String[] args) throws Exception{
        Dog dog = new Dog();
        Class cls = Class.forName("test.Dog");
        Field field = cls.getDeclaredField("myName");
        field.setAccessible(true);

        System.out.println("default field : " + field.get(dog));
        // default field : 뽀삐 출력
        
        field.set(dog, "펩시");
        System.out.println("update field : " + field.get(dog));
        // update field : 펩시 출력
    }
}

 

리플렉션을 이용한 동적 메서드 호출 예제
  • Hello 클래스의 callA() 메서드와 callB() 메서드를 호출하는 부분만 다를 뿐 나머지는 동일한 코드 구조로 아루어져 있다고 생각을 하고 예제를 진행하겠습니다.
  • 아래처럼 호출하는 메서드만 다를 뿐  나머지는 코드 중복이 발생하고 있습니다.
@Slf4j
public class ReflectionTest {

    @Test
    void reflection() {
        Hello target = new Hello();

        // 공통 로직 1 시작
        log.info("start");
        String result1 = target.callA();
        log.info("result1={}", result1);
        // 공통 로직 1 종료

        // 공통 로직 2 시작
        log.info("start");
        String result2 = target.callB();
        log.info("result2={}", result2);
        // 공통 로직 2 종료
    }


    @Slf4j
    static class Hello {
        public String callA(){
            log.info("CallA");
            return "A";
        }

        public String callB(){
            log.info("CallB");
            return "B";
        }
    }
}

 

리플렉션을 이용한 동적 메서드로 변경
  • getMethod() 메서드를 활용하여 호출하고자하는 메서드의 이름을 인자값으로 넘기면서 처리를 할 수 있습니다.
@Slf4j
public class ReflectionTest {


    @Test
    void reflection() throws Exception {
        // 클래스 정보 획득
        Class classHello = Class.forName("jdkdynamic.ReflectionTest$Hello");

        Hello target = new Hello();

        // callA 메서드 정보
        Method methodCallA = classHello.getMethod("callA");
        dynamicCall(methodCallA, target);

        // callB 메서드 정보
        Method methodCallB = classHello.getMethod("callB");
        dynamicCall(methodCallB, target);
    }

    // 동일한 메서드 처리
    private void dynamicCall(Method method, Hello target) throws Exception {
        log.info("start");
        Object result = method.invoke(target);
        log.info("result={}", result);
    }

    @Slf4j
    static class Hello {
        public String callA(){
            log.info("CallA");
            return "A";
        }

        public String callB(){
            log.info("CallB");
            return "B";
        }
    }
}

 

주의 사항
  • Field.setAccessible() 메서드를 사용하면 외부에서 접근할 수 없는 private 멤버 변수에도 접근할 수 있으니 주의가 필요합니다.
  • 컴파일 에러가 아닌 런타임시에 에러가 발생하기 때문에 상당한 주의가 필요하며 되도록 사용을 하지 않는것이 좋습니다.
728x90
반응형