본문 바로가기
프로그래밍 언어/Java

Reflection API

by 조엘 2021. 6. 9.

안녕하세요조엘입니다! 🎉

 

오늘은 자바의 Reflection API에 대해 알아보는 시간을 가져볼게요! 💪💪

피드백 환영입니다! 댓글 달아주세요 :)

 

 

*** Reflection API? ***

Reflection API의 정의를 먼저 알아보도록 해요.

공식문서에서는 다음과 같이 Java Reflection API를 설명하는데요.

 

Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions. The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.

 

조금 해석 + 요약을 해보면 다음과 정리할 수 있을 것 같아요

> 리플렉션 API는 자바 코드를 통해 로드된 클래스의 필드/메서드/생성자를 찾을 수 있게 지원한다

> 또한 클래스의 접근 제한자와 상관없이 클래스의 필드/메서드/생성자를 사용할 수 있게 지원한다

 

여기서 로드된 클래스라고 함은, JVM의 클래스 로더에서 클래스 파일에 대한 로딩을 완료한 후

해당 클래스의 정보를 담은 Class 타입의 객체를 생성해 메모리의 힙 영역에 저장해 둔 것을 지칭해요

 

해당 내용은 이전 JVM의 포스팅에서 다뤘으니 참고해주세요

 (참고https://papimon.tistory.com/81)

 

또한 다른 선배 개발자 분들의 글들을 보면 다음과 같이 정의 내리는 것을 볼 수 있는데요

> 구체적인 클래스 타입을 알지 못해도 클래스의 메서드/타입/변수들에 접근할 수 있도록 해주는 자바 API

> 클래스의 정보를 분석해 내는 프로그램 기법

 

이를 기반으로 제 나름대로 Reflection API를 정의해보자면 다음과 같아요

 

"힙 영역에 로드된 Class 타입의 객체를 통해원하는 클래스의 인스턴스를 생성할 수 있게 지원하고,

인스턴스의 필드와 메서드를 접근 제어자와 상관없이 사용할 수 있도록 지원하는 자바 API"

 

 

*** 어떻게 쓸까? ***

사실 줄 글로 보면 조금 와닿지 않는 것 같아요. 정의 내린 리플렉션을 코드를 통해 이해해보도록 해요!

 

"힙 영역에 로드된 Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있게 지원하고, 인스턴스의 필드와 메서드를 접근 제어자와 상관없이 사용할 수 있도록 지원하는 자바 API"

 

우선 Heap 영역에 로드되어 있는 Class 타입의 객체를 가져와볼게요. 총 3가지 방법이 있어요. 

1. 클래스.class 로 가져오기

2. 인스턴스.getClass() 로 가져오기

3. Class.forName("클래스이름") 으로 가져오기

 

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        final Class<Test> classTypeTest = Test.class;
        System.out.println("classTypeTest = " + System.identityHashCode(classTypeTest));

        final Test test = new Test();
        final Class<? extends Test> getClassTest = test.getClass();
        System.out.println("getClassTest = " + System.identityHashCode(getClassTest));

        final Class<?> classForNameTest = Class.forName("Test");
        System.out.println("classForNameTest = " + System.identityHashCode(classForNameTest));
    }
}

>> 결과
classTypeTest = 460141958
getClassTest = 460141958
classForNameTest = 460141958

 

3가지 방법으로 가져온 Class 타입의 인스턴스는 모두 같은 것으로 확인할 수 있었는데요!

따라서 어떤 방식으로 Class 타입의 인스턴스를 가져오던 상관없을 것이에요.

 

"힙 영역에 로드된 Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있게 지원하고, 인스턴스의 필드와 메서드를 접근 제어자와 상관없이 사용할 수 있도록 지원하는 자바 API"

 

이번에는 Class 타입의 인스턴스를 통해, 해당 클래스의 인스턴스를 생성해보도록 할게요. 

Class 타입의 인스턴스에서 getConstructor() 메서드를 통해 Constructor 인스턴스를 가져와요.

가져온 Constructor 인스턴스에서 newInstance() 메서드를 호출해주면 우리가 원하는 클래스의 인스턴스가 생성돼요.

 

말이 어렵죠? 코드로 살펴볼게요. 🎯

 

public class Test {
    private String c = "c";
    protected String d = "d";
    public String e = "e";

    public Test() {
    }

    public Test(String c, String d, String e) {
        this.c = c;
        this.d = d;
        this.e = e;
    }
    
    @Override
    public String toString() {
        return "Test{" +
                "c='" + c + '\'' +
                ", d='" + d + '\'' +
                ", e='" + e + '\'' +
                '}';
    }
}

public class Main {
    public static void main(String[] args) {
        final Test test = new Test();
        final Class<? extends Test> getClassTest = test.getClass();

        //생성자를 가져와 보자
        Arrays.stream(getClassTest.getConstructors()).forEach(System.out::println);

        //생성자를 리플렉션으로 가져와서 객체 생성하기
        final Constructor<? extends Test> constructor = getClassTest.getConstructor(null);
        final Test test1 = constructor.newInstance();
        System.out.println("test1 = " + test1);

        final Constructor<? extends Test> stringConstructor = 
        		getClassTest.getConstructor(String.class, String.class, String.class);
        final Test test2 = stringConstructor.newInstance("hi1", "hi2", "hi3");
        System.out.println("test2 = " + test2);
    }
}

>> 결과
public Test()
public Test(java.lang.String,java.lang.String,java.lang.String)
test1 = Test{c='c', d='d', e='e'}
test2 = Test{c='hi1', d='hi2', e='hi3'}

 

우선 Class 타입의 인스턴스에서 .getConstructors() 를 통해 Test 클래스에 어떤 생성자들이 정의되어 있는지 가져올 수 있어요. 

개별적으로 생성자를 하나 가져오기 위해서는 .getConstructor() 메서드에 생성자에 필요한 파라미터들의 타입을 기술하여 가져올 수 있어요. 

이렇게 개별적으로 가져온 Constructor 클래스에 .newInstance() 메서드를 필요한 파라미터들을 채워 넣어 호출해주면 성공적으로 Test 클래스의 인스턴스가 생성됨을 알 수 있어요.  

 

"힙 영역에 로드된 Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있게 지원하고, 인스턴스의 필드와 메서드를 접근 제어자와 상관없이 사용할 수 있도록 지원하는 자바 API"

 

이제 인스턴스의 필드와 메서드를 리플렉션을 통해 사용해보도록 할게요. 코드 먼저 볼게요. 💪

 

public class Test {
    private String c = "c";
    protected String d = "d";
    public String e = "e";

    public Test() {
    }

    public Test(String c, String d, String e) {
        this.c = c;
        this.d = d;
        this.e = e;
    }
    
    public void func() {
        System.out.println("hello");
    }
    
    @Override
    public String toString() {
        return "Test{" +
                "c='" + c + '\'' +
                ", d='" + d + '\'' +
                ", e='" + e + '\'' +
                '}';
    }
}

public class Main {
    public static void main(String[] args) {
        final Test test = new Test();
        final Class<? extends Test> getClassTest = test.getClass();

        //인스턴스 필드 가져와보기
        final Field c = getClassTest.getDeclaredField("c");
        c.setAccessible(true);
        System.out.println(c.get(test));
        c.set(test, "CCCCCCCCCCCCCC ");
        System.out.println(c.get(test));

        //이번엔 메서드를 가져와보자
        final Method func = getClassTest.getDeclaredMethod("func");
        func.invoke(test);
    }
}

>> 결과
c
CCCCCCCCCCCCCC 
hello

 

위의 예시를 보면 .getDeclaredField("필드명")를 통해 클래스의 인스턴스 변수에 접근한 것을 알 수 있어요. 

하지만 예시의 Test 클래스의 인스턴스 변수 "c"는 접근 제한자가 private인데요. 

해당 필드에 대해 .setAccessible(true) 를 설정해주면 접근이 가능한 것을 알 수 있어요. 

또한 리플렉션을 통해 원하는 인스턴스에 대해 해당 필드 값을 설정해 줄 수 있는데요. 

Field.set(인스턴스, "원하는 값")과 같이 해당 필드 값을 원하는 값으로 설정해 줄 수 있어요. 

설정을 해줬으니, 가져올 수도 있겠죠?

Field.get(인스턴스)를 통해 해당 필드 값을 가져올 수 있어요. 

 

메서드도 성공적으로 가져와 실행한 것을 볼 수 있는데요. 

.getDeclaredMethod("메서드명")을 통해 클래스의 메서드에 접근한 것을 알 수 있어요. 

원하는 인스턴스에서 해당 메서드를 호출하게 할 수 있는데요. 

Method.invoke(인스턴스)를 통해 메서드를 호출하게 되어요. 

 

간략하게 제가 내린 리플렉션 정의에 기반하여 활용법을 알아봤어요. 

사실 여기서 소개한 리플렉션 API 보다 더 많은 기능들이 있어요. 

해당 기능들을 소개한 글을 아래 참고에 적어둘게요. 

 

 

*** 왜 필요할까? ***

리플렉션 API를 통해 클래스 정보에 접근하여 클래스를 원하는 대로 조작할 수 있어요. 

심지어 private으로 선언해둔 필드와 메서드까지 접근이 가능해요. 

객체 지향 설계에서 그토록 강조한 캡슐화, 은닉성 등이 전부 다 깨져버리는데 왜 필요한 걸까요?

 

한 마디로 요약하자면, 런타임에 비로소 객체가 생성되어야 할 필요성이 있기 때문이에요. 

 

사실 리플렉션 기술은 콘솔 기반의 작은 프로그램을 만들 때에는 필요 없어요. 

개발자가 충분히 컴파일 타임에 프로그램에서 사용될 객체와 의존 관계를 모두 알고 때문이죠. 

 

하지만 프레임워크 안에서 개발할 때에는 얘기가 조금 달라요. 

Spring의 Bean Factory를 예로 들어볼게요. 

Spring에서 개발할 때 @Controller, @Service, @Repository 등의 어노테이션만 붙이면,

Bean Factory에서 알아서 해당 어노테이션이 붙은 클래스를 생성하고 관리해줘요. 

이게 어떻게 가능할까요? 개발자들은 Bean Factory에 해당 클래스들을 알려준 적이 없는데요?

 

리플렉션 기술이 여기서 사용이 되어요.

컴파일 이후 런타임에 해당 어노테이션이 붙은 클래스들을 탐색하고 발견한다면,

리플렉션을 통해 해당 클래스의 인스턴스를 생성하고 필요한 필드를 주입하여 Bean Factory에 저장해둔 것이죠. 

 

어노테이션에 대한 얘기가 자연스럽게 나왔는데, 어노테이션에 대한 포스팅도 조만간 올릴게요~ 🖌🖌

 

읽어주셔서 감사합니다 😊😊 

 

 

참고

https://www.inflearn.com/course/the-java-code-manipulation

https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/index.html

https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/package-summary.html#package.description

https://brunch.co.kr/@kd4/8

https://gyrfalcon.tistory.com/entry/Java-Reflection

https://dublin-java.tistory.com/53

https://www.baeldung.com/java-reflection

 

 

 

반응형

'프로그래밍 언어 > Java' 카테고리의 다른 글

저는 GC가 처음이라니까요?  (0) 2021.10.02
저는 어노테이션이 처음이라니까요?  (2) 2021.06.11
저는 JVM이 처음이라니까요?  (6) 2021.05.28
HashMap/HashSet의 원리  (4) 2021.03.13
Java Code Conventions  (0) 2020.11.24

댓글