Java Reflection API

앵우·2025년 10월 3일
post-thumbnail

Guide to Java Reflection -Baeldung 를 통해 자바 리플렉션에 대해 학습한 글입니다.

Java Reflection

자바의 Reflection은 런타임시에 동적으로 객체를 생성하고 메서드를 호출할 수 있는 방법이다.

  • 클래스, 인터페이스, 필드, 메서드를 검사하거나 수정할 수 있다.
  • 컴파일 타임에 해당 속성의 이름을 모를 때 유용하다.
  • 새로운 객체를 인스턴스화하고 메서드를 호출하고, 필드값을 가져오거나 설정할 수 있다.

Java Reflection API는 다음 임포트문을 통해서 사용할 수 있다.

import java.lang.reflect.*;

활용 사례

어떻게 Java Reflection을 활용할 수 있을까?

예를 들어, 데이터베이스 테이블에 학생 데이터를 저장하는 테이블인 tblstudent_data가 있다고 해보자. tbl 접두사를 붙여 일관성을 유지했다.

그리고 학생 데이터를 담는 객체를 Student 혹은 StudentData로 정의할 수 있다.

이제 CRUD 작업을 수행할 때, Create 작업은 객체 하나만 매개변수로 받도록 하나의 진입점을 만들 수 있다.

Reflection을 사용하면 객체 이름과 필드 이름을 런타임에 가져올 수 있으므로 이를 이용해 객체 필드와 데이터베이스 테이블 필드를 자동으로 매핑하고 필드 값을 적절한 DB 컬럼에 할당할 수 있다.

실제 사례

Spring 프레임워크의 DI, AOP 그리고 Hibernate, Jackson 등에서 Reflection을 사용한다.

장단점

Reflection은 개념 자체로 런타임시에 객체에 접근할 수 있다는 장점이 있으나, 런타임시에 동작하므로 성능 오버헤드가 존재한다는 치명적인 단점이 있다.

또한 CheckedException 발생으로 인한 예외 처리를 해주어야 하고, private 필드 접근 등으로 객체의 은닉셩이 깨져 캡슐화가 위반될 수 있다.

기능과 예시 코드

Java Reflection API에서 어떤 기능들을 제공하는지 알아보자.

모든 소스 코드는 Guide to Java Reflection -Baeldung 를 참고하였다.

👉🏻 전체 소스 코드

기본적으로 프로젝트 구조는 다음과 같다.

getClass

  • 인스턴스의 클래스, 메서드, 필드 정보에 접근할 수 있다.
  • 객체의 런타임 클래스 표현을 반환한다.
  • 반환된 클래스 객체는 클래스 정보에 접근하기 위한 메서드를 제공한다.
public class Person {
    private String name;
    private int age;
}
class PersonTest {
    @Test
    public void givenObject_whenGetsFieldNamesAtRuntime_thenCorrect() {
        Object person = new Person();
        Field[] fields = person.getClass().getDeclaredFields();

        List<String> actualFieldNames = getFieldNames(fields);

        assertTrue(Arrays.asList("name", "age")
                .containsAll(actualFieldNames));
    }

    private static List<String> getFieldNames(Field[] fields) {
        List<String> fieldNames = new ArrayList<>();
        for (Field field : fields)
            fieldNames.add(field.getName());
        return fieldNames;
    }
  
}

클래스 이름 가져오기: getSimpleName(), getName(), getCanonicalName()

getSimpleName()는 선언에 나타난 객체의 기본 이름 반환하고, 나머지 두 메서드는 패키지 선언을 포함한 정규화된 클래스 이름을 반환한다.

    @Test
    public void givenObject_whenGetsClassName_thenCorrect() {
        Object goat = new Goat("Goat");
        Class<?> clazz = goat.getClass();

        assertEquals("Goat", clazz.getSimpleName());
        assertEquals("ch9.Goat", clazz.getName());
        assertEquals("ch9.Goat", clazz.getCanonicalName());
    }

forName()

패키지 정보가 포함되어야 한다. 그렇지 않으면 ClassNotException이 발생한다.

    @Test
    public void givenClassName_whenCreatesObject_thenCorrect() throws ClassNotFoundException{
        Class<?> clazz = Class.forName("ch9.Goat");

        assertEquals("Goat", clazz.getSimpleName());
        assertEquals("ch9.Goat", clazz.getName());
        assertEquals("ch9.Goat", clazz.getCanonicalName());
    }

클래스 접근지정자: getModifiers

  • 클래스에 사용된 접근 제어자를 확인할 수 있다.
  • 해당 메서드는 Integer를 반환하며, 각 지정자는 비트 플래그(flag bit)로 표현되어 켜져 있거나 꺼져 있다.
  • java.lang.reflect.Modifier 클래스는 이러한 정수 값을 분석해서 특정 지정자가 존재하는지 여부를 확인할 수 있는 여러 정적 메서드를 제공한다.
    예: Modifier.isPublic(mods), Modifier.isPrivate(mods), Modifier.isFinal(mods)
  • 프로젝트에서 가져오는 라이브러리 jar에 있는 모든 클래스의 지정자를 검사할 수 있다.
    @Test
    public void givenClass_whenRecognisesModifiers_thenCorrect() throws ClassNotFoundException{
        Class<?> goatClass = Class.forName("ch9.Goat");
        Class<?> animalClass = Class.forName("ch9.Animal");

        int goatMods = goatClass.getModifiers();
        int animalMods = animalClass.getModifiers();

        assertTrue(Modifier.isPublic(goatMods));
        assertTrue(Modifier.isAbstract(animalMods));
    }

조상클래스: getSuperclass()

    @Test
    public void givenClass_whenGetsSuperClass_thenCorrect() {
        Goat goat = new Goat("goat");
        String str = "any string";

        Class<?> goatClass = goat.getClass();
        Class<?> goatSuperClass = goatClass.getSuperclass();

        assertEquals("Animal", goatSuperClass.getSimpleName());
        assertEquals("Object", str.getClass().getSuperclass().getSimpleName()); // String 객체의 슈퍼 클래스
    }

구현한 인터페이스 목록 가져오기: getInterfaces()

    @Test
    public void givenClass_whenGetsImplementedInterfaces_thenCorrect() throws ClassNotFoundException{
        Class<?> goatClass = Class.forName("ch9.Goat");
        Class<?> animalClass = Class.forName("ch9.Animal");

        Class<?>[] goatInterfaces = goatClass.getInterfaces();
        Class<?>[] animalInterfaces = animalClass.getInterfaces();

        assertEquals(1, goatInterfaces.length);
        assertEquals(1, animalInterfaces.length);
        assertEquals("Locomotion", goatInterfaces[0].getSimpleName());
        assertEquals("Eating", animalInterfaces[0].getSimpleName());
    }
  • 클래스가 implements 키워드로 구현되었다고 명시적으로 선언한 인터페이스만 반환된 배열에 나타난다.
  • 슈퍼클래스가 해당 인터페이스를 구현하여 클래스가 인터페이스 메서드를 구현하더라도, 서브클래스가 implements 키워드로 해당 인터페이스를 직접 선언하지 않으면 해당 인터페이스는 배열에 나타나지 않는다.

생성자, 메서드 및 필드:getConstructors(), getDeclaredFields(), getFieldNames(fields), getDeclaredMethods(), getMethodNames(methods)

객체의 생성자, 메서드, 필드를 검사할 수 있다.

    @Test
    public void givenClass_whenGetsConstructor_thenCorrect() throws ClassNotFoundException{
        Class<?> goatClass = Class.forName("ch9.Goat");

        Constructor<?>[] constructors = goatClass.getConstructors();

        assertEquals(1, constructors.length);
        assertEquals("ch9.Goat", constructors[0].getName());
    }

    @Test
    public void givenClass_whenGetsFields_thenCorrect() throws ClassNotFoundException{
        Class<?> animalClass = Class.forName("ch9.Animal");
        Field[] fields = animalClass.getDeclaredFields();

        List<String> actualFields = getFieldNames(fields);

        assertEquals(2, actualFields.size());
        assertTrue(actualFields.containsAll(Arrays.asList("name", "CATEGORY")));
    }

    @Test
    public void givenClass_whenGetsMethods_thenCorrect() throws ClassNotFoundException{
        Class<?> animalClass = Class.forName("ch9.Animal");
        Method[] methods = animalClass.getDeclaredMethods();
        List<String> actualMethods = getMethodNames(methods);

        assertEquals(3, actualMethods.size());
        assertTrue(actualMethods.containsAll(Arrays.asList("getName",
                "setName", "getSound")));
    }

    private static List<String> getFieldNames(Field[] fields) {
        List<String> fieldNames = new ArrayList<>();
        for (Field field : fields)
            fieldNames.add(field.getName());
        return fieldNames;
    }

	// Method 객체 배열에서 메서드 이름을 검색하는 헬퍼 메서드
    private static List<String> getMethodNames(Method[] methods) {
        List<String> methodNames = new ArrayList<>();
        for (Method method : methods)
            methodNames.add(method.getName());
        return methodNames;
    }

생성자 검사

  • java.lang.reflect.Constructor 클래스를 통해 모든 클래스의 생성자를 검사하고 런타임에 클래스 객체를 생성할 수 있다.
  • 특정 생성자를 검색할 수 있다.

생성자의 개수 검사

    @Test
    public void givenClass_whenGetsAllConstructors_thenCorrect() throws ClassNotFoundException {
        Class<?> birdClass = Class.forName("ch9.Bird");
        Constructor<?>[] constructors = birdClass.getConstructors();

        assertEquals(3, constructors.length);
    }

매개변수 유형으로 생성자 검색

    /*
    선언된 순서대로 생성자의 매개변수 클래스 유형을 전달하여 Bird 클래스의 각 생성자를 검색
    - NoSuchMethodException이 발생하고 주어진 순서대로 주어진 매개변수 유형을 갖는 생성자가 존재하지 않으면 테스트가 자동으로 실패하므로 assertion이 필요하지 않다.
     */
    @Test
    public void givenClass_whenGetsEachConstructorByParamTypes_thenCorrect() throws ClassNotFoundException, NoSuchMethodException {
        Class<?> birdClass = Class.forName("ch9.Bird");

        Constructor<?> cons1 = birdClass.getConstructor();
        Constructor<?> cons2 = birdClass.getConstructor(String.class);
        Constructor<?> cons3 = birdClass.getConstructor(String.class, boolean.class);
    }

매개변수를 제공하면서 런타임에 객체 인스턴화

    @Test
    public void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() throws Exception{
        Class<?> birdClass = Class.forName("ch9.Bird");
        Constructor<?> cons1 = birdClass.getConstructor();
        Constructor<?> cons2 = birdClass.getConstructor(String.class);
        Constructor<?> cons3 = birdClass.getConstructor(String.class,
                boolean.class);

        // 생성자 클래스의 newInstance 메서드를 호출하고 필요한 매개변수를 선언된 순서대로 전달하여 클래스 객체를 인스턴스화한다.
        Bird bird1 = (Bird) cons1.newInstance();
        Bird bird2 = (Bird) cons2.newInstance("Weaver bird");
        Bird bird3 = (Bird) cons3.newInstance("dove", true);

        assertEquals("bird", bird1.getName());
        assertEquals("Weaver bird", bird2.getName());
        assertEquals("dove", bird3.getName());

        assertFalse(bird1.walks());
        assertTrue(bird3.walks());
    }

필드 검사

런타임에 필드 값을 가져오고 설정할 수 있다.

  • getFields()
    /*
    getFields()
    - 해당 클래스의 접근 가능한 모든 public 필드를 반환
    - 클래스와 모든 조상 클래스의 모든 public 필드를 반환
     */
    @Test
    public void givenClass_whenGetsPublicFields_thenCorrect() throws ClassNotFoundException{
        Class<?> birdClass = Class.forName("ch9.Bird");
        Field[] fields = birdClass.getFields();

        assertEquals(1, fields.length);
        assertEquals("CATEGORY", fields[0].getName());
    }
  • getField(String fieldName)
    // getField() - 단 하나의 Field 객체만 반환(조상 클래스에 선언되었지만 자식 클래스에는 선언되지 않은 private 필드에는 접근 불가)
    @Test
    public void givenClass_whenGetsPublicFieldByName_thenCorrect() throws ClassNotFoundException, NoSuchFieldException{
        Class<?> birdClass = Class.forName("ch9.Bird");
        Field field = birdClass.getField("CATEGORY");

        assertEquals("CATEGORY", field.getName());
    }
  • getDeclaredFields()
    // 클래스에 선언된 필드를 검사할 수 있다.
    @Test
    public void givenClass_whenGetsDeclaredFields_thenCorrect() throws ClassNotFoundException{
        Class<?> birdClass = Class.forName("ch9.Bird");
        Field[] fields = birdClass.getDeclaredFields();

        assertEquals(1, fields.length);
        assertEquals("walks", fields[0].getName());
    }
  • getDeclaredFields()
    // 필드 이름을 알고 있는 경우
    // 필드 이름을 잘못 입력하거나 존재하지 않는 필드를 입력하면 NoSuchFieldException 발생
    @Test
    public void givenClass_whenGetsFieldsByName_thenCorrect() throws ClassNotFoundException, NoSuchFieldException{
        Class<?> birdClass = Class.forName("ch9.Bird");
        Field field = birdClass.getDeclaredField("walks");

        assertEquals("walks", field.getName());
    }

필드 유형 가져오기

    @Test
    public void givenClassField_whenGetsType_thenCorrect() throws ClassNotFoundException, NoSuchFieldException{
        Field field = Class.forName("ch9.Bird")
                .getDeclaredField("walks");
        Class<?> fieldClass = field.getType();

        assertEquals("boolean", fieldClass.getSimpleName());
    }

필드값에 접근하고 수정하기

필드값을 가져오려면, 먼저 Field 객체에서 setAccessible 메서드를 호출하여 접근 가능하도록 설정하고 boolean값 true를 전달한다.

    public void givenClassField_whenSetsAndGetsValue_thenCorrect() throws Exception{
        Class<?> birdClass = Class.forName("ch9.Bird");
        Bird bird = (Bird) birdClass.getConstructor().newInstance();
        Field field = birdClass.getDeclaredField("walks");
        field.setAccessible(true);

        assertFalse(field.getBoolean(bird));
        assertFalse(bird.walks());

        field.set(bird, true);

        assertTrue(field.getBoolean(bird));
        assertTrue(bird.walks());
    }

public static으로 선언하면 해당 객체를 포함하는 클래스의 인스턴스가 필요하지 않다. null을 대신 전달해도 필드의 기본값을 얻을 수 있다.

    @Test
    public void givenClassField_whenGetsAndSetsWithNull_thenCorrect() throws Exception{
        Class<?> birdClass = Class.forName("ch9.Bird");
        Field field = birdClass.getField("CATEGORY");
        field.setAccessible(true);

        assertEquals("domestic", field.get(null));
    }

메서드 검사

  • 런타임에 메서드를 호출하고 필요한 매개변수를 전달할 수 있다.
  • 각 메서드의 매개변수 유형을 지정하여 오버로드된 메서드를 호출할 수 있다.

getMethods()

  • 클래스와 상위 클래스의 모든 public 메서드 배열을 반환
  • 예: toString(), hashCode, notifyAll과 같은 java.lang.Object 클래스의 공개 메서드를 얻기
    @Test
    public void givenClass_whenGetsAllPublicMethods_thenCorrect() throws Exception{
        Class<?> birdClass = Class.forName("ch9.Bird");
        Method[] methods = birdClass.getMethods();
        List<String> methodNames = getMethodNames(methods);

        assertTrue(methodNames.containsAll(Arrays
                .asList("equals", "notifyAll", "hashCode",
                        "walks", "eats", "toString")));
    }

getDeclaredMethods()

  • 해당 클래스에 선언된 것만 얻기
    @Test
    public void givenClass_whenGetsOnlyDeclaredMethods_thenCorrect() throws ClassNotFoundException{
        Class<?> birdClass = Class.forName("ch9.Bird");
        List<String> actualMethodNames
                = getMethodNames(birdClass.getDeclaredMethods());

        List<String> expectedMethodNames = Arrays
                .asList("setWalks", "walks", "getSound", "eats");

        assertEquals(expectedMethodNames.size(), actualMethodNames.size());
        assertTrue(expectedMethodNames.containsAll(actualMethodNames));
        assertTrue(actualMethodNames.containsAll(expectedMethodNames));
    }

getDeclaredMethod(String methodName)

  • 메서드 이름을 통해 단일 Method 객체를 반환
  • 인자가 없는 메서드 검색: getDeclaredMethod(String methodName)
  • 인자가 있는 메서드 검색: getDeclaredMethod(String methodName, 매개변수타입...)
    @Test
    public void givenMethodName_whenGetsMethod_thenCorrect() throws Exception {
        Bird bird = new Bird();
        Method walksMethod = bird.getClass().getDeclaredMethod("walks");
        Method setWalksMethod = bird.getClass().getDeclaredMethod("setWalks", boolean.class);

        assertTrue(walksMethod.canAccess(bird));
        assertTrue(setWalksMethod.canAccess(bird));
    }

런타임에 메서드 호출

예: Bird클래스의 walks 속성의 기본값은 false -> true로 바꾸기 (setWalks 메서드 호출을 통해)

    @Test
    public void givenMethod_whenInvokes_thenCorrect() throws Exception{
        Class<?> birdClass = Class.forName("ch9.Bird");
        Bird bird = (Bird) birdClass.getConstructor().newInstance();
        Method setWalksMethod = birdClass.getDeclaredMethod("setWalks", boolean.class);
        Method walksMethod = birdClass.getDeclaredMethod("walks");
        boolean walks = (boolean) walksMethod.invoke(bird);

        assertFalse(walks);
        assertFalse(bird.walks());

        setWalksMethod.invoke(bird, true);

        boolean walks2 = (boolean) walksMethod.invoke(bird);
        assertTrue(walks2);
        assertTrue(bird.walks());
    }

0개의 댓글