본문 바로가기
언어 공부/Java

[Java] Reflection

by 희조당 2023. 1. 11.
728x90

🎇 Java

Java를 사용하다 보면 리플렉션(Reflection)이라는 용어를 자주 접한다.

대충 무엇인지는 알겠는데 설명해 주세요! 하면 못 할 것 같아서 정리해 본다😋😋

🪞 리플렉션, Reflection

리플렉션이란? 단어의 의미를 떠올려서, 거울에 반사된 어떤 대상을 제약 없이 사용하는 기술이다.

자세하게 설명하면, 힙 영역에 로드된 Class 타입의 객체를 통해서 인스턴스를 생성하고,

접근 지정자의 제약 없이 인스턴스의 메소드와 필드를 사용할 수 있게 하는 Java API이다.

😮 힙 영역에서 Class 타입 가져오기

힙 영역에 로드된 Class 타입은 다음 3가지 방법으로 가져올 수 있다.

  • 클래스.class 
  • 인스턴스.getClass()
  • Class.forName(클래스명)
public class GetClassTest {
    public static void main(String[] args) throws ClassNotFoundException {
        // 클래스.class로 가져오기
        Class<Pokemon> pokemonClass = Pokemon.class;

        // 인스턴스.getClass()로 가져오기
        Pokemon pokemon = new Pokemon();
        Class<? extends Pokemon> pokemonClass2 = pokemon.getClass();

        // Class.forName(클래스명)로 가져오기
        Class<?> pokemonClass3 = Class.forName("Reflection.Pokemon");

        System.out.println(System.identityHashCode(pokemonClass));
        System.out.println(System.identityHashCode(pokemonClass2));
        System.out.println(System.identityHashCode(pokemonClass3));
    }
}

😋 리플렉션 사용하기

리플렉션을 사용하기 앞서 사용할 객체는 다음과 같다. 포켓몬 짱..!

public class Pokemon {
    public String name;
    protected int stat;
    private String hiddenPassive;

    public Pokemon() {}

    public Pokemon(String name, int stat, String hiddenPassive) {
        this.name = name;
        this.stat = stat;
        this.hiddenPassive = hiddenPassive;
    }

    public void printName(String name) {
        System.out.println("이 포켓몬의 이름은 " + name + "입니다.");
    }

    private void printHidden() {
        System.out.println("히든 패시브는 " + this.hiddenPassive + "입니다.");
    }

    @Override
    public String toString() {
        return "\n포켓몬 이름 : " + name + " 스탯 : " + stat + " 히든 패시브 : " + hiddenPassive;
    }
}

1️⃣ Step 1, 인스턴스 생성하기

앞서 알아본 Class 타입 가져오기를 통해서 어떤 클래스를 리플렉션 할지 정보를 가져와야 한다.

이후 getConstructor() 메서드를 통해서 생성자를 가져오고, newInstance()로 인스턴스를 생성한다.

public class Reflection {
    public static void main(String[] args) throws Exception {
    	// 생성자 가져오기
        Class<? extends Pokemon> pokemonClass = Pokemon.class;
        Arrays.stream(pokemonClass.getConstructors()).forEach(System.out::println);

        // 인스턴스 생성하기
        Constructor<? extends Pokemon> constructor = pokemonClass.getConstructor();
        Pokemon pokemon = constructor.newInstance();
        System.out.println(pokemon);

        // 인스턴스 생성하기 (all)
        Constructor<? extends  Pokemon> constructor2 =
                pokemonClass.getConstructor(String.class, int.class, String.class);
        Pokemon 피카츄 = constructor2.newInstance("피카츄", 50, "감전");
        System.out.println(피카츄);
}

2️⃣ Step 2, 필드와 메소드 사용하기

리플렉션을 통해서 어떤 제약도 없이 필드와 메소드를 사용할 수 있다고 했다.

getDeclaredFields() 메서드와 getDeclaredMethod() 메소드를 통해서 필드와 메소드 정보를 가져와 사용할 수 있다.

메서드의 경우 invoke() 메서드를 통해서 직접 실행시켜 줘야 한다.

public class Reflection {
    public static void main(String[] args) throws Exception {
        Pokemon pokemon2 = new Pokemon("파이리", 100, "화상");
        Class<?> pokemonClass2 = pokemon2.getClass();

        // 리플렉션으로 필드 접근하기
        Field[] fields = pokemonClass2.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            System.out.println(field.get(pokemon2));
        }

        fields[1].set(pokemon2, 90);
        System.out.println(pokemon2);

        // 리플렉션으로 메소드 접근하기
        Method printName = pokemonClass2.getDeclaredMethod("printName", String.class);
        printName.invoke(pokemon2, "꼬부기");

        Method printHidden = pokemonClass2.getDeclaredMethod("printHidden");
        printHidden.setAccessible(true);
        printHidden.invoke(pokemon2);
    }
}

주의할 점은 private로 선언된 필드나 메서드는 setAccessible() 메서드를 통해서 접근 권한을 true로 변경해야 한다.

🤔 왜 사용할까?

딱 보면 OOP를 위한 기술은 아니다, 외부에서도 내부를 볼 수 있기 때문이다. (캡슐화)

그럼에도 사용하는 이유는, 정적인 Java를 보완해 줄 수 있는 강력한 기술이라서 그렇다. 자세하게 알아보자!

1️⃣ 런타임 시점에도 알 수 있다

리플랙션을 사용하면 컴파일 단계에서 할 수 없는 것들을 할 수 있다.

  • 알 수 없는 자원을 동적으로 사용
  • 동적으로 클래스의 요소 검사

이는 컴파일 시점에서 알 수 없는 자원들 즉, 외부 라이브러리를 사용하는 상황에서 유용하다.

2️⃣ 자동 리소스 관리

리플렉션은 Spring과 같은 DI 프레임워크에서 자동으로 객체 생성 및 구성을 관리하는 데 사용된다.

스프링에서 Controller, Repository, Service 같은 컴포넌트들은 단순히 어노테이션만 붙여주면 바로 사용할 수 있다.

해당 클래스를 스프링한테 알려주지 않았는데도 다 알아서 관리 해주는 것도 리플렉션 때문이다.

 

런타임에 해당 어노테이션이 붙은 클래스를 찾은 뒤 리플랙션을 사용해서 인스턴스를 생성한다.

이후 필요한 정보를 주입하고 Bean Factory 즉 스프링에 저장해서 사용할 수 있는 것이다.

3️⃣ 더 나은 테스트 환경

리플렉션을 사용하면 private 또는 protected 접근 지정자를 사용한 필드와 메서드를 사용할 수 있다.

제한되어 있는 자원들을 접근할 수 있어 더 나은 테스트 범위를 허용하는 것이다.

😉 정리

이렇게 보면 엄청 대단한 기술일 수 있지만 캡슐화를 해치고 런타임 단계에서 알 수 있기 때문에 동작의 흐름을 알기는 어렵다. 하지만 동적으로 가능하게 한다는 것은 엄청 의미 있으므로 잘 알아보고 사용하자!😋😋


😋 지극히 개인적인 블로그지만 훈수와 조언은 제 성장에 도움이 됩니다 😋 

 

댓글