공부해봅시당

[JAVA] 자바의 Reflection 본문

STUDY/JAVA

[JAVA] 자바의 Reflection

tngus 2024. 3. 6. 20:07

 

0. 자바의 Class 클래스 (java.lang.Class)

자바 프로그래밍을 할때 우리는 보통 변수나 클래스를 직접 선언하고 만들어 사용해 왔다.

 

그런데 어떤 경우에는 애플리케이션 실행 중에서 클래스를 동적으로 불러와 다루어야 할 경우가 생긴다.

즉, 코드를 실행하기 전 컴파일 단에서 개발자가 직접 폴더를 뒤져가며 클래스 정의문을 찾아 클래스 정보를 얻는 것이 아닌,

코드 상에서 호출 로직을 통해 클래스 정보를 얻어와 다룸으로써

런타임 단에서 다이나믹하게 클래스를 핸들링 하는 것이다.

 

이때 사용되는 것이 바로 Class 클래스 객체이다.
 
Class 클래스는 java.lang.Class 패키지에 별도로 존재하는 독립형 클래스로서, 

자신이 속한 클래스의 모든 멤버 정보를 담고 있기 때문에 

런타임 환경에서 동적으로 저장된 클래스나 인터페이스 정보를 가져오는데 사용된다. 

 

여기서 오해하지 말아야 할 것이 클래스 자료형을 말하는게 아니라 클래스 이름이 "Class" 인 클래스를 말하는 것이다. 

 

자바의 모든 클래스와 인터페이스는 컴파일 후 .java → .class 파일로 변환된다. 

 

이 .class 파일에는 멤버변수, 메서드, 생성자 등 객체의 정보들이 들어 있는데, 

JVM의 클래스 로더(ClassLoader)에 의해서 클래스 파일이 메모리에 올라갈 때, 

Class 클래스는 이 .class 파일의 클래스 정보들을 가져와 힙 영역에 자동으로 객체화가 되게 된다. 

 

그래서 따로 new 인스턴스화 없이 바로 가져와 사용하면 된다.

 

JVM의 클래스 로더(class loader)는 실행 시에 필요한 클래스를 동적으로 메모리에 로드하는 역할을 한다.

먼저 기존에 생성된 클래스 객체가 메모리에 존재하는지 확인하고 있으면 객체의 참조를 반환하고,
없으면 classpath에 지정된 경로를 따라서 클래스 파일을 찾아 해당 클래스 파일을 읽어서 Class 객체로 변환한다.

만일 못 찾으면 우리가 익히아는 ClassNotFoundException 예외를 띄우게 된다.

 

 

1. 리플렉션(Reflection)이란?

Class 객체를 이용하면 클래스에 대한 모든 정보(클래스의 정의된 멤버의 이름이나 개수 등)를 

런타임 단에서 코드 로직으로 얻을 수 있다.


클래스 정보들을 실행부에서 얻을 수 있는 점은 꽤나 매력적인데, 

이러한 정보들을 이용하여 오로지 Class 객체만으로 본 클래스를 인스턴스화 할 수 있고, 

메서드를 호출 할 수 있는 등 보다 동적인 코드를 작성할 수 있게 된다. 

 

이처럼 구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(메소드, 타입, 변수, ...)에 접근할 수 있게 해주는 자바 기법을 Reflection API 라고 부른다.

컴파일 시간이 아닌 실행 시간에 동적으로 특정 클래스의 정보를 추출할 수 있는 프로그래밍 기법이라 할 수 있다.

 

자바 리플렉션(Reflection - 사전적 의미 : 거울 등에 비친, 반사)은 객체를 통해 클래스의 정보를 분석하여 

런타임에 클래스의 동작을 검사하거나 조작하는 프로그램 기법이다. 

 

클래스 파일의 위치나 이름만 있다면 해당 클래스의 정보를 얻어내고, 

객체를 생성하는 것 또한 가능하게 해주어 유연한 프로그래밍을 가능케 해준다.

2. 언제 사용할까?

동적으로 클래스를 사용해야할 때 사용한다.

 

다시 말해, 작성 시점에는 어떠한 클래스를 사용해야할지 모르지만 런타임 시점에서 가져와 실행해야하는 경우 필요하다.
프레임워크나 IDE에서 이런 동적 바인딩을 이용한 기능을 제공한다.

 

2-1. 리플렉션 사용 예시

1) IntelliJ의 자동완성 기능

IntelliJ IDEA와 같은 통합 개발 환경(IDE)는 리플렉션을 사용하여 개발자가 코드를 작성할 때 클래스의 메서드, 필드 등을 동적으로 조회하고, 해당 정보를 바탕으로 자동완성 기능을 제공한다.

 

예를 들어, 개발자가 특정 객체를 타이핑하고 점(.)을 입력했을 때, 해당 객체의 클래스 타입을 실시간으로 분석하여 사용 가능한 메서드나 속성 목록을 자동으로 제시한다.

이 과정에서 리플렉션 API는 클래스의 메타데이터를 조회하여 현재 컨텍스트에서 유효한 코드 완성 옵션을 도출한다.


2) 스프링 어노테이션

스프링 프레임워크는 리플렉션을 사용하여 어노테이션 기반의 설정을 처리한다.

스프링에서는 @Component, @Service, @Autowired 등 다양한 어노테이션을 통해 빈(Bean)의 등록, 의존성 주입 등의 작업을 수행한다.

리플렉션 API를 사용하여 런타임에 클래스 파일을 분석하고,

해당 클래스에 적용된 어노테이션 정보를 기반으로 스프링 컨테이너가 빈을 자동으로 등록하거나 의존성을 주입한다.

예를 들어, @Autowired 어노테이션이 붙은 필드가 있다면, 

스프링은 리플렉션을 사용하여 해당 필드의 타입을 조회하고, 

스프링 컨테이너에서 관리되는 해당 타입의 빈을 찾아 자동으로 주입한다.

 

이 과정에서 리플렉션은 필드, 메서드, 생성자 등의 정보를 동적으로 액세스하여 의존성 주입을 수행하는 데 필수적이다.

 

2-2. 리플렉션을 사용하여 가져올 수 있는 정보

- Class
- Constructor
- Method
- Field

3. 예시 코드

이제 이 리플렉션을 사용하여 정보를 가져오는 방법을 예시를 통해 알아보자.
예시 작성을 위해 Parent, Child, Test.java 클래스를 만들어주자.
 

Parent.java

package reflectiontest;

public class Parent {
    private String str1 = "1";
    public String str2 = "2";

    public Parent() {
    }

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

    public void method2(int n) {
        System.out.println("method2: " + n);
    }

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

 

 

Child.java

package reflectiontest;

public class Child extends Parent {
    public String cstr1 = "1";
    private String cstr2 = "2";

    public Child() {
    }

    private Child(String str) {
        cstr1 = str;
    }

    public int method4(int n) {
        System.out.println("method4: " + n);
        return n;
    }

    private int method5(int n) {
        System.out.println("method5: " + n);
        return n;
    }
}

 

Test.java

package reflectiontest;

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

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

    }
}

 

아래의 값을 넣어 출력하는 로직은 모두 Test.java안에서 이뤄진 것이다.

 

 

3-1. Class 찾기

Class 객체는 클래스 또는 인터페이스를 가리킨다. (java.lang.Class)
Class 객체는 여러 메서드를 제공한다. 그 중 getName()은 클래스의 이름을 리턴해준다.

Class clazz = Child.class;
System.out.println("Class name: " + clazz.getName());

//출력
Class name: test.Child

위와 같이 클래스 명을 가져올 수 있다.

 

그런데 만약, 클래스의 이름을 몰랐다고 했을 때는 어떻게 클래스 명을 가져올까?

아래의 내용을 보자.

Class clazz2 = Class.forName("test.Child");
System.out.println("Class name: " + clazz2.getName());

//출력
Class name: test.Child

위와 같이 Class.forName()에 클래스 이름을 인자로 전달하여 클래스 정보를 가져올 수 있다. 

주의할점은 패키지 네임이 포함된 클래스 명을 써줘야한다.

 

3-2. Constructor 찾기

Class clazz = Class.forName("test.Child");
Constructor constructor = clazz.getDeclaredConstructor();
System.out.println("Constructor: " + constructor.getName());

//출력
Constructor: test.Child


위와 같이 getDeclaredConstructor을 활용하여 인자 없는 생성자를 가져올 수 있다. 

그렇다면, 인자가 있는 오버로딩 생성자를 가져오는 방법은 무엇일까? 

 

간단하다. 타입에 일치하는 인자를 넣어주면 된다.

Class clazz = Class.forName("test.Child");
Constructor constructor2 = clazz.getDeclaredConstructor(String.class);
System.out.println("Constructor(String): " + constructor2.getName());

//출력
Constructor(String): test.Child

 

위 두개는 인자가 없거나, 있거나 한 생성자를 가져오는 방법이고 모든 생성자를 가져오는 방법도 있다.

Class clazz = Class.forName("test.Child");
Constructor constructors[] = clazz.getDeclaredConstructors();
for (Constructor cons : constructors) {
    System.out.println("Get constructors in Child: " + cons);
}

//출력
Get constructors in Child: private test.Child(java.lang.String)
Get constructors in Child: public test.Child()

 

위와 같이 모든 생성자를 가져올 수 있고, public 생성자만 가져오는 방법도 있다.

Class clazz = Class.forName("test.Child");
Constructor constructors2[] = clazz.getConstructors();
for (Constructor cons : constructors2) {
    System.out.println("Get public constructors in Child: " + cons);
}

//출력
Get public constructors in both Parent and Child: public test.Child()

 

 

3-3. Method 찾기

Class clazz = Class.forName("test.Child");
Method method1 = clazz.getDeclaredMethod("method4", int.class);
System.out.println("Find out method4 method in Child: " + method1);

//출력
Find out method4 method in Child: public int test.Child.method4(int)

 

위와 같이 메서드를 찾을 수 있다. 

만약 인자가 없거나 있는 메서드를 찾고 싶다면 아래와 같이 진행하면 된다.

//인자가 없는 경우 null을 넣어준다.
Class clazz = Class.forName("test.Child");
Method method1 = clazz.getDeclaredMethod("method4", null);

// 인자가 두 개 이상이 경우
Class clazz = Class.forName("test.Child");
Class partypes[] = new Class[1];
partypes[0] = int.class;
Method method = clazz.getDeclaredMethod("method4", partypes);

//public 메서드만
Class clazz = Class.forName("test.Child");
Method methods2[] = clazz.getMethods();
for (Method method : methods2) {
    System.out.println("Get public methods in both Parent and Child: " + method);
}

위와 같이 인자 별, 또는 접근제어자 public만 추출해 낼 수 있다. 

마지막으로 Field에 대해 알아보자.

3-4. Field 변경

Class clazz = Class.forName("test.Child");
Field field = clazz.getDeclaredField("cstr1");
System.out.println("Find out cstr1 field in Child: " + field);

//출력
Find out cstr1 field in Child: public java.lang.String test.Child.cstr1

위의 다른 예시들과 동일하게 사용하면 된다. 

getDeclaredField()를 사용하면 되며, 만약. public 필드만 찾고싶다면 getFields()를 사용하면 된다.

Class clazz = Class.forName("test.Child");
Field fields2[] = clazz.getFields();
for (Field field : fields2) {
    System.out.println("Get public fields in both Parent and Child: " + field);
}

//출력
Get public fields in both Parent and Child: public java.lang.String test.Child.cstr1
Get public fields in both Parent and Child: public java.lang.String test.Parent.str2

이상 리플렉션의 기능 중 대표적으로 네 가지에 대해 알아보았고, 

추가로 Static 메서드 호출과 필드를 변경하는 방법에 대해서도 알아보자.

3-5. Static 메서드 호출 또는 필드 변경

아래의 예시와 같이 클래스를 생성해준다.
 

StaticExample.java

package test;

public class StaticExample {
    public static String EXAMPLE = "Example";

    public static int getSquare(int num) {
        System.out.println("Get square: " + num * num);
        return num * num;
    }
}

 

static 메서드 정보를 가져올 땐 invoke()라는 키워드를 사용하여 객체를 전달하는 인자에 대해 null을 넣어주면 static 메서드가 호출된다.

Class clazz = Class.forName("test.StaticExample");
Method method = clazz.getDeclaredMethod("getSquare", int.class);
method.invoke(null, 10);

//출력
Get square: 100

 

static 필드 정보를 가져오는 방법도 위와 동일한데, 

신경써야 할 점은 set() 또는 get() 함수를 사용할 때 객체로 전달되는 인자에 대해 null을 넣어줘야 된다.

Class clazz = Class.forName("test.StaticExample");
Field fld = clazz.getDeclaredField("EXAMPLE");
fld.set(null, "Hello, World");
System.out.println("StaticExample.EXAMPLE: " + fld.get(null));

//출력
StaticExample.EXAMPLE: Hello, World

 

 

 


참조

https://jeongkyun-it.tistory.com/225

 

[Java] 리플렉션 (Reflection)이란 무엇일까? (개념/ 예시)

서론 이번 포스팅에서 다룰 내용은 '리플렉션'이다. 최근 "리플렉션이 무엇인가요?" 라는 질문을 받았는데, 제대로 된 답변을 못한 것 같다. C# 개발을 할 때 분명 사용은 해보았지만 개념적으로

jeongkyun-it.tistory.com

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EB%88%84%EA%B5%AC%EB%82%98-%EC%89%BD%EA%B2%8C-%EB%B0%B0%EC%9A%B0%EB%8A%94-Reflection-API-%EC%82%AC%EC%9A%A9%EB%B2%95

 

☕ 누구나 쉽게 배우는 Reflection API 사용법

자바의 Class 클래스 (Java.lang.Class) 자바 프로그래밍을 할때 우리는 보통 변수나 클래스를 직접 선언하고 만들어 사용하여 왔다. 그런데 어떤 경우에는 애플리케이션 실행 중에서 클래스를 동적으

inpa.tistory.com