그동안 얕게 알고 쓰던 자바를 정리하기 위한 포스팅 / 개인 기록용
자바에서는 리플렉션이란 개념이 있다.
리플렉션은 간단하게 런타임 시점에 클래스에 대한 정보 혹은 구조를 조회하고 수정하는 것이라고 표현할 수 있다.
어떻게 그리고 왜 사용되는지 알아보자
리플렉션 용도?
일반적으로 비즈니스 로직을 작성하는 경우에는 리플렉션을 사용할 일이 별로 없다.
적어도 비즈니스 로직을 작성할 때에는 의도를 가지고 사용할 객체를 인식하는 상태에서 코드를 작성하기 때문이다.
그래서 주로 리플렉션은 프레임워크, 라이브러리에서 사용된다. 프레임워크, 라이브러리는 자신의 기능을 외부에서 쓰이길 바란다.
근데 외부에서 자신들의 기능을 사용할 때 어떤 객체들이 전달될 지 모른다. 따라서 런타임 시점에 클래스에 대한 정보 혹은 구조를 조회해야할 이유는 어떤 객체가 전달될 지 모르는 상황에서 일관된 행위를 하기 위해서라고 볼 수 있지 않을까 싶다.
public class ClassParser {
public void parseVariable(Object object) {
}
}
객체가 전달되었을 때 해당 객체에 존재하는 필드들을 이쁘게 출력해주는 라이브러리를 만든다고 생각해보자.
Jackson처럼 객체를 Json으로 변환하는 그런 느낌이라고 생각하면 된다.
이 라이브러리는 어떤 객체가 올지 모르니까 Object로 받는다. 근데 이제 어떻게 객체의 필드들을 탐색할까?
이럴 때 리플렉션이 사용된다.
리플렉션은 Class를 사용하는 것
먼저 리플렉션을 사용하기 전에 리플렉션이란 것을 가능하게 해주는 것은 Class 덕분이다.
여기서 말하는 Class는 객체지향에 나오는 그 Class는 아니다.
위 코드는 java.lang 패키지에 존재하는 Class라는 타입 / 클래스이다.
JVM은 자바 프로그램이 실행되면서 필요한 클래스들을 로딩 한다. 그리고 로딩 된 클래스는 JVM 런타임 메모리 영역에서 관리 된다.
그리고 로딩하면서 수집된 정보를 잘 가꾸어서 보관하는데 그 데이터가 Class인 것이다.
따라서 리플렉션이란 것은 JVM이 만들어서 관리하고 있는 Class를 이용하는 것이다.
Class 사용하기 - 리플렉션 사용 준비
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
String data = new String("data");
Class<?> strClazz1 = String.class;
Class<?> strClazz2 = Class.forName("java.lang.String");
Class<?> strClazz3 = data.getClass();
}
}
Class는 만드는 것이 아니다. JVM이 만들어둔 Class를 사용할 뿐이다.
Class를 사용하는 방법은 X.class로 접근하기, Class.forName 메서드 사용하기, 객체의 getClass 메서드 사용하기이다.
위 방법으로 Class를 불러왔다면 리플렉션 사용 준비는 끝이다.
필드 조회 하기
public class ClassParser {
public void parseVariable(Object object) throws IllegalAccessException {
Class<?> clazz = object.getClass(); // 1. Class 불러오기
Field[] fields = clazz.getDeclaredFields(); // 2. 필드 정보 배열 조회
for (Field field : fields) { // 3. 필드 정보 배열 순회
field.setAccessible(true); // 4. private 접근을 위한 설정
System.out.println("fieldName: " + field.getName() + " || value: " + field.get(object));
}
}
}
위 코드는 전달받은 객체의 필드를 출력하는 라이브러리 기능이라고 하자.
먼저 전달받은 객체의 getClass 메서드를 이용해서 리플렉션을 사용가능 하게 해주는 Class를 불러온다.
Class는 객체에 존재하는 필드 정보를 조회할 수 있도록 getFields, getDeclaredFields를 제공한다.
일단 두 메서드의 차이는 아래에서 알아보고 지금은 getDeclaredFields를 사용하자.
getDeclaredFields는 Field 배열을 반환한다. Field는 필드에 대한 정보가 담겨있다. 필드 이름, 필드 타입, 필드 접근 제어자 등등..
따라서 이 Field들을 순회하면서 출력을 해주면 된다.
그런데 필드 정보 배열을 순회하는 for문에서 setAccessible이 있다. 리플렉션의 강력한 기능인데 private, protected 접근 제어자를
무시하고 그냥 접근하게 해주는 설정이다. true로 해주면 접근 제어자에 관계 없이 필드에 접근한다.
필드의 값을 불러오기 위해서는 Filed의 get 메서드를 사용한다. 단순히 조회하고자 하는 객체를 전달해주면 된다.
public class Main {
public static void main(String[] args) throws IllegalAccessException {
ClassParser cvp = new ClassParser();
Parent parent = new Parent(1, "name", 15);
cvp.parseVariable(parent);
}
}
/*
fieldName: id || value: 1
fieldName: name || value: name
fieldName: age || value: 15
*/
위 코드를 실행하면 위와 같이 Parent 객체의 정보가 출력된다.
getDeclared vs get
위에서 잠깐 언급되었던 getDeclaredFields와 getFields의 차이를 알아보자.
이 내용은 필드 뿐만 아니라 Class로부터 얻을 수 있는 정보에 대해 동일하다. 위 사진을 보면 필드 정보 뿐만 아니라
메서드 정보, 어노테이션 정보, 생성자 정보도 조회할 수 있음을 알 수 있다.
아무튼 getXXX의 경우는 상속하고 있는 클래스, 구현하고 있는 인터페이스를 포함하여 모든 public 정보를 조회한다.
getField라면 자신이 가지고 있는 필드뿐만 아니라 부모 클래스의 public 필드까지 조회된다.
getMethod라면 자신이 가지고 있는 메서드뿐만 아니라 부모 클래스의 public 메서드까지 조회된다.
반면에 getDeclaredXXX의 경우는 자신이 선언한 모든 정보를 조회한다.
getDeclaredField라면 부모로부터 받고 있는 필드 정보는 생략하고 자신이 선언한 모든 필드(접근 제어자 상관 X)를 조회한다.
getDeclaredMethod라면 부모로부터 받고 있는 메서드 정보는 생략하고 자신이 선언한 모든 메서드를(접근 제어자 상관 X) 조회한다.
package com.eager;
public class Parent {
public int id;
public String name;
public int age;
Parent(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public void sayHello() {
}
public void sayBye() {
}
}
public class Child extends Parent {
public String address;
Child(int id, String name, int age, String address) {
super(id, name, age);
this.address = address;
}
@Override
public void sayBye() {
}
}
부모 클래스는 id, name, age를 가지고 있다. 자식 클래스는 부모 클래스를 상속하면서 자신만의 필드인 address를 추가한다.
또한 sayBye 메서드만 오버라이딩 한다.
public class Main {
public static void main(String[] args) throws IllegalAccessException {
Class<?> clazz = Child.class;
for (Field field : clazz.getFields()) {
System.out.println(field.getName());
}
System.out.println("--------------------");
for (Field field : clazz.getDeclaredFields()) {
System.out.println(field.getName());
}
}
}
/*
address
id
name
age
--------------------
address
*/
getFields는 부모 클래스의 필드까지 전부 조회하므로 4개의 필드가 조회되었다. (부모 필드는 전부 public이기 때문)
반면에 getDeclaredFields는 자신이 선언한 필드만 조회한다. 즉 부모로부터 받고 있는 필드는 조회하지 않는다.
따라서 자신이 새로 선언한 address 필드만 조회된다.
public class Main {
public static void main(String[] args) throws IllegalAccessException {
Class<?> clazz = Child.class;
for (Method method : clazz.getMethods()) {
System.out.println(method.getName());
}
System.out.println("--------------------");
for (Method method : clazz.getDeclaredMethods()) {
System.out.println(method.getName());
}
}
}
/*
sayBye
sayHello
wait
wait
wait
equals
toString
hashCode
getClass
notify
notifyAll
--------------------
sayBye
*/
getMethods의 경우는 부모 클래스의 public 메서드 전부 조회된다고 하였다. 따라서 Child -> Parent -> Object의 모든
public 메서드가 조회된다.
그리고 getDeclaredMethods의 경우 sayBye만 조회된다. sayHello 메서드는 별도로 오버라이딩 하지 않고 그대로 전달받고 있기
때문에 조회되지 않았고 sayBye는 Child에서 별도로 오버라이딩 선언을 하였기 때문에 조회된다.
필드 수정 하기
package com.eager;
public class Foo {
private final String name;
public Foo(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = Foo.class;
Foo foo = new Foo("FOOFOO");
System.out.println(foo.getName());
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
field.set(foo, "Change FOO");
System.out.println(foo.getName());
}
}
/*
Foo
Change FOO
*/
리플렉션을 통해 필드 수정도 당연히 가능하다.
public 필드는 당연하고 declaredField를 통해 조회한다면 private 필드도 수정이 가능하다.
한가지 더 충격적인 건 final 필드도 수정이 된다는 것이다.
Enum과 Record는 그나마 안전하다.
리플렉션을 사용하면 private 접근 제어자 필드여도 외부에서 접근이 가능하고 final로 지정된 필드도 변경이 될 수 있다는 것이다.
그런데 Enum과 Record는 리플렉션으로부터 그나마 안전하다.
리플렉션과 Enum
위에서 간단하게 리플렉션으로 생성자 정보를 조회할 수 있고 사용할 수 있다고 언급하였다. 즉 클래스에 선언되어 있는 생성자를
리플렉션을 통해 불러올 수 있고 객체를 생성할 수 있다.
public enum Days {
Monday, TuesDay, SunDay;
Days() {
}
}
public class Main {
public static void main(String[] args) {
Class<?> clazz = Days.class;
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
constructor.setAccessible(true);
System.out.println(constructor);
}
}
}
/*
private com.eager.Days(java.lang.String,int)
*/
요일을 의미하는 Days Enum을 정의하였고 리플렉션을 통해 생성자를 조회한 결과 생성하지 않은 private 생성자가 보인다.
또한 이 생성자는 String과 int를 파라미터로 전달받는다.
리플렉션을 통해 Enum이 별도의 생성자를 혼자 만들어서 처리 한다는 것을 알게 되었다.
그럼 리플렉션을 통해 생성자를 조회할 수 있으니 리플렉션을 통해 마음대로 Monday 객체를 또 생성할 수 있을까?
public class Main {
public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException {
Class<?> clazz = Days.class;
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
constructor.setAccessible(true);
constructor.newInstance("Monday", 3);
}
}
}
/*
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
at com.eager.Main.main(Main.java:12)
*/
강력한 리플렉션일지라도 Enum 생성자를 불러와서 객체를 생성하는 것은 불가능하다.
이러한 점 때문에 안전한 싱글톤 객체 생성 방식 중 하나가 Enum으로 불리기도 하다.
Enum에 정의 된 객체는 리플렉션으로도 뚫지 못하기에 JVM내에서 유일하게 1개만 생성됨을 보장받기 때문이다.
public class Main {
public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException {
Class<?> clazz = Days.class;
for (Field field : clazz.getDeclaredFields()) {
System.out.println(field);
}
}
}
/*
public static final com.eager.Days com.eager.Days.Monday
public static final com.eager.Days com.eager.Days.TuesDay
public static final com.eager.Days com.eager.Days.SunDay
private static final com.eager.Days[] com.eager.Days.$VALUES
*/
마지막으로 리플렉션을 통해 Enum의 필드들을 조회해보았다. 그 결과 Enum에서 정의한 객체들이
public static final로 생성 되어있음을 알 수 있었다. 그리고 내부적으로 배열을 통해 Days 객체들을 관리하는 것을 알 수 있었다.
public final class Days extends java.lang.Enum<Days> {
public static final Days Monday = new Days("Monday", 0);
public static final Days TuesDay = new Days("TuesDay", 1);
public static final Days SunDay = new Days("SunDay", 2);
private static final Days[] $VALUES = new Days[]{Monday, TuesDay, SunDay};
private Days(String name, int ordinal) {
super(name, ordinal);
}
}
리플렉션을 통해 생성자와 필드를 조회한 것을 종합해보자면 Enum은 컴파일러에 의해 Monday, TuesDay, SunDay 객체를 생성하고
관리하기 위한 별도의 생성자를 만든다. 첫번째 인자의 String은 Monday, TuesDay, SunDay 객체 이름 문자열이며, int는 객체가
보관 될 배열의 인덱스(상수)이다. 각 객체들은 VALUES라는 배열에서 관리된다.
따라서 실제로는 컴파일러로 인해 Days Enum이 위 코드처럼 유사하게 생성된다고 볼 수 있다.
그럼 리플렉션을 통해 Monday 필드를 변경할 수 있을까?
public class Main {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = Days.class;
Field field = clazz.getDeclaredField("Monday");
field.setAccessible(true);
field.set(Days.Monday, Days.TuesDay);
}
}
/*
Exception in thread "main" java.lang.IllegalAccessException: Can not set static final com.eager.Days field com.eager.Days.Monday to com.eager.Days
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80)
at java.base/jdk.internal.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl.set(UnsafeQualifiedStaticObjectFieldAccessorImpl.java:77)
at java.base/java.lang.reflect.Field.set(Field.java:799)
at com.eager.Main.main(Main.java:12)
*/
리플렉션으로도 이 또한 불가능하다. 리플렉션으로는 Enum을 다루는 것은 읽기용으로만 가능하다.
리플렉션과 Record
자바 14부터 Record 클래스라는 것이 도입되었다. 이는 내부 필드를 불변으로 보관하기 위해 설계 된 클래스이다.
public record Foo(String name) {
}
Foo 클래스는 Record 클래스이고 그 어떠한 행위로도 name 필드의 값을 변경할 수 없다.
리플렉션으로도 안될까?
public class Main {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = Foo.class;
Foo foo = new Foo("FOO");
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
field.set(foo, "ChangeFoo");
}
}
/*
Exception in thread "main" java.lang.IllegalAccessException: Can not set final java.lang.String field com.eager.Foo.name to java.lang.String
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80)
at java.base/jdk.internal.reflect.UnsafeQualifiedObjectFieldAccessorImpl.set(UnsafeQualifiedObjectFieldAccessorImpl.java:79)
at java.base/java.lang.reflect.Field.set(Field.java:799)
at com.eager.Main.main(Main.java:13)
*/
리플렉션으로도 Record의 필드는 변경할 수 없다.
나머지 기능들..
이외에도 리플렉션을 통해 메서드 정보를 불러와서 메서드를 호출할 수 있고 부모 클래스, 나를 감싸는 클래스 등등 여러가지 정보를
조회할 수 있으나 이런 내용들은 이제 단순 사용법에 가깝기 때문에 필요할 것 같은 내용들은 그때 그때 찾아보는 것이 좋을 것 같다.
핵심은 리플렉션은 런타임 시점에 전달되는 객체들의 정보를 파악해야할 때 유용하게 쓰일 수 있다는 것이다.
'java' 카테고리의 다른 글
[JAVA] GC, G1GC (0) | 2025.06.24 |
---|---|
[JAVA] 제네릭 공변, 반공변 (0) | 2025.05.26 |