Java 리플렉션 API 사용해보기

이상민·2021년 10월 25일
1
post-thumbnail

클래스, 인터페이스, 필드, 메소드의 런타임 애트리뷰트를 확인하거나 수정할 수 있게 하는 기능

  • 단순히 확인하는 것을 넘어서, 객체를 생성하고, 메소드를 호출하고, 필드를 수정할 수 있다

  • 외부 의존성이 필요하지 않다. 자바는 기본적으로 java.lang.reflect 패키지의 클래스들로 리플렉션 관련 기능들을 제공한다


1. 리플렉션 패키지

  • 기본 내장 패키지로 리플렉션 관련된 다양한 클래스를 제공한다. 예를 들어 Field 클래스가 있다.

  • Field : 클래스나 인터페이스의 필드 한개에 대한 정보와 동적 접근을 제공하는 클래스. 정적, 인스턴스 변수 모두 리플렉트 될 수 있다

public class Person {
    private String name;
    private int age;
}
public class Reflection {

    @Test
    public void testGetDeclaredFields() {
        Object person = new Person();
        // 객체의 필드 정보를 가저온다
        Field[] fields = person.getClass().getDeclaredFields();

        List<String> actualFieldNames = getFieldNames(fields);

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

    private List<String> getFieldNames(Field[] fields) {
        List<String> fieldNames = new ArrayList<>();
        // 필드 정보에서 필드명을 가져온다 
        for (Field field : fields) {
            fieldNames.add(field.getName());
        }
        return fieldNames;
    }
    
}

// output
[name, age]
  • 이외에도 Field와 같이 AccessibleOjbect를 상속하는 Contructor, Method 클래스가 존재한다. 리플렉션 클래스들에 대한 자세한 설명은 아래에서 한다

2. Class 클래스

  • Class는 리플렉션 클래스들을 반환하는 메소드들을 제공한다. 리플렉션 API를 활용하기위해 필수적이다

  • 아래와 같은 객체들이 있을때 다양한 메소드를 사용할 수 있다

public interface Eating {
    String eats();
}
public abstract class Animal implements Eating {

    public static String CATEGORY = "domestic";
    private String name;

    protected abstract String getSound();

    ...
}
public class Goat extends Animal {

    @Override
    protected String getSound() {
        return "bleat";
    }

    @Override
    public String eats() {
        return "grass";
    }

    ...
}

2-1. 클래스 이름 가져오기

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

    assertEquals("Goat", clazz.getSimpleName());
    assertEquals("com.baeldung.reflection.Goat", clazz.getName());
    assertEquals("com.baeldung.reflection.Goat", clazz.getCanonicalName());
}

2-2. 클래스 접근 지정자

@Test
public void givenClass_whenRecognisesModifiers_thenCorrect() {
    // 클래스가 거대할 경우 생성하지 않고 아래처럼 할 수 있다 
    Class<?> goatClass = Class.forName("com.baeldung.reflection.Goat");
    Class<?> animalClass = Class.forName("com.baeldung.reflection.Animal");

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

    assertTrue(Modifier.isPublic(goatMods));
    assertTrue(Modifier.isAbstract(animalMods));
    assertTrue(Modifier.isPublic(animalMods));
}
  • getModifiers()는 정수를 반환한다. 정수는 리눅스의 chmod처럼 접근 지정자의 플래그 비트를 나타낸다

  • java.lang.reflect.Modifier 클래스가 .getModifiers()의 결과값을 분석해주는 Modifier.isPulblic() 정적 메소드를 제공한다

2-3. 패키지 정보

@Test
public void givenClass_whenGetsPackageInfo_thenCorrect() {
    Goat goat = new Goat("goat");
    Class<?> goatClass = goat.getClass();
    Package pkg = goatClass.getPackage();

    assertEquals("com.baeldung.reflection", pkg.getName());
}

2-4. 부모 클래스

@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());
}

2-5. 구현 인터페이스

@Test
public void givenClass_whenGetsImplementedInterfaces_thenCorrect(){
    Class<?> goatClass = Class.forName("com.baeldung.reflection.Goat");
    Class<?> animalClass = Class.forName("com.baeldung.reflection.Animal");

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

    assertEquals(1, animalInterfaces.length);
    assertEquals("Eating", animalInterfaces[0].getSimpleName());
}
  • 클래스 시그니쳐에 명시된 인터페이스만 확인할 수 있다

3. 리플렉션 API

public class Bird extends Animal {
    private boolean walks;

    public Bird() {
        super("bird");
    }

    public Bird(String name, boolean walks) {
        super(name);
        setWalks(walks);
    }

    public Bird(String name) {
        super(name);
    }

    public boolean walks() {
        return walks;
    }

    // standard setters and overridden methods
}

3-1. Constructor 클래스

  • 리플렉션을 통해 생성자를 확인하는 것은 물론, 런타임에 객체를 생성할 수도 있다

  • 복수개 생성자가 존재할시, 시그니쳐가 다른 점을 이용해 특정 생성자를 선택할 수도 있다

  • 객체를 생성하고 클래스를 가져오는 것의 비용이 너무 클 경우, 아래처럼 forName을 통해 Class객체를 생성할 수도 있다

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

    assertEquals(3, constructors.length);
}
  • getConstructor 메소드에 인자를 지정해 특정 생성자를 가져올 수 있다. 일치하는 생성자가 없을시 NoSuchMethodException이 발생한다
@Test
public void givenClass_whenGetsEachConstructorByParamTypes_thenCorrect(){
    Class<?> birdClass = Class.forName("com.baeldung.reflection.Bird");

    Constructor<?> cons1 = birdClass.getConstructor();
    Constructor<?> cons2 = birdClass.getConstructor(String.class);
    Constructor<?> cons3 = birdClass.getConstructor(String.class, boolean.class);
}
  • 아래처럼 Constructor 객체를 통해 인스턴스화 할 수 있다
@Test
public void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() {
    Class<?> birdClass = Class.forName("com.baeldung.reflection.Bird");
    Constructor<?> cons1 = birdClass.getConstructor();
    Constructor<?> cons2 = birdClass.getConstructor(String.class);
    Constructor<?> cons3 = birdClass.getConstructor(String.class,
      boolean.class);

    // 생성자 객체를 통해 생성할 수 있다 
    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());
}
  • setAccessible을 설정하면 private 생성자에 접근할 수도 있다
public class Person {
    private String name;
    private int age;

    private Person() {}
}

public class Reflection {

    @Test
    public void testGetDeclaredFields() {
        Class<?> personClass = Class.forName("Person");
        Constructor<?> constructor = personClass.getDeclaredConstructor();
        constructor.setAccessible(true);

        Person person =  (Person) constructor.newInstance();
    }

}

3-2. Field 클래스

  • Class에서는 모든 필드를 가져오는걸 보여줬지만, 이름으로 특정 필드를 가져올 수도 있다
@Test
public void givenClass_whenGetsPublicFieldByName_thenCorrect() {
    Class<?> birdClass = Class.forName("com.baeldung.reflection.Bird");
    Field field = birdClass.getField("CATEGORY");

    assertEquals("CATEGORY", field.getName());
}
  • getDeclaredFields를 사용하면 private 필드들도 가져올 수 있다
@Test
public void givenClass_whenGetsDeclaredFields_thenCorrect(){
    Class<?> birdClass = Class.forName("com.baeldung.reflection.Bird");
    Field[] fields = birdClass.getDeclaredFields();

    assertEquals(1, fields.length);
    assertEquals("walks", fields[0].getName());
}
  • 필드의 타입도 가져올 수 있다
@Test
public void givenClassField_whenGetsType_thenCorrect() {
    Field field = Class.forName("com.baeldung.reflection.Bird")
      .getDeclaredField("walks");
    Class<?> fieldClass = field.getType();

    assertEquals("boolean", fieldClass.getSimpleName());
}
  • 필드 값에 접근과 변경도 가능하다
@Test
public void givenClassField_whenSetsAndGetsValue_thenCorrect() {
    Class<?> birdClass = Class.forName("com.baeldung.reflection.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 필드의 경우 인스턴스가 없어도 된다
@Test
public void givenClassField_whenGetsAndSetsWithNull_thenCorrect(){
    Class<?> birdClass = Class.forName("com.baeldung.reflection.Bird");
    Field field = birdClass.getField("CATEGORY");
    field.setAccessible(true);

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

3-3. Method 클래스

  • 자바 리플렉션을 활용하면 메소드를 런타임에 호출할 수도 있다. 생성자를 특정했던 것처럼, 오버로딩된 메소드를 특정할 수 있다

  • getMethods는 클래스와 부모 클래스의 모든 public 메소드를 반환한다

  • getDeclaredMethods는 클래스의 public 메소드만 반환한다

@Test
public void givenClass_whenGetsAllPublicMethods_thenCorrect(){
    Class<?> birdClass = Class.forName("com.baeldung.reflection.Bird");
    Method[] methods = birdClass.getMethods();
    List<String> methodNames = getMethodNames(methods);
    
    List<String> expectedMethodNames = Arrays.asList("equals", "notifyAll", "hashCode", "walks", "eats", "toString");

    assertTrue(methodNames.containsAll(expectedMethodNames));
}
@Test
public void givenClass_whenGetsOnlyDeclaredMethods_thenCorrect(){
    Class<?> birdClass = Class.forName("com.baeldung.reflection.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));
}
  • 두 메소드 모두 이름을 통해 특정 메소드를 가져올 수 있다
@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));
}
  • 아래처럼 메소드를 호출할 수 있다

@Test
public void givenMethod_whenInvokes_thenCorrect() {
    Class<?> birdClass = Class.forName("com.baeldung.reflection.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());
}

참고자료

Guide to Java Reflection. (2021). https://www.baeldung.com/java-reflection

profile
편하게 읽기 좋은 단위의 포스트를 추구하는 개발자입니다

0개의 댓글