Reflaction? 그게 뭔데?

노혁·2023년 7월 3일
1
post-thumbnail

💡 구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API

Reflection 이란?

리플렉션은 힙 영역에 로드된 Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있도록 지원하고, 인스턴스의 필드와 메소드를 접근 제어자와 상관 없이 사용할 수 있도록 지원하는 API이다.

사용 방법

리플렉션을 사용하기에 앞서, 힙 영역에 로드된 클래스 타입의 객체를 가져와야 한다. 가져오는 방법으로는

  • 클래스.class로 가져오기
  • 인스턴스.getClass()로 가져오기
  • Class.forName(”클래스명”)으로 가져오기

가 있습니다.

public class Member {

    private String name;

    protected int age;

    public Member() {
    }

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void speak(String message) {
        System.out.println(message);
    }

    private void secret() {
        System.out.println("비밀번호는 1234입니다.");
    }

    @Override
    public String toString() {
        return "Member{" +
            "name='" + name + '\'' +
            ", age=" + age + '\'' 
            '}';
    }
}

public class Main {

    public static void main(String[] args) throws ClassNotFoundException {
        Class<Member> memberClass = Member.class;
        System.out.println(System.identityHashCode(memberClass));

        Member member = new Member("노혁", 19);
        Class<? extends Member> memberClass2 = member.getClass();
        System.out.println(System.identityHashCode(memberClass2));

        Class<?> memberClass3 = Class.forName("{패키지명}.Member");
        System.out.println(System.identityHashCode(memberClass3));
    }
}

// 실행 결과
1740000325
1740000325
1740000325

3가지 방법으로 가져온 인스턴스는 모두 같은 해시 값인것을 확인 할 수 있다.
가져온 Class 타입을 통해 해당 클래스의 인스턴스를 생성할 수도 있고, 인스턴스의 필드와 메서드를 접근 제어자와 상관 없이 사용할 수 있게 되었다. 먼저 해당 클래스의 인스턴스를 생성해 보자.

해시값이란 객체를 구분할 수 있는 고유값을 나타낸다.

public class Main {

    public static void main(String[] args) throws Exception {
        // Member의 모든 생성자 출력
        Member member = new Member();
        Class<? extends Member> memberClass = member.getClass();
        Arrays.stream(memberClass.getConstructors()).forEach(System.out::println);

        // Member의 기본 생성자를 통한 인스턴스 생성
        Constructor<? extends Member> constructor = memberClass.getConstructor();
        Member member2 = constructor.newInstance();
        System.out.println("member2 = " + member2);

        // Member의 다른 생성자를 통한 인스턴스 생성
        Constructor<? extends Member> fullConstructor =
            memberClass.getConstructor(String.class, int.class);
        Member member3 = fullConstructor.newInstance("노혁", 19);
        System.out.println("member3 = " + member3);
    }
}

// 실행 결과
public Member()
public Member(java.lang.String,int)
member2 = Member{name='null', age=0}
member3 = Member{name='노혁', age=19}

getConstructor()를 통해서 생성자를 가져오고, newInstance()를 통해서 Member 인스턴스를 동적으로 생성해 줄 수 있다.

마지막으로 인스턴스의 필드와 메소드를 접근 제어자와 상관없이 접근하여 사용해 보자.

public class Main {

    public static void main(String[] args) throws Exception {
        Member member = new Member("노혁", 19);
        Class<? extends Member> memberClass = member.getClass();

        // 필드 접근
        Field[] fields = memberClass.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            System.out.println(field.get(member));
        }
        fields[0].set(member, "노혁2");
        System.out.println(member);

        // 메소드 접근
        Method speakMethod = memberClass.getDeclaredMethod("speak", String.class);
        speakMethod.invoke(member, "리플렉션 테스트");

        Method secretMethod = memberClass.getDeclaredMethod("secret");
        secretMethod.setAccessible(true);
        secretMethod.invoke(member);
    }
}

// 실행 결과
노혁
19
Member{name='노혁2', age=19}
리플렉션 테스트
비밀번호는 1234입니다.

getDeclaredFileds() 를 통해 클래스의 인스턴스 변수를 모두 가져올 수 있고, get() 을 통해 필드 값을 반환받을 수 있고, set() 을 통해 필드 값을 수정할 수 있는 것을 알 수 있다. 이때 주의할 점은 private 접근 제어자가 있는 필드에 접근할 때는 setAccessible() 의 인자를 true로 넘겨주어야 한다.

메소드도 getDeclaredMethod() 를 통해 메소드를 가져올 수 있다. 이때 메소드의 이름과 파라미터의 타입을 같이 인자로 넘겨줘야 한다. 마찬가지로 private 접근 제어자가 있는 메소드에 접근할 때는 setAccessible() 의 인자를 true로 설정해야 한다. 마지막으로 invoke() 메소드를 통해 리플렉션 API로 얻어 온 메소드를 호출할 수 있다.

장단점

장점

  • 접근 제어자를 무시할 수 있습니다. private 필드나 메소드에 접근이 가능합니다.
  • 런타임에 객체의 타입 정보를 알 수 있기 때문에 유연한 프로그래밍이 가능합니다.

단점

  • 캡슐화를 저해합니다.
  • 접근 제한자를 무시하면서 코드가 복잡해지는 경우가 있습니다.
  • 리플렉션을 사용하면 런타임에 객체 정보를 검색하므로 성능이 떨어질 수 있습니다.

왜 사용하는 걸까?

Spring의 Bean Factory를 보면, @Controller, @Service, @Repository 등의 어노테이션만 붙이면 Bean Factory에서 알아서 해당 어노테이션이 붙은 클래스를 생성하고 관리해 주는 것을 알 수 있다. 개발자는 Bean Factory에 해당 클래스를 알려준 적이 없는데 가능한 이유는 바로 리플렉션 덕분이다. 런타임에 해당 어노테이션이 붙은 클래스를 탐색하고 발견한다면, 리플렉션을 통해 해당 클래스의 인스턴스를 생성하고 필요한 필드를 주입하여 Bean Factory에 저장하는 식으로 사용이 된다.

물론, 위에 단점에서 말했듯이 캡슐화를 저해하기 때문에 꼭 필요한 상황에서만 사용하는 것이 좋습니다.

profile
백엔드 개발자입니다.

0개의 댓글