Java Reflection 기법을 활용하여 동적으로 코드 조작하기
Java Reflection
리플렉션(Reflection)은 프로그램이 실행 중에 자신의 구조를 반영하여 코드를 변경하거나 조작할 수 있게 하는 컴퓨터 과학의 중요한 개념입니다. 이 기능은 메타프로그래밍의 한 형태로, 프로그램의 재사용성, 동적 로딩, 디버깅 도구 등에 널리 활용됩니다. 또한, 프레임워크, 라이브러리, 플러그인 시스템 개발에 필수적입니다.
Java에서는 이를 지원하기 위해 java.lang.reflect
패키지를 제공하며, 개발자는 이를 통해 런타임에 클래스의 메타데이터를 조회하거나 조작할 수 있습니다. 리플렉션의 강력한 기능을 효과적으로 사용하기 위해서는 주의 깊은 사용이 요구되며, 잘못 사용될 경우 성능 저하나 보안 문제를 일으킬 수 있습니다.
Prerequisites
리플렉션을 살펴보기 위해, 다음과 같이 정의된 사용자 클래스를 활용하고 있습니다:
public class Cat {
private String name;
private int age;
private Cat() {
}
private Cat(String name, int age) {
this.name = name;
this.age = age;
}
private String getName() {
return name;
}
private int getAge() {
return age;
}
@Override
public String toString() {
return "Cat: {name='" + name + "', age=" + age + "}";
}
}
Fundamentals of Java Reflection API
Java 리플렉션 API는 컴파일 시에 저장된 클래스 파일의 메타데이터에 직접적으로 접근합니다. 이 메타데이터에는 클래스의 필드, 메서드, 생성자, 어노테이션 등의 정보가 포함되어 있습니다.
- 클래스 로더: 모든 Java 클래스는
.class
파일에서 읽힌 바이너리 데이터를 기반으로 JVM(Java Virtual Machine)에 의해 로드됩니다. 이 과정에서 클래스에 대한 데이터 구조가 JVM 내 메모리에 생성됩니다. - 메타데이터 접근: 리플렉션 API는 JVM에 저장된 클래스의 메타데이터를 사용하여 클래스의 구조를 파악합니다. 예를 들어,
Class
객체의getDeclaredFields()
메서드는 클래스에서 선언된 모든 필드를 조회합니다. - 필드와 메서드 조작: 리플렉션을 통해 개발자는 런타임에 필드의 값을 읽거나 변경하고, 메서드를 실행할 수 있습니다. 이 기능은
Field
와Method
클래스의 메서드(get()
,set()
,invoke()
)를 통해 제공됩니다.
Java Reflection vs. CGLIB
Java 리플렉션 API와 CGLIB(Code Generation Library)는 유사한 목적으로 사용되지만, 다른 접근 방식을 사용합니다. CGLIB는 런타임에 바이트 코드를 조작하여 새로운 클래스를 생성하거나 기존 클래스를 수정합니다. 이는 주로 프록시 객체를 생성하거나, 메서드를 오버라이딩하는 등의 목적으로 사용됩니다. 반면, 리플렉션은 이미 컴파일된 클래스의 메타데이터를 활용하며, 이 두 기술은 사용 목적과 요구 사항에 따라 선택됩니다.
Spring Framework에서는 JdkDynamicProxy
와 CGLIB를 동적 프록시 생성에 활용합니다:
- JdkDynamicProxy: 인터페이스가 있는 클래스에 대해서는 Java의 기본 리플렉션 API인
Proxy
클래스와InvocationHandler
인터페이스를 사용하여 동적 프록시를 생성합니다. 이 방법은 인터페이스를 구현하는 클래스에 대해 프록시를 쉽게 생성할 수 있게 해주며, 메모리 사용량과 성능 측면에서 효율적입니다. - CGLIB: 인터페이스가 없는 클래스에 대해서는 CGLIB를 사용하여 동적 프록시를 생성합니다. CGLIB는 바이트코드 조작을 통해 클래스의 인스턴스 자체를 프록시하며, 인터페이스가 필요하지 않습니다. 이 방식은 인터페이스를 사용하지 않는 클래스를 대상으로 할 때 유용하며, 메서드 호출의 인터셉션 등 복잡한 상황에서 강력한 기능을 제공합니다.
Spring은 이러한 기술들을 통해 개발자들이 프록시 생성에 있어서 최적의 전략을 선정하도록 지원합니다. 이런 접근 방식은 AOP와 같이 복잡한 프로그래밍 요구를 효과적으로 관리하고 해결하는 데 큰 역할을 합니다.
Class
Java 리플렉션 API를 이용하면 주어진 클래스 또는 객체에 대한 상세한 내부 정보를 얻을 수 있습니다. 그 정보로는 클래스의 이름, 슈퍼 클래스(Superclass), 인터페이스(Interfaces), 필드(Fields), 생성자(Constructors), 메서드(Methods) 등이 포함됩니다. 이런 정보들을 클래스의 메타데이터라고 합니다.
리플렉션 API를 사용하는 첫 번째 단계는 클래스에 대한 Class<?>
객체를 얻는 것입니다. 클래스 타입을 표현하는 Class<?>
객체를 얻는 방법은 다음 세 가지가 있습니다.
Class.forName(String)
Class.forName(String)
메서드는 런타임 시점에서 특정 클래스를 동적으로 로드하는 용도로 사용됩니다. 이 메서드는 클래스의 완전한 이름을 문자열로 받아, 해당 클래스를 로드하고 이에 해당하는 Class<?>
객체를 반환합니다.
Class<?> cls = Class.forName("java.lang.String");
- 이 메서드는 해당 클래스의 정규화된 이름을 매개변수로 받아야 합니다. 클래스 정규화된 이름이란, 패키지 이름까지 포함한 전체 경로를 의미합니다.
- 이 방식의 주요 장점은 런타임 동안에 프로그램 코드에 없는 추가적인 클래스를 동적으로 로드할 수 있다는 것입니다. 이는 유연성을 높이며 동적 프로그래밍 상황에서 유익하게 활용할 수 있습니다.
- 그러나, 주의해야 할 점은
Class.forName(String)
메서드를 호출할 때 잘못된 클래스 이름을 전달하게 되면ClassNotFoundException
이 발생합니다. 따라서, 이 방식을 사용할 때는 적절한 예외 처리가 필요합니다. Class.forName(String)
메서드를 사용하여 클래스를 로드할 때, 해당 클래스의 static 초기화 블록이 실행되기 때문에, 초기화 시 수행해야 하는 작업이 있다면 이 메서드를 사용하는 것이 좋습니다.
ClassName.class
ClassName.class
구문은 컴파일 시점에 클래스의 존재를 정의하고 사용하는 상황에 적합합니다. 이는 컴파일 시점에 알려진 정적 클래스만을 대상으로 하며, 해당 클래스의 Class<?>
객체를 얻는 데 사용됩니다.
클래스 리터럴(Class Literal)을 이용하여 클래스의 Class<?>
객체를 얻습니다. 이는 컴파일 시점에 클래스의 존재를 보장하고, 로드된 클래스의 Class<?>
객체를 반환합니다.
Class<?> cls = String.class;
- 이 방식은 참조하는 클래스가 존재하지 않는 경우 컴파일 시점에 에러를 발생시킵니다. 이로 인해 코드에 포함된 모든 클래스 참조가 정확한지를 미리 확인할 수 있습니다.
ClassName.class
구문은 컴파일 시점에 클래스를 로드하므로, 런타임에 동적으로 로드되는 클래스를 처리하는데 제한적입니다. 따라서, 런타임에 동적으로 클래스를 로드해야 하는 상황에서는Class.forName(String)
과 같은 런타임 로드 방식을 사용하는 것이 더 적합합니다.ClassName.class
구문으로 클래스를 로드하게 되면, 해당 클래스의 static 초기화 블록이 실행됩니다. 이 점은 앞서 설명한Class.forName(String)
방식과 동일하며, 클래스 변수 초기화 등 static 초기화 블록에서 수행되는 추가 작업들의 복잡성을 감안할 필요가 있습니다.
Object.getClass()
Object.getClass()
메서드는 이미 인스턴스화된 객체에서 사용됩니다. 이 방식을 사용하면 해당 객체의 런타임 시점의 클래스를 나타내는 Class<?>
객체를 반환할 수 있습니다. 이는 객체의 실제 타입을 식별하는 데 적합하며, 해당 타입의 상속 관계나 구현 인터페이스와 같은 추가 정보를 얻을 수 있습니다.
String str = "Hello, World!";
Class<?> cls = str.getClass();
Object.getClass()
메서드는 메모리에 이미 로드된 객체의 런타임 타입 정보를 제공합니다. 따라서, 이 방식은 형변환 확인, 동적 프로그래밍, 정적 제너릭 타입 정보의 식별 등에 유용하게 사용될 수 있습니다.- 그러나 한 가지 주의할 점은
Object.getClass()
메서드를 호출하기 위해서는 해당 클래스의 객체가 이미 존재해야 합니다. 이로 인해, 아직 인스턴스화되지 않은 클래스 또는 런타임 시점에 동적으로 로드할 클래스에 이 방법을 사용하는 것은 적절하지 않습니다. - 이미 인스턴스화된 객체에 대해서만
Class<?>
객체를 얻을 수 있기 때문에,Object.getClass()
메서드를 호출할 때 해당 클래스의 static 초기화 블록은 다시 실행되지 않습니다. 이는 추가적인 사이드 이펙트 없이Class<?>
객체를 얻을 수 있는 장점으로 작용합니다.
Static Initialization Block
Java에서 static 초기화 블록은 클래스가 로드될 때 한 번만 실행됩니다. 이 블록은 특정 클래스의 static 변수를 초기화하는 데 주로 사용됩니다. 이 초기화는 클래스 로더가 클래스를 처음으로 로드할 때 수행되는데, Java Virtual Machine(JVM)에서 클래스 로더가 이 작업을 보장합니다. static 초기화 블록에서는 일반적으로 클래스 변수들을 초기화하는 작업이 이루어지지만, 경우에 따라서는 파일 시스템에 접근하거나 네트워크 연결을 설정하는 등의 복잡한 동작을 수행하기도 합니다. 이러한 동작들은 예외 상황을 유발할 수 있거나, 예상치 못한 성능 문제를 초래할 수 있습니다. 또한 static 초기화 블록이 실행되는 시점을 정확히 인지하지 못하는 경우, 초기화 블록 내의 코드가 예상보다 더 일찍 혹은 늦게 실행될 수 있습니다. 이는 프로그램의 동작에 미묘한 영향을 미칠 수 있습니다. 따라서 static 초기화 블록의 실행 시점에 대한 정확한 이해와 그에 따른 알맞은 코드 구성이 중요합니다.
Accessibility
Java에서는 클래스의 생성자, 필드 또는 메서드에 대한 접근 제어를 위해 다음과 같은 네 가지 접근 제어자를 제공합니다:
public
: 어느 클래스에서나 접근이 가능합니다.protected
: 같은 패키지 내의 클래스 또는 해당 클래스를 상속받은 외부 패키지의 클래스에서 접근이 가능합니다.default
: 접근 제어자를 별도로 지정하지 않았을 경우, 해당 클래스는 동일한 패키지 내의 다른 클래스에서만 접근이 가능하게 됩니다. 이러한 특징 때문에 Package Private이라고도 불립니다.private
: 같은 클래스 내에서만 접근이 가능합니다.
이는 객체 지향 프로그래밍의 핵심 원칙 중 하나인 캡슐화를 구현하는 중요한 도구입니다. 그러나, 경우에 따라서는 접근 제한자가 설정된 범위를 벗어나 필드나 메서드에 접근해야 할 때가 있습니다.
이러한 상황에 대응하기 위해 Java의 리플렉션 API에서는 setAccessible()
메서드를 제공합니다. 이 메서드를 사용하면 접근 제어자에 의해 제한된 클래스 멤버에 대해 접근이 가능해집니다.
다음은 setAccessible()
메서드를 사용해 private
필드에 접근 권한을 부여하는 방법을 보여주는 코드입니다:
public class JavaReflectionAccessibility {
public static void main(String[] args) {
try {
Cat cat = new Cat("Ongs", 10);
Field fieldName = Cat.class.getDeclaredField("name");
Field fieldAge = Cat.class.getDeclaredField("age");
fieldName.setAccessible(true);
fieldAge.setAccessible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Fields
리플렉션 API를 이용하면, 클래스의 필드를 직접 탐색하고 해당 정보를 런타임에 추출할 수 있습니다. 이 추출 정보는 필드의 이름, 타입, 접근 제어자 등을 포함하며, 이를 통해 객체의 상태를 동적으로 확인하거나 변경하는 것이 가능합니다.
Accessing Fields
클래스의 필드 정보를 담고 있는 Field
객체를 얻기 위한 주요 메서드는 다음과 같습니다:
getField()
: 이 메서드는 지정된 클래스와 상속받은 클래스에서public
으로 선언된 특정 필드의 정보를 조회하여Field
객체로 반환합니다.getFields()
: 이 메서드는 지정된 클래스와 상속받은 클래스에서 모든public
접근 제어자를 가진 필드를 검색하고List<Field>
형태로 반환합니다.getDeclaredField()
: 이 메서드는 지정된 클래스에 선언된 특정 필드의 정보를Field
객체로 반환합니다. 상속받은 필드는 포함하지 않으며, 접근 제어자에 상관없이 해당 클래스 내에서 선언된 모든 필드에 접근할 수 있습니다.getDeclaredFields()
: 이 메서드는 지정된 클래스에 선언된 모든 필드의 정보를List<Field>
형태로 반환합니다. 상속받은 필드는 포함하지 않으며, 접근 제어자에 상관없이 해당 클래스 내에서 선언된 모든 필드 정보를 제공합니다.
예를 들어, String
에 대해 getDeclaredFields()
메서드를 실행하면, 이 클래스에 정의된 모든 필드에 대한 정보를 얻을 수 있습니다:
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class JavaReflectionField {
public static void main(String[] args) {
try {
Class<?> cls = Class.forName("java.lang.String");
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
System.out.println("Field name: " + field.getName());
System.out.println("Type: " + field.getType());
System.out.println("Modifiers: " + Modifier.toString(field.getModifiers()));
System.out.println();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
위 코드를 실행하면, String
클래스의 내부 필드에 대한 정보를 얻을 수 있습니다:
Field name: value
Type: class [B
Modifiers: private final
Field name: coder
Type: byte
Modifiers: private final
Field name: hash
Type: int
Modifiers: private
Field name: hashIsZero
Type: boolean
Modifiers: private
Field name: serialVersionUID
Type: long
Modifiers: private static final
Field name: COMPACT_STRINGS
Type: boolean
Modifiers: static final
Field name: serialPersistentFields
Type: class [Ljava.io.ObjectStreamField;
Modifiers: private static final
Field name: REPL
Type: char
Modifiers: private static final
Field name: CASE_INSENSITIVE_ORDER
Type: interface java.util.Comparator
Modifiers: public static final
Field name: LATIN1
Type: byte
Modifiers: static final
Field name: UTF16
Type: byte
Modifiers: static final
...
다음과 같이 필드 이름을 지정하여 특정 필드에만 접근하는 것도 가능합니다:
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class JavaReflectionField {
public static void main(String[] args) {
try {
Field field = String.class.getDeclaredField("value");
System.out.println("Field name: " + field.getName());
System.out.println("Type: " + field.getType());
System.out.println("Modifiers: " + Modifier.toString(field.getModifiers()));
System.out.println();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Reading Field Values
리플렉션을 통해 필드에 접근한 후에는, 그 필드의 값도 파악할 수 있어야 합니다. 필드의 값을 가져오기 위해서는 Field
객체의 get**()
메서드를 활용합니다. 만약 필드의 타입이 원시 타입인 경우에는 해당 타입에 맞는 메서드를 사용해야 합니다. 예를 들어, int
타입의 필드 값을 가져오려면 Field
객체의 getInt()
메서드를 사용합니다.
다음 코드는 Cat
클래스의 인스턴스에서 name
필드와 age
필드의 값을 가져오는 예시입니다.
public class JavaReflectionField {
public static void main(String[] args) {
try {
Cat cat = new Cat("Ongs", 10);
Field fieldName = Cat.class.getDeclaredField("name");
Field fieldAge = Cat.class.getDeclaredField("age");
fieldName.setAccessible(true);
fieldAge.setAccessible(true);
System.out.println("Name: " + fieldName.get(cat));
System.out.println("Age: " + fieldAge.getInt(cat));
} catch (Exception e) {
e.printStackTrace();
}
}
}
리플렉션의 getDeclaredField()
메서드로 필요한 필드를 검색하고, setAccessible(true)
를 호출하여 해당 필드에 접근할 수 있게 설정하였습니다. 이렇게 하면, Cat
인스턴스의 private
필드에도 직접적으로 접근할 수 있게 됩니다.
위의 코드를 실행하면 다음과 같은 출력을 얻을 수 있게 됩니다:
Name: Ongs
Age: 10
Setting Field Values
리플렉션을 사용하면 객체의 필드에 저장된 값 또한 동적으로 변경할 수 있습니다. 필드의 값을 변경하려면 Field
객체의 set**()
메서드를 사용하면 됩니다. 원시 타입의 필드를 변경하려면 적절한 타입의 set()
메서드를 사용해야 합니다. 예를 들어, int
타입의 필드를 변경하려면 Field
객체의 setInt()
메서드를 사용합니다.
다음 코드는 Cat
인스턴스의 name
필드와 age
필드의 값을 변경하는 예시입니다:
public class JavaReflectionField {
public static void main(String[] args) {
try {
Cat cat = new Cat("Ongs", 10);
Field fieldName = Cat.class.getDeclaredField("name");
Field fieldAge = Cat.class.getDeclaredField("age");
fieldName.setAccessible(true);
fieldAge.setAccessible(true);
System.out.println("prev Name: " + fieldName.get(cat));
System.out.println("prev Age: " + fieldAge.getInt(cat));
fieldName.set(cat, "Mongs");
fieldAge.setInt(cat, 6);
System.out.println("next Name: " + fieldName.get(cat));
System.out.println("next Age: " + fieldAge.getInt(cat));
} catch (Exception e) {
e.printStackTrace();
}
}
}
리플렉션을 사용하여 필드의 값을 변경하는 과정은 다음과 같습니다. 먼저, getDeclaredField()
메서드를 사용하여 인스턴스에서 특정 필드에 해당하는 Field
객체를 획득합니다. 그런 다음, setAccessible()
를 호출하여 private
접근 제한이 있는 필드에도 접근할 수 있도록 설정합니다. 마지막으로, set**()
메서드를 호출하면 필드의 값을 원하는 새로운 값으로 설정할 수 있습니다. 이 메서드는 첫 번째 파라미터로 객체의 인스턴스, 두 번째 파라미터로 새로 설정할 값을 받습니다. 이렇게 하면, 실행 중에도 필드의 값을 자유롭게 변경할 수 있습니다.
위의 코드를 실행하면 아래와 같은 결과를 얻을 수 있습니다:
prev Name: Ongs
prev Age: 10
next Name: Mongs
next Age: 6
리플렉션을 이용해서 name
필드의 값이 "Ongs"
에서 "Mongs"
로, age
필드의 값이 10
에서 6
으로 변경되었음을 확인했습니다. 이처럼 리플렉션을 사용하면 실행 중인 프로그램의 상태를 동적으로 바꿀 수 있습니다.
Methods
Java의 리플렉션 API는 클래스의 메서드에 접근하고 동적으로 조작하는 기능을 제공합니다. 이는 런타임에서 접근 제한이 설정된 메서드를 인스턴스에서 접근하거나, 객체의 메서드를 동적으로 호출하는데 매우 유용하게 사용됩니다.
Accessing Methods
클래스의 메서드 정보를 담고 있는 Method
객체를 얻기 위한 주요 메서드는 다음과 같습니다:
getMethod()
: 이 메서드는 지정된 이름과 파라미터 타입에 일치하는public
메서드를 찾는데 사용됩니다. 상속된 클래스의 메서드도 포함해서 검색합니다.getMethods()
: 이 메서드는 해당 클래스와 그 상위 클래스에서 정의된 모든public
메서드를 배열 형태로 반환합니다.getDeclaredMethod()
: 이 메서드는 지정된 이름과 매개변수 유형에 일치하는 메서드를 찾습니다. 이 메서드는 접근 제어자에 영향 받지 않으며, 상속된 클래스의 메서드는 포함하지 않습니다.getDeclaredMethods()
: 이 메서드는 현재 클래스에서 정의된 모든 메서드를 접근 제어자와 관계없이 배열 형태로 반환합니다.
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
public class JavaReflectionMethod {
public static void main(String[] args) {
try {
Class<?> cls = Class.forName("java.lang.String");
Method[] methods = cls.getDeclaredMethods();
for (Method method : methods) {
System.out.println("Method name: " + method.getName());
System.out.println("Return type: " + method.getReturnType().getSimpleName());
System.out.println("Parameter types: " + Arrays.toString(method.getParameterTypes()));
System.out.println("Modifiers: " + Modifier.toString(method.getModifiers()));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
리플렉션 API를 통해 얻을 수 있는 클래스의 메서드 정보는 다음과 같습니다:
getName()
: 메서드의 이름getParameterTypes()
: 메서드의 파라미터 타입getReturnType()
: 메서드의 반환 타입getModifiers()
: 메서드의 접근 제어자 정보getExceptionTypes()
: 메서드가 던지는 예외 타입
다음은 String
클래스에서 추출된 메서드들의 출력 예시입니다:
Method name: value
Return type: byte[]
Parameter types: []
Modifiers:
Method name: equals
Return type: boolean
Parameter types: [class java.lang.Object]
Modifiers: public
Method name: length
Return type: int
Parameter types: []
Modifiers: public
Method name: toString
Return type: String
Parameter types: []
Modifiers: public
Method name: hashCode
Return type: int
Parameter types: []
Modifiers: public
Method name: getChars
Return type: void
Parameter types: [int, int, class [C, int]
Modifiers: public
Method name: compareTo
Return type: int
Parameter types: [class java.lang.String]
Modifiers: public
...
이런 방식으로 리플렉션을 사용하여 클래스에서 사용 가능한 다양한 메서드들의 이름, 반환 타입, 파라미터 타입, 접근 제한자를 확인할 수 있습니다. 이를 통해 개발자는 클래스의 구조를 더 잘 이해하고, 필요한 기능을 동적으로 호출할 수 있습니다.
지정한 이름과 파라미터 유형에 일치하는 특정 메서드에 접근하는 예시도 살펴보겠습니다:
import java.lang.reflect.Method;
public class JavaReflectionMethod {
public static void main(String[] args) {
try {
Class<?> cls = Class.forName("java.lang.StringBuffer");
Method appendMethod = cls.getDeclaredMethod("append", String.class);
} catch (
Exception e) {
e.printStackTrace();
}
}
}
위 코드는 StringBuffer
클래스 안에 있는 append()
라는 메서드를 찾는 방법을 보여주며, 이과정을 통해 해당 메서드의 정보를 획득하게 됩니다.
Invoking Methods
리플렉션을 사용하여 얻은 Method
객체의 invoke()
메서드를 이용하면, 특정 객체의 메서드를 실행할 수 있습니다. 이때, 인스턴스 메서드를 호출하려면, 첫번째 인자로 그 메서드를 실행할 객체 인스턴스를 넣어주고, 그 다음 인자들은 호출할 메서드에 필요한 파라미터들을 넣어줍니다. 만약 정적 메서드를 호출하려한다면, 첫번째 인자로 null
을 입력합니다.
다음은 리플렉션을 이용해 StringBuffer
클래스에서 append()
메서드를 알아내고, 이 메서드를 호출하여 "Hello, World!"
라는 문자열을 추가한 후 그 출력결과를 표시하는 코드입니다:
import java.lang.reflect.Method;
public class JavaReflectionMethod {
public static void main(String[] args) {
try {
StringBuffer buffer = new StringBuffer();
Method appendMethod = StringBuffer.class.getDeclaredMethod("append", String.class);
appendMethod.invoke(buffer, "Hello, World!");
System.out.println("Buffer content: " + buffer);
} catch (
Exception e) {
e.printStackTrace();
}
}
}
위의 코드를 실행하면, StringBuffer
인스턴스에 성공적으로 "Hello, World!"
문자열이 추가된 것을 확인할 수 있습니다. 출력 결과는 아래와 같습니다:
Buffer content: Hello, World!
Constructors
Java 리플렉션 API는 클래스의 생성자에 접근하고 객체를 인스턴스화하는 다양한 방법을 제공합니다. 이를 통해 개발자는 클래스에 선언된 생성자들을 조사하고, 필요에 따라 동적으로 객체를 생성할 수 있습니다.
Accessing Constructors
클래스의 생성자 정보를 담고 있는 Constructor
객체에 접근하는 주요 메서드는 다음과 같습니다:
getConstructor()
: 이 메서드는 지정된 파라미터 유형에 일치하는public
생성자를 찾는데 사용됩니다.getConstructors()
: 이 메서드는 해당 클래스에서 선언된 모든public
생성자를 배열 형태로 반환합니다.getDeclaredConstructor()
: 이 메서드는 지정된 파라미터 유형에 일치하는 모든 생성자를 찾습니다. 이 메서드는 접근 제어자에 영향 받지 않습니다.getDeclaredConstructors()
: 이 메서드는 해당 클래스에서 선언된 모든 생성자를 접근 제어자와 관계없이 배열 형태로 반환합니다.
예를 들어, Cat
클래스의 모든 생성자 정보를 얻기 위해 getDeclaredConstructors()
메서드를 사용할 수 있습니다:
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Arrays;
public class JavaReflectiopnConstructor {
public static void main(String[] args) {
try {
Class<?> catClass = Class.forName("app.catsriding.algorithms.Cat");
Constructor<?>[] declaredConstructors = catClass.getDeclaredConstructors();
for (Constructor<?> catConstructor : declaredConstructors) {
catConstructor.setAccessible(true);
System.out.println("Name: " + catConstructor.getName());
System.out.println("Parameter types: " + Arrays.toString(catConstructor.getParameterTypes()));
System.out.println("Modifiers: " + Modifier.toString(catConstructor.getModifiers()));
System.out.println();
}
} catch (
Exception e) {
e.printStackTrace();
}
}
}
리플렉션의 Constructor
객체는 클래스의 생성자 정보를 나타내며, 다음의 주요 메서드들을 제공합니다:
newInstance()
: 인스턴스화된 클래스의 새로운 인스턴스를 생성하며, 인수로 제공된 객체를 생성자의 인수로 사용합니다.getParameterTypes()
: 이 메서드는 이 생성자가 취하는 매개변수의 타입을Class<?>
객체로 반환합니다.getModifiers()
: 이 메서드는 생성자의 접근 제어자를 반환합니다. 반환 값은Modifier
클래스에 정의된public
,protected
,private
등의 상수값입니다.setAccessible()
: 이 메서드를 사용하면 리플렉션으로private
생성자에 접근이 가능하게 됩니다.
Instantiating Objects
객체 생성 과정은 Constructor
객체의 newInstance()
메서드를 사용하여 수행됩니다. 이 메서드는 필요한 파라미터를 받아 해당 파라미터와 일치하는 생성자를 이용하여 새로운 객체를 생성합니다.
리플렉션을 사용하여 Cat
클래스의 private
생성자로 객체를 생성하고 필드에 값을 입력하는 과정을 살펴보겠습니다:
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class JavaReflectionConstructor {
public static void main(String[] args) {
try {
Class<?> catClass = Class.forName("app.catsriding.dev.Cat");
Constructor<?> privateConstructor = catClass.getDeclaredConstructor();
privateConstructor.setAccessible(true);
Object privateMongs = privateConstructor.newInstance();
Field nameField = catClass.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(privateMongs, "Mongs");
Field ageField = catClass.getDeclaredField("age");
ageField.setAccessible(true);
ageField.set(privateMongs, 6);
System.out.println(publicOngs);
System.out.println(privateMongs);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Class.forName()
메서드를 사용하여"app.catsriding.dev.Cat"
클래스의Class<?>
객체를 얻습니다.getDeclaredConstructor()
메서드를 사용하여private
생성자를 가져옵니다. 이 생성자는 매개변수를 받지 않습니다.setAccessible()
를 사용하여private
생성자에 접근할 수 있도록 합니다.newInstance()
메서드를 사용하여private
생성자를 통해 새Cat
객체를 생성합니다.getDeclaredField()
메서드를 사용하여private
필드에 접근합니다.setAccessible()
를 사용하여private
필드에 접근할 수 있도록 설정합니다.set()
메서드를 사용하여 필드의 값을 설정합니다. 여기서는 생성된Cat
객체의 이름과 나이를 설정합니다.System.out.println()
을 사용하여 생성된Cat
객체의 값을 출력합니다.
코드를 실행해보면 다음과 같이 인스턴스가 생성된 것을 확인할 수 있습니다:
Cat: {name='Ongs', age=10}
Cat: {name='Mongs', age=6}
Modifiers
Java 리플렉션 API는 클래스, 메서드, 그리고 필드와 같은 멤버의 특성을 파악할 수 있는 강력한 도구입니다. Java에서 이 멤버들의 접근 권한, 상태 및 다양한 특성은 제어자(Modifiers)라고 하는 비트 플래그 형태로 지정됩니다. 프로그램 실행 중 이 제어자들을 검사함으로써, 클래스의 특성을 동적으로 분석하고 조작할 수 있습니다. 각 제어자가 나타내는 특성은 다음과 같습니다:
public
(0x0001): 모든 클래스에서 접근이 가능하도록 합니다.private
(0x0002): 선언된 클래스 내부에서만 접근할 수 있도록 제한합니다.protected
(0x0004): 동일 패키지 내의 다른 클래스 또는 상속받은 클래스에서 접근이 가능합니다.static
(0x0008): 클래스 레벨에서 관리되며, 객체를 생성하지 않고도 접근할 수 있습니다.final
(0x0010): 한 번 초기화되면 변경할 수 없습니다. 변수에 대해서는 값이 고정되며, 메서드는 하위 클래스에서 재정의될 수 없습니다.abstract
(0x0400): 구현되지 않은 메서드를 포함하며, 이 메서드들은 상속받은 클래스에서 구현되어야 합니다.interface
(0x0200): 인터페이스를 선언하며, 인터페이스에 선언된 메서드들은 구현 클래스에서 정의되어야 합니다.volatile
(0x0040): 멀티스레드 환경에서 변수의 값이 메인 메모리에 직접적으로 쓰이고 읽히게 합니다.transient
(0x0080): 객체 직렬화시 이 변수를 무시하도록 합니다.
제어자들은 각각의 비트 값으로 표현되며, Java 리플렉션 API를 통해 이들을 확인하여 클래스의 특성을 정확히 파악할 수 있습니다. 이런 동적 분석은 클래스의 구조와 동작 방식을 이해하는 데 중요한 역할을 합니다.
다음은 Java 리플렉션 API를 사용하여 StringBuffer
클래스의 제어자를 확인하는 예시 코드입니다:
import java.lang.reflect.Modifier;
public class JavaReflectionModifier {
public static void main(String[] args) {
int modifiers = StringBuffer.class.getModifiers();
System.out.println("Modifiers for StringBuffer class: " + Integer.toBinaryString(modifiers));
System.out.println("Is public: " + Modifier.isPublic(modifiers));
System.out.println("Is private: " + Modifier.isPrivate(modifiers));
System.out.println("Is protected: " + Modifier.isProtected(modifiers));
System.out.println("Is static: " + Modifier.isStatic(modifiers));
System.out.println("Is final: " + Modifier.isFinal(modifiers));
System.out.println("Is abstract: " + Modifier.isAbstract(modifiers));
System.out.println("Is interface: " + Modifier.isInterface(modifiers));
System.out.println("Is volatile: " + Modifier.isVolatile(modifiers));
System.out.println("Is transient: " + Modifier.isTransient(modifiers));
}
}
실행 결과, StringBuffer
클래스의 제어자들을 확인할 수 있으며, 이를 통해 해당 클래스가 어떻게 정의되었는지 알 수 있습니다.
Modifiers for StringBuffer class: 10001
Is public: true
Is private: false
Is protected: false
Is static: false
Is final: true
Is abstract: false
Is interface: false
Is volatile: false
Is transient: false
Dynamic Proxies
Java 리플렉션 API는 실행 시 인터페이스를 구현하는 클래스의 인스턴스를 동적으로 생성하고, 메소드 호출을 가로채 특정 작업을 수행할 수 있게 하는 동적 프록시(Dynamic Proxy) 생성 기능을 제공합니다. 이 기능은 인터페이스 기반 API 개발과 Aspect-Oriented Programming 기법 구현에 매우 유용합니다. 특히, Spring Framework에서는 이 리플렉션 기능을 활용하여 JdkDynamicProxy
를 포함한 동적 프록시 기능을 구현하고 있습니다.
동적 프록시를 구현하기 위해 사용되는 주요 함수들은 다음과 같습니다:
Proxy.newProxyInstance()
: 동적 프록시 인스턴스를 생성하는 정적 메서드입니다. 이 함수는 클래스 로더, 구현할 인터페이스 목록, 그리고 호출을 처리할 핸들러 객체를 인자로 받아 동적으로 프록시 객체를 생성합니다.InvocationHandler.invoke()
: 프록시 인스턴스를 통해 발생하는 모든 메서드 호출을 처리하며, 이 메서드 호출 시 수행될 사용자 정의 동작을 지정하기 위해 사용됩니다.Class.getInterfaces()
: 클래스가 구현하는 인터페이스의 배열을 반환하는 함수로, 프록시 클래스가 구현해야 할 인터페이스 목록을 제공하는 데 사용됩니다.Proxy.isProxyClass(Class<?>)
: 지정된 클래스가 동적 프록시 클래스인지 여부를 확인하는 함수로, 주어진 클래스가 프록시인지 확인 할 때 사용됩니다.Proxy.getInvocationHandler()
: 지정된 프록시 인스턴스에 연결된InvocationHandler
를 반환하는 함수로, 프록시 객체의 핸들러를 가져올 때 사용됩니다.
아래에서는 Meow
인터페이스를 정의하고 이를 구현하는 동적 프록시 생성 과정을 살펴보겠습니다:
public interface Meow {
void greet(String name);
}
이 인터페이스를 구현하는 동적 프록시는 아래의 코드와 같이 생성할 수 있습니다:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class JavaReflectionDynamicProxy {
public static void main(String[] args) {
InvocationHandler handler = (proxy, method, args1) -> {
System.out.println("Hello, " + args1[0]);
return null;
};
Meow greetingProxy = (Meow) Proxy.newProxyInstance(
Meow.class.getClassLoader(),
new Class<?>[]{Meow.class},
handler);
greetingProxy.greet("Cats!");
}
}
InvocationHandler
인터페이스를 구현하는 익명 클래스를 생성하였습니다. 이 익명 클래스는invoke()
메소드를 통해 어떻게 메서드 호출을 처리할지를 정의합니다. 인자를 받아"Hello, "
라는 문자열과 함께 출력되도록 구현하였습니다.Proxy.newProxyInstance()
메소드를 활용하여Meow
인터페이스를 구현하는 동적 프록시를 생성합니다. 메서드는 클래스 로더, 구현하고자 하는 인터페이스의 배열, 그리고InvocationHandler
인스턴스를 인자로 받는다는 것을 확인할 수 있습니다.- 생성한 프록시 인스턴스를 통해
Meow
인터페이스에 정의된greet()
메소드를 호출합니다.
호출된 메서드는 InvocationHandler
에 의해 처리되며, 실행된 결과는 아래와 같습니다:
Hello, World!
Java 리플렉션 API를 이용한 동적 프록시 생성은 인터페이스 기반의 설계를 한단계 더 발전시킬 수 있습니다. 메서드 호출에 대해 중간에서 추가적인 조작이나 기능을 수행할 수 있도록 도와주는 이러한 기능은 Spring과 같은 프레임워크에서 AOP(Aspect-oriented Programming)를 구현하는 데 필수적입니다.
Limits of Reflection
Java 리플렉션 API는 런타임에 클래스의 메타데이터에 접근하거나, 클래스의 인스턴스를 조작하는 강력한 기능을 제공하지만, 그 사용에는 몇 가지 주요한 제약이 있습니다.
- 모듈화된 클래스의 접근 제한: Java 9 이상에서는 클래스가 모듈 내에 있을 경우 기본적으로 해당 모듈에서
export
하지 않은 클래스에는 접근이 불가능합니다. 이는 리플렉션에도 적용되기 때문에, 모듈화된 클래스에 리플렉션을 사용하려면 추가적인 고려가 필요합니다. - 성능 저하: 리플렉션이 일반적인 Java 코드보다 조금 느립니다. 이는 타입 검증을 컴파일 타임이 아닌 런타임에 수행하기 때문입니다. 이로 인해 애플리케이션의 전반적인 성능에 영향을 끼칠 수 있습니다.
- 비공개 멤버 접근: 리플렉션은
private
필드나 메소드에 접근하고 수정할 수 있습니다. 이는 요구사항에 따라 유용할 수도 있지만, 원치 않게 클래스의 내부 상태를 변경하거나 보안 취약점을 초래할 수 있습니다. Java는 보안 위협을 방지하기 위해SecurityManager
를 제공하지만, 적절한 보안 조치가 이루어지지 않으면 클래스는 여전히 위험에 노출될 수 있습니다. 이는 정보 은닉 원칙을 위반하고, 예기치 않은 시스템 동작을 초래할 수 있으므로 주의가 필요합니다. - 가독성 저하: 리플렉션은 일반적인 코드보다 덜 직관적이고 이해하기 어렵습니다. 이로 인해 코드의 유지 보수성이 악화되고, 디버깅 과정이 더욱 복잡해집니다.
The End of the Beginning
지금까지 Java 리플렉션 API의 기본적인 원리와 사용 방법, 그리고 이 기술이 가지는 여러 한계에 대해 자세히 살펴보았습니다. 리플렉션은 프레임워크나 플러그인과 같은 도구를 개발할 때 필수적인 기술일 수 있지만, 일반적인 소프트웨어 개발 실무에서는 그 사용 빈도가 높지 않을 수 있습니다. 그럼에도 불구하고, 많은 인기 있는 라이브러리와 프레임워크가 이 기술을 적극적으로 활용하고 있기 때문에, 리플렉션의 동작 원리를 이해하는 것은 개발자에게 중요한 자산이 될 것입니다. 💰
- Java