1. 리플렉션이란 무엇인가
Java의 리플렉션은 프로그램이 실행 중에 자신의 구조를 조사하고 심지어 변경할 수 있게 해 주는 메커니즘입니다. 마치 스스로의 내부를 들여다보며 “내게 어떤 필드와 메서드가 있지? 생성자는 무엇이지? 나는 대체 어떤 클래스지?”라고 알아보고 — 심지어 자신의 private 메서드도 호출할 수 있는 것과 같습니다.
공식 정의:
리플렉션은 실행 중에 클래스, 인터페이스, 필드, 메서드, 생성자에 대한 정보를 얻고 그것들을 다룰 수 있게 해 주는 API입니다.
리플렉션이 왜 필요한가?
- 프레임워크: Spring, Hibernate, JUnit은 리플렉션으로 “마법”을 구현합니다: 객체 자동 생성, 의존성 주입, 이름이나 애너테이션으로 메서드 호출.
- 직렬화: 객체를 JSON/XML로 변환하고 다시 되돌리려면 필드를 알아내고 접근할 수 있어야 합니다.
- 테스트: 애너테이션(예: @Test)이 붙은 메서드를 자동으로 찾고 호출.
- 클래스 동적 로딩: 실행 중 이름으로 클래스를 로딩(플러그인, 드라이버).
- IDE와 코드 분석: 자동 완성, 검사, 리팩터링.
생활 속 예시
public class Person {
private String name;
private int age;
public void sayHello() {
System.out.println("안녕하세요, 제 이름은 " + name);
}
}
리플렉션을 사용하면 실행 중에 Person 클래스에 name과 age 필드, 그리고 sayHello 메서드가 있다는 것을 알 수 있습니다. 더 나아가, private 필드 값을 바꾸거나 이름으로 메서드를 호출할 수도 있습니다.
약간의 유머
리플렉션은 “매트릭스” 같다고 할 수 있습니다: 여러분의 모든 코드는 실행 중에 조사하고 바꿀 수 있는 데이터라는 사실을 자각하는 것이죠. 다만 보안이라는 빨간 알약은 잊지 마세요!
2. Class 클래스: 리플렉션의 핵심
Java에서는 모든 객체와 모든 타입이 실행 중에 특별한 Class 타입의 객체와 연결됩니다. 이 객체가 바로 타입에 대한 “메타정보”입니다. java.lang.Class<T> 클래스는 리플렉션 세계로 들어가는 중심 진입점입니다.
Class 객체를 얻는 방법?
몇 가지 방법이 있습니다:
- .class로
클래스가 컴파일 타임에 이미 알려져 있다면 가장 직접적이고 안전한 방법입니다:Class<Person> personClass = Person.class; - 객체를 통해: getClass()
이미 인스턴스가 있다면:
여기서 clazz는 객체 p의 실제 클래스를 설명하는 Class 객체입니다.Person p = new Person(); Class<?> clazz = p.getClass(); - 문자열로: Class.forName()
클래스 이름을 실행 중에만 알 수 있다면:
플러그인, 드라이버 등 동적 로딩에 적합합니다.Class<?> clazz = Class.forName("com.example.Person");
예: 클래스 이름 얻기
Person p = new Person();
Class<?> clazz = p.getClass();
System.out.println(clazz.getName()); // 출력: Person 또는 com.example.Person
표: Class 객체를 얻는 주요 방법
| 방법 | 사용 시점 | 예 |
|---|---|---|
|
컴파일 시점에 클래스가 알려져 있을 때 | |
|
객체가 있을 때 | |
|
런타임에 클래스 이름만 알려져 있을 때 | |
도식: 객체와 그 Class
+-------------------+
| Person p |
+-------------------+
|
v
+-------------------+
| p.getClass() |
+-------------------+
|
v
+-------------------+
| Class<Person> |
+-------------------+
Class로 타입 확인
가끔 객체가 특정 클래스에 속하는지 확인해야 합니다:
if (p.getClass() == Person.class) {
System.out.println("이건 확실히 Person!");
}
상속을 고려하려면 — 일반적인 instanceof 연산자:
if (p instanceof Person) {
// 평소처럼 동작한다
}
3. 언제 리플렉션이 필요하고 언제는 아닌가
큰 힘에는 큰 책임이 따릅니다. 리플렉션은 강력한 도구지만, 정말 필요할 때만 사용해야 합니다.
리플렉션이 꼭 필요한 곳
- 프레임워크와 라이브러리: Spring, Hibernate, JUnit, Jackson — 자동화, “마법”, 그리고 더 적은 보일러플레이트 코드.
- 직렬화: 객체를 JSON/XML로 바꾸려면 필드를 알아야 합니다.
- 플러그인과 확장: 클래스를 동적으로 로딩하며, 구조를 미리 알 수 없습니다.
리플렉션을 남용하면 안 되는 이유
- 타입 안정성 저하: 오류가 런타임에 드러납니다.
- 유지보수 어려움: 코드가 덜 명확해집니다.
- 성능: 직접 호출보다 느립니다.
- 보안: 캡슐화를 우회해 private 데이터를 노출시킬 수 있습니다.
예: 리플렉션이 필요 없는 경우
p.sayHello();
예: 리플렉션이 반드시 필요한 경우
미니 테스트 프레임워크를 작성한다고 합시다. 사용자는 메서드를 @Test 애너테이션으로 표시하고, 프로그램은 이를 자동으로 찾아 호출해야 합니다 — 리플렉션 없이 불가능합니다.
4. 데모: Class와 친해지기
예: 클래스 이름과 슈퍼클래스 출력
public class ReflectionDemo {
public static void main(String[] args) {
String s = "Hello, reflection!";
Class<?> clazz = s.getClass();
System.out.println("클래스 이름: " + clazz.getName());
System.out.println("단순 이름: " + clazz.getSimpleName());
System.out.println("패키지: " + clazz.getPackageName());
System.out.println("슈퍼클래스: " + clazz.getSuperclass().getName());
}
}
결과:
클래스 이름: java.lang.String
단순 이름: String
패키지: java.lang
슈퍼클래스: java.lang.Object
예: 기본 타입의 Class 얻기
Class<Integer> intClass = int.class;
System.out.println(intClass.getName()); // int
Class<?> doubleClass = double.class;
System.out.println(doubleClass.getName()); // double
예: 배열의 Class 얻기
int[] arr = new int[10];
Class<?> arrClass = arr.getClass();
System.out.println(arrClass.getName()); // [I (배열용 특수 형식)
예: 타입 소속 여부 확인
if (arrClass.isArray()) {
System.out.println("이건 배열입니다!");
}
5. 실습: 미니 프로그램 “이건 어떤 클래스?”
클래스의 전체 이름(예: "java.util.ArrayList")을 입력받아 해당 클래스의 기본 정보를 출력하는 프로그램을 작성합니다.
import java.util.Scanner;
public class ClassInfoPrinter {
public static void main(String[] args) throws Exception {
Scanner scanner = new Scanner(System.in);
System.out.print("클래스의 전체 이름을 입력하세요: ");
String className = scanner.nextLine();
Class<?> clazz = Class.forName(className);
System.out.println("클래스 이름: " + clazz.getName());
System.out.println("패키지: " + clazz.getPackageName());
System.out.println("슈퍼클래스: " + clazz.getSuperclass().getName());
Class<?>[] interfaces = clazz.getInterfaces();
System.out.print("구현 인터페이스: ");
for (Class<?> i : interfaces) {
System.out.print(i.getName() + " ");
}
System.out.println();
}
}
실행 예:
클래스의 전체 이름을 입력하세요: java.util.ArrayList
클래스 이름: java.util.ArrayList
패키지: java.util
슈퍼클래스: java.util.AbstractList
구현 인터페이스: java.util.List java.util.RandomAccess java.lang.Cloneable java.io.Serializable
Class와 리플렉션에 관한 흥미로운 사실
- JVM에 로드된 각 타입에는 하나의 대표자 — Class 객체 — 가 있습니다. 같은 타입의 모든 인스턴스는 동일한 Class<T>를 공유합니다.
- 타입의 종류를 판별할 수 있습니다: isInterface(), isEnum(), isArray(), isPrimitive() 등.
- Class.forName에 잘못된 이름을 주면 ClassNotFoundException 예외가 발생합니다.
- 리플렉션을 사용하면 컴파일 타임에 클래스를 몰라도 객체를 생성할 수 있습니다 — 이는 다음 강의에서 다룹니다.
6. 리플렉션을 처음 접할 때 흔한 실수
오류 №1: 리플렉션은 빠르다고 기대함.
사실 리플렉션은 일반적인 메서드 호출이나 필드 접근보다 느립니다. 사소한 것마다 사용하지 마세요.
오류 №2: 존재하지 않는 클래스의 Class를 얻으려 함.
이름을 틀리면 ClassNotFoundException이 발생합니다. 이 예외를 처리하세요.
오류 №3: Class 객체와 인스턴스를 혼동함.
Class 객체는 타입의 구조를 설명할 뿐, 그 타입의 인스턴스가 아닙니다.
오류 №4: 불필요한 리플렉션 사용.
일반 코드로 충분하다면 — 그렇게 하는 것이 더 낫습니다. 리플렉션은 동적 동작, 프레임워크, 플러그인과 같은 작업에 필요합니다.
오류 №5: private 필드와 메서드를 부주의하게 다룸.
리플렉션은 캡슐화를 우회할 수 있게 하므로, 특히 큰 프로젝트에서 오류와 보안 문제로 이어질 수 있습니다.
GO TO FULL VERSION