본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
9장 : 고급 타입 모델링

리플렉션으로 타입 조사하기

리플렉션은 실행 중에 클래스, 생성자, 메서드, 필드와 애너테이션 정보를 조사합니다.

프레임워크가 사용자 타입을 미리 알 수 없을 때 유용하지만 문자열 이름과 런타임 검사가 늘어 컴파일러의 도움은 줄어듭니다.

공개 멤버와 선언된 멤버를 조회하는 API도 다릅니다.

메서드 호출은 InvocationTargetException 같은 추가 실패 경로를 만듭니다.

이 절에서는 동적 유연성을 좁은 경계에 격리합니다.

일반 메서드 호출은 컴파일러가 이름과 타입을 확인하지만 리플렉션은 이 검사를 실행 시점으로 미룹니다. Method.invoke의 인자와 반환값도 Object 경계를 지나며, 대상 메서드가 던진 예외는 InvocationTargetException에 감싸집니다. 유연성의 비용이 어디에서 생기는지 알고 써야 합니다.


Class 객체가 런타임 타입의 입구다

Type.class, object.getClass, Class.forName으로 얻는 경로가 각각 다릅니다.

문자열 로딩은 클래스 이름 오류와 초기화 시점을 고려합니다.


공개 멤버와 선언 멤버 조회를 구분한다

getMethods는 상속된 public 메서드를 포함하고 getDeclaredMethods는 현재 클래스에 선언된 접근 수준 전체를 봅니다.

예제는 “현재 타입에 선언된 메서드 목록”과 “외부에서 호출할 public 메서드 선택”을 일부러 다른 API로 수행합니다. 두 조회를 같은 것으로 생각하면 private 메서드를 찾지 못하거나 호출 단계에서 접근 오류를 만나게 됩니다.

ReflectionProbe.java
import java.lang.reflect.Method;
import java.util.Arrays;

public class ReflectionProbe {
    static final class Greeter {
        public String greet(String name) { return "Hello, " + name; }
        private void secret() { }
    }

    public static void main(String[] args) throws Exception {
        Class<?> type = Greeter.class;
        System.out.println(type.getSimpleName());
        Arrays.stream(type.getDeclaredMethods())
                .map(Method::getName)
                .sorted()
                .forEach(System.out::println);

        Method greet = type.getMethod("greet", String.class);
        Object result = greet.invoke(new Greeter(), "Java");
        System.out.println(result);
        // type.getMethod("missing"); // 실행 시 NoSuchMethodException
    }
}

메서드 목록은 JVM이 선언 순서를 보장하지 않으므로 예제는 이름을 정렬한 뒤 출력합니다. secret을 발견할 수 있다는 사실과 호출 권한이 있다는 사실은 별개이며, 실제 호출은 public greet(String)만 선택합니다.

Greeter
greet
secret
Hello, Java

getDeclaredMethods는 private secret까지 찾지만 getMethod는 공개 greet만 이름과 매개변수 타입으로 조회합니다.

Class 메타데이터 획득

Type.class, object.getClass, Class.forName으로 얻는 경로가 각각 다릅니다.

이름·매개변수로 공개 메서드 선택

getMethods는 상속된 public 메서드를 포함하고 getDeclaredMethods는 현재 클래스에 선언된 접근 수준 전체를 봅니다.

invoke 결과와 반사 예외를 명시적으로 처리

이름만으로 메서드를 고르면 오버로드를 구분하지 못합니다.


호출 전에 매개변수와 반환 타입을 확인한다

이름만으로 메서드를 고르면 오버로드를 구분하지 못합니다.

정확한 타입 배열로 찾고 호출 결과를 명시 타입으로 검증합니다.

Class를 얻는 세 경로와 공개·선언 멤버 조회의 차이를 짧은 조각으로 다시 대조합니다.

비교 1
Class<?> fromLiteral = Greeter.class;       // 초기화 없이 명시 타입
Class<?> fromObject = new Greeter().getClass(); // 실제 객체 타입
Class<?> fromName = Class.forName("ReflectionProbe$Greeter");
비교 2
Method[] publicIncludingParents = fromLiteral.getMethods();
Method[] declaredHere = fromLiteral.getDeclaredMethods();
비교 3
// getDeclaredMethod("secret").invoke(...)는 접근 검사에 실패할 수 있음

리플렉션 실패를 도메인 오류로 번역한다

프레임워크 내부의 조사 예외를 그대로 퍼뜨리기보다 어떤 설정이나 선언이 잘못됐는지 문맥을 붙여 전달합니다.


접근 우회는 모듈 경계와 충돌할 수 있다

private 멤버를 강제로 여는 코드는 강한 캡슐화와 보안 정책에 영향을 받습니다.

공개 생성자와 메서드를 우선 사용합니다.


반복 조회 결과는 필요하면 캐시한다

리플렉션 메타데이터 탐색은 직접 호출보다 비용과 실패 지점이 많습니다.

프레임워크 경계에서 한 번 확인하고 명시적 모델로 변환하는 편이 좋습니다.

getMethod("greet", Integer.class)로 매개변수 타입만 바꿔 보세요. 이름이 존재해도 정확한 시그니처가 아니면 NoSuchMethodException이 납니다. 이어서 greet 내부에서 예외를 던져 invoke가 만든 예외의 getCause()를 확인하면 반사 API의 실패와 대상 코드의 실패를 구분할 수 있습니다.

설정 로딩이나 의존성 주입처럼 동적 탐색이 꼭 필요한 계층에서는 시작 시 한 번 검증한 결과를 명시적 호출 객체로 바꾸어 두는 편이 좋습니다. 업무 코드 전체에 문자열 메서드 이름을 흩뿌리면 리팩터링과 오류 진단이 함께 어려워집니다.


리플렉션은 타입 정보를 없애는 도구가 아니라 동적 탐색이 필요한 한 경계에서 정보를 읽어 명시적 모델로 되돌리는 도구입니다.