Java Reflection

Java Reflection 정의

리플렉션(Reflection)이란 객체를 통해 클래스의 정보를 분석해 내는 프로그램 기법을 말한다. 투영, 반사 라는 사전적인 의미를 지니고 있다.

자바의 리플렉션은 클래스, 인터페이스, 메소드들을 찾을 수 있고, 객체를 생성하거나 변수를 변경할 수 있고 메소드를 호출할 수도 있다. Reflection은 Java 표준 API이다.

Reflection은 다음과 같은 정보를 가져올 수 있다. 이 정보를 가져와서 객체를 생성하거나 메소드를 호출하거나 변수의 값을 변경할 수 있다.

Class Pageage
Class java.lang
Constructor java.lang.reflect
Method java.lang.reflect
Field java.lang.reflect

리플렉션을 안다는 건

Spring과 같은 프레임워크는 내부 처리에서 많은 리플렉션를 이용한다. 그래서 리플렉션을 알면 다음과 같은 이점이 있다.

  • 프레임워크 소스 코드를 읽고 이해할 수 있다!
  • 소스 코드를 읽지 않아도 프레임워크 내부의 처리를 이미지할 수 있게 된다!
  • 스스로 프레임워크를 만들 수 있게 된다!
    • 최근의 Java에서는 기존의 OSS(Open Source Software) 프레임워크를 사용하는 경우가 많기에 그다지하지 만드는건 드물 것이다.

꼭, 리플렉션을 알고 초급에서 중급 Java 엔지니어로 스텝 업합시다!

보통은 비즈니스 로직등에서 리플렉션을 사용하는 것은 추천하지 않으므로 주의해야 한다(이유는 뒤에서 설명한다).

리플렉션의 기본

Class 클래스 가져오기

클래스를 가져오려면 Class 클래스를 사용한다. Class 클래스를 사용하는 방법은 다음과 같다.

방법 1

Class<클래스명> 오브젝트명 = 클래스명.class;

방법 2

클래스명 오브젝트명1 = new 클래스명();
Class<? extends 클래스명> 오브젝트명2 = 오브젝트명1.getClass();

방법 3

Class<?> 오브젝트명1 = Class.forName("클래스명");

foName 메서드를 사용하는 경우는 ClassNotFoundException의 예외 처리를 해야 한다.

덧붙여 리플렉션에 관련하는 예외의 공통 클래스 ReflectiveOperationException로 처리하는 것도 가능하다.

인스턴스 생성

인스턴스를 생성하는 방법은 다음과 같다.

// Java 9 이전
Class<클래스명> 오브젝트명1 = 클래스명.class;
Object 오브젝트명2 = 오브젝트명1.newInstance(); // @Deprecated(since="9")

// Java 9 이후
Class<클래스명> 오브젝트명1 = 클래스명.class;
Object 오브젝트명2 = 오브젝트명1.getDeclaredConstructor().newInstance()

이는 아래에 해당한다.

클래스명 오브젝트명2 = new 클래스명();

메서드의 가져오기와 실행

메서드를 가져오려면 Method 클래스의 객체에 getMethod 메서드의 반환값을 저장한다.

Method 오브젝트명 = "Class 클래스의 오브젝트명".getMethod("메서드명", "인수1의 유형명.class", "인수2의 유형명.class", ...);

getMethod 메서드의 인수2 이후로는 Class 클래스의 가변 인수이다.

메서드를 실행하려면 invoke메서드를 실행한다.

"Method 클래스의 객체".invoke("생성된 인스턴스", "인수1", "인수2", ...)

invoke 메서드의 인수2 이후는 가변 인수가 된다.

인스턴스 생성과 메서드의 가져오기와 실행 예제

위에 내용을 예제 프로그램으로 알아보자.

아래 코드에는 단순한 set, get 메소드가 있다.

package com.devkuma.basic.reflection.ex1;

public class Foo {
    private String str;

    public void setStr(String str) {
        this.str = str;
    }

    public String getStr() {
        return str;
    }
}

아래 코드는 Foo의 오브젝트를 생성하고, setStr, getStr 메소드를 실행한다.

package com.devkuma.basic.reflection.ex1;

import java.lang.reflect.Method;

public class Main {

    public static void main(String[] args) {
        try {
            // 클래스 가져오기
            Class<?> fooClazz = Class.forName("com.devkuma.basic.reflection.ex1.Foo");
            // 인스턴스 생성
            Object myObj = fooClazz.getDeclaredConstructor().newInstance();

            // 메서드(setStr) 가져오기
            Method setStrMethod = fooClazz.getMethod("setStr", String.class);
            // 메서드(setStr) 실행
            setStrMethod.invoke(myObj, "test");

            // 메서드(getStr) 가져오기
            Method getStrMethod = fooClazz.getMethod("getStr");
            // 메서드(getStr) 실행
            System.out.println(getStrMethod.invoke(myObj));

        } catch (ReflectiveOperationException e) {
            e.printStackTrace();
        }
    }
}

실행 결과:

test

위에 예제에서는 클래스명이나 메소드명의 문자열을 forName 메서드, getMethod 메서드, invoke 메서드등의 메소드의 인수로 지정해 실행하고 있다.

private 필드 참조 및 변경

여기에서는 private 필드의 참조와 변경의 방법에 대해 살펴 보겠다.

아래의 예제 코드로 확인해 보자.

package com.devkuma.basic.reflection.ex1;

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

public class Main {

    public static void main(String[] args) {
        try {
            // 클래스 가져오기
            Class<?> fooClazz = Class.forName("com.devkuma.basic.reflection.ex1.Foo");
            // 인스턴스 생성
            Object myObj = fooClazz.getDeclaredConstructor().newInstance();

            // 메서드(setStr) 가져오기
            Method setStrMethod = fooClazz.getMethod("setStr", String.class);
            // 메서드(setStr) 실행
            setStrMethod.invoke(myObj, "test");

            // 메서드(getStr) 가져오기
            Method getStrMethod = fooClazz.getMethod("getStr");
            // 메서드(getStr) 실행
            System.out.println(getStrMethod.invoke(myObj));

            
            // 필드(str) 가져오기
            Field strField = fooClazz.getDeclaredField("str");
            strField.setAccessible(true);
            System.out.println(strField.get(myObj));

            // 필드(str) 변경
            strField.set(myObj, "test2");
            System.out.println(strField.get(myObj));

        } catch (ReflectiveOperationException e) {
            e.printStackTrace();
        }
    }
}

실행 결과:

test
test2

이 예제 코드에서는 먼저 setStr 메서드로 필드에 값을 저장한다.

그 값을 Field 클래스의 객체의 strField으로 불려와서 get 메소드를 사용해 가져오고 있다. 그러고, 그 값을 오브젝트 strField로부터 set 메소드를 사용해 변경하고 있다.

리플렉션의 위험

리플렉션을 사용할 때는 다음 사항에 유의해야 한다.

클래스 디자인을 파괴할 위험성

지금까지 말했듯이, 접근 제한자 private로 “캡슐화"한 값을 직접 취득, 변경할 수 있었다.
이와 같이 직접 조작하는 것을 가능하게 하므로써, 리플렉션에는 기존의 클래스 설계를 파괴해 버릴 위험성이 있다.

컴파일시 오류가 감지되지 않음

이전에는 리플렉션을 사용한 부분의 코드는 컴파일시의 에러 검출 대상외에서 에러를 검출시킬 수 없었다.
리플렉션 부분의 에러는 프로그램 실행 시에만 검출할 수 밖에 없었다.
그러나 Java7부터는 ReflectiveOperationException에서 리플렉션과 관련된 예외의 공통 클래스가 정의되었으므로 예외 처리를 할 수 있다.

정리

여기에서는 리플렉션에 대해 설명하였다.

리플렉션을 사용하면, 후에는 클래스명이나 메소드명 등 변수에 대입하는 값을 변경하는 것만으로 다른 클래스나 메소드를 실행할 수가 있다. 프로그램을 테스트하는 경우 등에서 사용하면 편리하다.