리플렉션이 필요한 이유
커맨드 패턴으로 서블릿을 구현할 때 흔히 부딪히는 두 가지 불편함이 있다
- 클래스 하나 = 기능 하다: 단순히 기능을 추가할 때마다 클래스를 새로 만들어야 한다
- URL 매핑을 항상 수동으로: 새 기능을 만들 때마다 URL과 클래스를 직접 연결해야 한다
기능 하나당 클래스 하나 – 클래스가 폭발적으로 늘어난다
servletManager.add("/site1", new Site1Servlet());
servletManager.add("/site2", new Site2Servlet());
servletManager.add("/search", new SearchServlet());
반면 아래처럼 하나의 컨트롤러에 여러 기능을 모을 수 있다면 훨씬 깔끔해진다
public class ReflectController {
public void site1(HttpRequest request, HttpResponse response) {
response.writeBody("<h1>site1</h1>");
}
public void site2(HttpRequest request, HttpResponse response) {
response.writeBody("<h1>site2</h1>");
}
public void search(HttpRequest request, HttpResponse response) {
String query = request.getParameter("q");
response.writeBody("<h1>Search: " + query + "</h1>");
}
}
URL 경로 /site1이 들어오면 site1() 메서드를 이름으로 찾아서 호출하면 된다. 이것이 바로 리플렉션이 필요한 핵심 이유이다
클래스와 메타데이터
리플렉션(Reflection)이란, 프로그램이 실행 중에 자기 자신의 구조를 들여다보고 조작할 수 있는 기능이다. 마치 거울(mirror)에 자신을 비춰보듯, 클래스·메서드 필드 등의 메타데이터를 런타임에 동적으로 조사하고 활용한다
주요 특징
- 클래스 메타데이터: 클래스, 이름, 접근 제어자, 부모 클래스, 구현 인터페이스
- 필드 정보: 필드 이름·타입·접근 제어자, 값 읽기/쓰기
- 메서드 정보: 메서드 이름·반환 타입·매개변수, 동적 호출
- 생성자 정보: 매개변수 타입·개수, 동적 객체 생성
예제 클래스
package reflection.data;
public class BasicData {
public String publicField;
private int privateField;
public BasicData() {
System.out.println("BasicData.BasicData");
}
private BasicData(String data) {
System.out.println("BasicData.BasicData: " + data);
}
public void call() {
System.out.println("BasicData.call");
}
public String hello(String str) {
System.out.println("BasicData.hello");
return str + " hello";
}
private void privateMethod() {
System.out.println("BasicData.privateMethod");
}
void defaultMethod() {
System.out.println("BasicData.defaultMethod");
}
protected void protectedMethod() {
System.out.println("BasicData.protectedMethod");
}
}
Class 객체를 얻는 3가지 방법
클래스의 메타데이터는 Class<T> 타입으로 표현된다. 이를 얻는 방법은 세 가지이다
// 1. 클래스 리터럴 (.class)
Class<BasicData> clazz1 = BasicData.class;
// 2. 인스턴스의 getClass()
BasicData instance = new BasicData();
Class<? extends BasicData> clazz2 = instance.getClass();
// 3. 문자열로 동적 조회 (패키지명 포함 필수)
Class<?> clazz3 = Class.forName("reflection.data.BasicData");
세 번째 방법이 핵심이다. 문자열 변수 하나로 클래스를 동적으로 찾을 수 있다는 것이 리플렉션의 강력함이다. 콘솔 입력, 설정 파일, 어노테이션 등 어떤 방식으로도 클래스를 런타임에 결정할 수 있다
getClass()의 변환 타입이Class<? extends BasicData>인 이유:Parent parent = new Child()처럼 다형성에 의해 실제 인스턴스가 자식 타입일 수 있기 때문이다
클래스 기본 정보 탐색
Class<BasicData> basicData = BasicData.class; basicData.getName(); // "reflection.data.BasicData" (패키지 포함 전체 이름) basicData.getSimpleName(); // "BasicData" (단순 클래스 이름) basicData.getPackage(); // package reflection.data basicData.getSuperclass(); // class java.lang.Object basicData.getInterfaces(); // [] (구현 인터페이스 배열) basicData.isInterface(); // false basicData.isEnum(); // false basicData.isAnnotation(); // false int modifiers = basicData.getModifiers(); Modifier.isPublic(modifiers); // true Modifier.toString(modifiers); // "public"
getModifiers()는 접근 제어자와 비접근 제어자가 조합된 정수값을 반환한다. Modifier 클래스로 실제 의미를 해석한다
- 접근 제어자:
public,protected,default(package-private),private - 비접근 제어자:
static,final,abstract,synchronized,volatile등
메서드 탐색과 동적 호출
메서드 메타데이터 조회
Class<BasicData> helloClass = BasicData.class; // 상속된 것 포함, public 메서드만 Method[] methods = helloClass.getMethods(); // 내가 선언한 것만, 접근 제어자 무관 (상속 제외) Method[] declaredMethods = helloClass.getDeclaredMethods();
| 메서드 | 대상 | 접근 제어자 | 상속 포함 |
getMethods() | 해당 클래스 + 상위 클래스 | public 만 | O |
getDeclaredMethods() | 해당 클래스만 | 모두 | X |
정적 호출 vs 동적 호출
BasicData helloInstance = new BasicData(); // [정적 호출] 컴파일 타임에 확정 — 코드를 바꾸지 않으면 항상 call() helloInstance.call(); // [동적 호출] 런타임에 결정 — methodName 변수에 따라 호출 대상이 바뀜 Class<? extends BasicData> helloClass = helloInstance.getClass(); String methodName = "hello"; // 사용자 입력, 설정값 등으로 얼마든지 교체 가능 Method method = helloClass.getDeclaredMethod(methodName, String.class); Object returnValue = method.invoke(helloInstance, "hi"); System.out.println(returnValue); // "hi hello"
invoke(인스턴스, 인자…) – 메서드 메타데이터는 “어떤 인스턴스의 메서드인지”를 모르기 때문에 호출 시 인스턴스를 명시해야 한다
동적 호출 실전 예시 – 계산기
public class Calculator {
public int add(int a, int b) { return a + b; }
public int sub(int a, int b) { return a - b; }
}
// main
Scanner scanner = new Scanner(System.in);
String methodName = scanner.nextLine(); // "add" 또는 "sub" — 런타임에 결정
int num1 = scanner.nextInt();
int num2 = scanner.nextInt();
Calculator calculator = new Calculator();
Class<? extends Calculator> aClass = calculator.getClass();
Method method = aClass.getMethod(methodName, int.class, int.class);
Object result = method.invoke(calculator, num1, num2);
System.out.println(result);
// 호출 메서드: add → returnValue = 3
// 호출 메서드: sub → returnValue = 2
calculator.add(num1, num2)로 정적으로 호출하면 add는 코드에 영원히 박혀있다. 반면 리플렉션을 쓰면 실행될 때마다 다른 메서드가 호출될 수 있다. 이것이 동적 호출의 본질이다
필드 탐색과 값 변경
필드 메타데이터 조회
Class<BasicData> helloClass = BasicData.class; // public 필드만 (상속 포함) Field[] fields = helloClass.getFields(); // 모든 필드 (접근 제어자 무관, 상속 제외) Field[] declaredFields = helloClass.getDeclaredFields();
private 필드 값 변경
public class User {
private String id;
private String name; // private!
private Integer age;
// ... 생성자, getter/setter, toString
}
User user = new User("id1", "userA", 20);
System.out.println("기존 이름 = " + user.getName()); // userA
Class<? extends User> aClass = user.getClass();
Field nameField = aClass.getDeclaredField("name"); // private 필드 탐색
nameField.setAccessible(true); // ← private 접근 허용 (핵심!)
nameField.set(user, "userB"); // 값 변경
System.out.println("변경된 이름 = " + user.getName()); // userB
setAccessible(true)는 Field뿐 아니라 Method에도 동일하게 적용된다. private 메서드도 이 방법으로 호출 가능하다
주의
private 접근 제어자를 우회하는 것은 캡슐화 원칙 위반이다. 클래스 내부 구조(필드 이름 등)가 바뀌면 리플렉션 코드는 컴파일 에러 없이 런타임에 조용히 깨진다. 비즈니스 로직에서는 getter/setter를 사용하자
리플렉션 활용 예제 — null → 기본값 변환 유틸
리플렉션을 써야 하는 상황의 전형적인 예이다. “null을 절대 저장하면 안된다”는 규칙이 있고, 대상 클래스가 수십 개라면 어떻게 할까?
기존 방식 – 반복의 지옥
// User, Team, Order, Cart, Delivery ... 클래스마다 이 코드를 반복?
if (user.getId() == null) user.setId("");
if (user.getName() == null) user.setName("");
if (user.getAge() == null) user.setAge(0);
if (team.getId() == null) team.setId("");
if (team.getName() == null) team.setName("");
// ...
클래스 50개 x 필드 10개 = if문 500개. 현실적으로 불가능하다
리플렉션으로 해결 – 유틸 하나로 끝
public class FieldUtil {
public static void nullFieldToDefault(Object target) throws IllegalAccessException {
Class<?> aClass = target.getClass();
Field[] declaredFields = aClass.getDeclaredFields();
for (Field field : declaredFields) {
field.setAccessible(true);
if (field.get(target) != null) continue; // 값이 있으면 패스
if (field.getType() == String.class) {
field.set(target, "");
} else if (field.getType() == Integer.class) {
field.set(target, 0);
}
}
}
}
User user = new User("id1", null, null);
Team team = new Team("team1", null);
FieldUtil.nullFieldToDefault(user);
FieldUtil.nullFieldToDefault(team);
System.out.println(user); // User{id='id1', name='', age=0}
System.out.println(team); // Team{id='team1', name=''}
Object target 하나로 세상의 모든 클래스를 처리한다. Order, Cart, Delivery도 한 줄이면 된다. 리플렉션은 이런 공통 유틸리티나 프레임워크/라이브러리 레이어에서 빛을 발한다
생성자 탐색과 객체 생성
생성자 탐색
Class<?> aClass = Class.forName("reflection.data.BasicData");
// public 생성자만
Constructor<?>[] constructors = aClass.getConstructors();
// 모든 생성자 (private 포함)
Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
생성자는 상속되지 않는다. 따라서 getConstructors()도 상속 개념 없이 해당 클래스의 public 생성자만 반환하다
동적 객체 생성
Class<?> aClass = Class.forName("reflection.data.BasicData");
// String 매개변수를 받는 private 생성자 탐색
Constructor<?> constructor = aClass.getDeclaredConstructor(String.class);
constructor.setAccessible(true); // private 생성자 접근 허용
Object instance = constructor.newInstance("hello"); // 동적 객체 생성
System.out.println(instance); // reflection.data.BasicData@...
// 생성한 인스턴스의 메서드도 동적 호출
Method method = aClass.getDeclaredMethod("call");
method.invoke(instance);
BasicData.BasicData: hello
BasicData.call
이 코드 어디에도 BasicData 타입이 직접 등장하지 않는다. 클래스 탐색, 인스턴스 생성, 메서드 호출까지 모두 문자열 기반으로 완전히 동적으로 처리된다
Spring의 DI 컨테이너가 @Component 붙은 내 클래스를 스캔하고 대신 생성해주는 것, 바로 이 원리이다
정리 및 주의 사항
API 요약
| 대상 | 전체 public (상속 포함) | 내 것만 (접근 제어자 무관) |
| 메서드 | getMethods() | getDeclaredMethods() |
| 필드 | getFields() | getDeclaredFields() |
| 생성자 | getConstructors() | getDeclaredConstructors() |
- private 멤버에 접근하려면 반드시
setAccessible(true)호출 - 동적 호출 시 다양한 체크 예외 발생 (
NoSuchMethodException,InvocationTargetException,IllegalAccessException등)
리플렉션의 단점
- 컴파일 타임 안전성 없음: 필드/메서드 이름을 문자열로 다루기 때문에 오타가 있어도 컴파일러가 잡지 못한다. 런타임에 예외로 터진다
- 캡슐화 훼손 가능성: private 멤버를 무분별하게 접근하면 OOP 설계 원칙이 무너진다
- 가독성 저하:
method,invoke(instance, args)는instance.method(args)보다 훨씬 읽기 어렵다 - 성능 오버헤드: 직접 호출보다 느리다. 고빈도 경로에서는 주의가 필요하다