티스토리 뷰
개요
JVM은 .class파일의 정보를 클래스 로더를 통해 읽어오고, 이 정보는 메소드 영역(Method Area) 혹은 Java 8 이상에서는 메타스페이스(Metaspace)에 저장된다. 이러한 구조는 런타임 시 클래스의 내부 정보를 동적으로 분석하거나 조작할 수 있게 해주는 기반이 되며, 바로 이 기능이 리플렉션(Reflection)이다.
Spring 프레임워크는 이 리플렉션 기능을 활용하여, 적절한 의존 객체를 찾아 자동으로 주입(Dependency Injection)해준다. 대표적으로 @Autowired 덕분에 클라이언트 코드는 구체 클래스가 아닌 추상화(인터페이스)에만 의존하게 되어 DIP(Dependency Inversion Principle) 를 만족하며, 새로운 구현체를 추가해도 기존 코드를 수정할 필요가 없으므로 OCP(Open-Closed Principle)지키는데 기여한다.
예를 들어, 다음과 같은 테스트 코드에서도 스프링은 BookService객체를 자동으로 주입해준다.
@SpringBootTest
public class BookServiceTest {
@Autowired
BookService bookService;
@Test
public void di(){
assertNotNull(bookService);
}
}
그렇다면, 먼저 리플렉션이 무엇인지부터 알아보자.
리플렉션(Reflection)
리플렉션이란, 런타임 시점에 클래스의 구조(필드, 메소드, 생성자 등)에 대한 정보를 동적으로 조회하거나 조작할 수 있게 해주는 자바의 기능이다. 개발자는 Class, Field, Method, Constructor와 같은 API를 통해 클래스 정보를 가져오고, 접근 권한을 우회하거나 메소드를 실행할 수 있다.
이러한 정보를 어디서 가져올 수 있을까? 바로 JVM 메모리 영역에 지정된 클래스 정보에서 가져오는 것이다. 애플리케이션이 실행되면 .java파일은 .class파일로 컴파일되고, 이 바이트코드들은 JVM의 클래스 로더에 의해 읽혀 메모리 영역에 적재된다.
클래스의 메타정보는 메소드(메타스페이스)영역에 저장되고, 이렇게 저장된 클래스 정보에 접근하여 동적으로 분석하거나 조작할 수 있는 것이 리플렉션인 것이다.

Class 클래스
리플렉션 기능의 출발점은 바로 java.lang.Class 클래스다. JVM에서 .class파일이 로딩되면, 해당 클래스의 메타데이터는 메소드 영역(또는 메타스페이스)에 저장되고, 이를 기반으로 클래스를 표현하는 Class객체가 생성되어 힙에 적재된다. 이 Class객체를 통해 개발자는 런타임 시 클래스 구조를 조회하거나 조작할 수 있다.
Class 객체를 얻는 방법은 3가지가 존재한다.
Class 객체 조회 해보기
@MyAnnotation
public class Human {
private String name;
public Human(String name) {
this.name = name;
}
private Human() {
}
public void getRestRoom() {
System.out.println(name + " 화장실로 갑니다.");
}
public void offPants() {
System.out.println(name + " 이 바지를 내립니다.");
}
public void dowork() {
System.out.println(name + " 이 볼일을 봅니다.");
}
private void poopOut() {
System.out.println("똥이 나옵니다.");
}
}
Class<?> class1 = human.getClass(); // 인스턴스가 존재한다면 getClass()메서드로 접근할 수 있다.
Class<?> class2 = Human.class;
Class<?> class3 = Class.forName("org.example.reflection.Human"); // FQCN(Fully Qualified Class Name)로 접근
생성자(Constructor)
리플렉션을 사용하면 클래스의 생성자 정보를 가져오고, 이를 통해 객체를 동적으로 생성할 수 있다.
[getConStructor]
Class<?> class1 = Class.forName("org.example.reflection.Human");
// 리플렉션을 통해 생성자 가져오기
Constructor<?> constructor1 = class1.getConstructor();
Constructor<?> constructor2 = class1.getConstructor(String.class);
// 가져온 생성자를 통해 객체를 생성한다.
Object human1 = constructor1.newInstance(); // NoSuchMethodException
Object human2 = constructor2.newInstance("신선영");
위의 코드를 실행하면 NoSuchMethodException이 발생할 것이다. 이유를 잠시 생각해보자. 리플렉션 기능을 통해 생성자 메서들 정보를 가져오려 헀으나, 기본 생성자가 private이라 메소드를 찾지 못해 발생한다. 접근 제어자와 상관없이 클래스 정보를 가져오고 싶다면 getConstructor() 대신 getDeclaredConstructor()메소드를 사용하면 된다.
[getDeclaredConstructor]
Class<?> class1 = Class.forName("org.example.reflection.Human");
// 리플렉션을 통해 생성자 가져오기
Constructor<?> constructor1 = class1.getDeclaredConstructor();
Constructor<?> constructor2 = class1.getDeclaredConstructor(String.class);
// 가져온 생성자를 통해 객체 생성하기
Object human1 = constructor1.newInstance(); // IllegalAccessException !!
Object human2 = constructor2.newInstance("승갱이");
이전과 달리, 이번에는 생성자 정보를 가져오는 데에는 성공하지만 newInstance() 호출 시 IllegalAccessException이 발생한다. 그 이유는 해당 생성자가 private으로 선언되어 있어 직접 호출할 수 없기 때문이다.
아래와 같이 setAccessible(true)메소드를 통해서 해당 생성자에 접근할 수 있도록 설정한다. 여기서 중요한점은 클래스를 수정하지 않고, 리플렉션을 통해 생성자 정보를 조작한 후 호출까지 했다는 점이다.
Class<?> class1 = Class.forName("me.shinsunyoung.Human");
Constructor<?> constructor1 = class1.getDeclaredConstructor();
Constructor<?> constructor2 = class1.getDeclaredConstructor(String.class);
// 해당 생성자에 접근할 수 있도록 설정
constructor1.setAccessible(true);
// 가져온 생성자를 통해 객체를 생성한다.
Object human1 = constructor1.newInstance();
Object human2 = constructor2.newInstance("신선영");
객체 필드(Field) 조회해보기
리플렉션을 이용하여 Filed 타입의 오브젝트로 객체 필드에 접근할 수 있다.
현재 Human의 필드가 Private으로 되어있기 때문에 getFields()메소드로는 조회되지 않는다. getDeclaredFields()로 접근해야 한다.
[getFields]
Class<?> class1 = Class.forName("org.example.reflection.Human");
for(Field field : class1.getFields()){
System.out.println(field);
}
[getDeclaredFields]
Class<?> class1 = Class.forName("me.shinsunyoung.Human");
for (Field field : class1.getDeclaredFields()) {
System.out.println(field);
}
이제 위 리플렉션 기능을 이용하여 객체를 생성하고, 해당 객체의 필드 정보를 조회해보자. 매개 변수가 있는 생성자는 현재 public이므로 setAccessible(ture) 메소드를 사용하지 않아도 된다. 하지만 객체의 name필드는 private 접근제어자이므로 field.get()메소드를 호출하기 전에 리플렉션에 대한 접근 설정을 true로 설정해줘야한다.
Class<?> class1 = Class.forName("me.shinsunyoung.Human");
Constructor<?> constructor = class1.getDeclaredConstructor(String.class);
Object human = constructor.newInstance("신선영");
for (Field field : class1.getDeclaredFields()) {
System.out.println(field);
field.setAccessible(true);
System.out.println(field.get(human));
}
객체 필드 수정하기
단순 조회 뿐만아니라 객체 필드의 값도 수정할 수 있다. Setter메소드가 없어도, 접근제어자가 private이라도 리플렉션은 가능하다.
Class<?> class1 = Class.forName("me.shinsunyoung.Human");
Constructor<?> constructor = class1.getDeclaredConstructor(String.class);
Object human = constructor.newInstance("신선영");
for (Field field : class1.getDeclaredFields()) {
System.out.println(field);
field.setAccessible(true);
field.set(human, "변경된 값");
System.out.println(field.get(human));
}
메소드(Method) 조회 및 호출하기
리플렉션을 사용하면 클래스 내부의 메소드에 접근하고, 직접 호출할 수도 있다. 이때 java.lang.reflect.Method 객체를 통해 특정 메소드를 조회하고, invoke() 메소드를 이용해 실행한다.
Class<?> class1 = Class.forName("me.shinsunyoung.Human");
Constructor<?> constructor = class1.getDeclaredConstructor(String.class);
Object human = constructor.newInstance("신선영");
// 메서드 조회
Method goRestRoomMethod = class1.getDeclaredMethod("getRestRoom");
Method popOutMethod = class1.getDeclaredMethod("poopOut");
// private 메서드는 접근 허용 필요
popOutMethod.setAccessible(true);
// 메서드 실행
goRestRoomMethod.invoke(human);
popOutMethod.invoke(human);
어노테이션 조회하기
먼저 어노테이션을 조회하려면 @Retention으로 해당 어노테이션이 언제까지 유지할지 정해야한다. 런타임까지 유지해야 바이트 코드안에 들어가게되며, 리플렉션으로 조회할 수 있다. 그리고 어디에 사용할 수 있는 어노테이션인지 @Target으로 지정해야한다.
@Retention(RetentionPolicy.RUNTIME) // 런타임까지 유지
@Target({ElementType.TYPE, ElementType.FIELD}) // 클래스, 필드에만 사용 가능
public @interface MyAnnotation {
}
Class<?> class1 = Class.forName("me.shinsunyoung.Human");
// 클래스에 선언된 어노테이션 조회
if (class1.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = class1.getAnnotation(MyAnnotation.class);
System.out.println("클래스 어노테이션: " + annotation.value());
}
마무리
이번 글에서는 리플렉션 API의 다양한 사용법에 대해 알아보았다. 우리가 주목해야할 점은, 리플렉션이 프레임워크나 라이브러리에서 코드의 동적 제어를 가능하게 하는 핵심 기술이라는 점이다. 코드를 작성한 개발자는 작성한 클래스의 정보를 확인할 수 있지만, 프레임워크나 라이브러리 입장에서는 알 수 없다. 때문에 리플렉션을 이용하여 런타임 시에 클래스 정보를 얻고 이를 기반하여 프레임워크나 라이브러리가 지원하는 기능을 수행하는 것이다. 스프링의 주요기능 IoC컨테이너의 DI도 리플렉션의 원리가 들어있다.
물론 단점도 존재한다. 위에서 봤듯이 Private 접근제한자를 무시하고 접근가능하기 때문에 클래스 설계자가 의도한 캡슐화가 깨질 것이며, 런타임에 클래스를 분석하기 때문에 성능 면에서도 불리하다.
따라서 리플렉션은 정말 필요한 곳에 한정적으로 사용하는 것이 바람직하다. 잘만 이용한다면 아이디어와 상상력만으로도 JVM 위에서 신박한 툴이나 프레임워크를 만들어낼 수 있는 잠재력을 지닌 API인 것 같다.
참고 자료
- 백기선, 인프런 - 더 자바, 코드를 조작하는 방법
'Java' 카테고리의 다른 글
| 다양한 I/O 모델을 지원하기 위한 Connector 구조 개선 (0) | 2025.05.31 |
|---|---|
| 오픈소스 분석 - Tomcat 소켓 I/O 동작 방식 파헤쳐보기(BIO, NIO Connector) (0) | 2025.05.28 |
| 자바에서는 멀티플렉싱을 어떻게 지원할까? – Java NIO의 Selector (0) | 2025.05.15 |
| 접두사 기반 URL 탐색 구조 성능 측정 및 최적화 (0) | 2025.05.05 |
| Servlet과 Servlet Container 동작 과정의 이해 (3) | 2025.04.09 |