리플렉션(Reflection)

리플렉션이 필요한 이유

커맨드 패턴으로 서블릿을 구현할 때 흔히 부딪히는 두 가지 불편함이 있다

  • 클래스 하나 = 기능 하다: 단순히 기능을 추가할 때마다 클래스를 새로 만들어야 한다
  • 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 클래스로 실제 의미를 해석한다

  • 접근 제어자: publicprotecteddefault(package-private)private
  • 비접근 제어자: staticfinalabstractsynchronizedvolatile 등

메서드 탐색과 동적 호출

메서드 메타데이터 조회

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)보다 훨씬 읽기 어렵다
  • 성능 오버헤드: 직접 호출보다 느리다. 고빈도 경로에서는 주의가 필요하다

출처 – 김영한 님의 강의 중 김영한의 실전 자바 – 고급 2편, I/O, 네트워크, 리플렉션