[JAVA] Reflection API

Welcome to Seoyun Dev Log·2023년 4월 18일
0

JAVA

목록 보기
8/12

Reflection API

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

  • 로드된 클래스라고 함은, JVM의 클래스 로더에서 클래스 파일에 대한 로딩을 완료한 후, 해당 클래스의 정보를 담은 Class 타입의 객체를 생성하여 메모리의 힙 영역에 저장해 둔 것을 의미한다.(new 키워드를 통해 만드는 객체와는 다른 것임을 유의)
  • 원래 자바에는 동적으로 객체를 생성하는 기술이 없었으나 동적으로 인스턴스를 생성하는 Reflection으로 그 역할을 대신하게 된다.
  • Reflection으로 객체의 형은 알고 있지만 형변환을 할 수 없는 상태에서 객체의 메서드를 호출할 수 있다.

Reflection API 사용법

    1. 클래스.class 로 가져오기
    • 인스턴스가 존재하지 않고, 컴파일된 클래스 파일만 있다면 리터럴로 Class 객체를 곧바로 얻을 수 있다.
    • 가장 심플하게 Class 객체를 가져오는 방법
public static void main(String[] args) {

    // 클래스 리터럴(*.class)로 얻기
    Class<? extends String> cls2 = String.class;
    System.out.println(cls2); // class java.lang.String
}
    1. 인스턴스(Object).getClass() 로 가져오기 : 리터럴로 얻기
    • 모든 클래스의 최상위 클래스인 Object 클래스에서 제공하는 getClass() 메서드를 통해 가져온다.
    • ⭐️단 해당 클래스가 인스턴스화 된 상태 이어야 사용할 수 있는 제약이 있다
public static void main(String[] args) {

    // 스트링 클래스 인스턴스화
    String str = new String("Class클래스 테스트");

    // getClass() 메서드로 얻기
    Class<? extends String> cls = str.getClass();
    System.out.println(cls); // class java.lang.String
}
    1. Class.forName("클래스명") 으로 가져오기 : 동적 로딩
    • 위의 리터럴 방식과 같이 컴파일된 클래스 파일이 있다면 클래스 이름만으로 Class 객체를 반환 받을 수 있다
    • ⭐️위 두가지 방법중에서 가장 메모리를 절약하며 동적으로 로딩할 수 있기 때문에 성능이 좋다
    • 클래스 도메인을 상세히 적어줘야한다. 클래스 파일 경로에 오타가 없는지 꼭 확인할 것 (대소문자 실수 등) 만일 Class 객체를 찾지 못한다면 ClassNotFoundException 을 방생 시키기 때문에 예외처리가 강제 된다.
    • 보통 다른 클래스 파일을 불러올 때 컴파일 시 JVM의 method Area에 클래스 파일이 같이 바인딩 되지만 동적 로딩의 경우 컴파일에 바인딩 되지않고 런타임때 불러오게 되기 때문에 동적 로직이라고 부른다
      그래서 컴파일 타입에 체크할 수 없기 때문에 클래스 유무가 확인되지 않아 예외처리를 해주어야 하는 이유도 있다.
public static void main(String[] args) {
    try {
        // 도메인.클래스명으로 얻기
        Class<?> cls3 = Class.forName("java.lang.String");
        System.out.println(cls3); // class java.lang.String
        
    } catch (ClassNotFoundException e) {}
}

1) 로드된 클래스 가져오기

  • 위의 3가지 방법으로 가져온 Class 타입의 인스턴스는 모두 동일하다.
  • Class 타입을 통해 클래스의 인스턴스를 생성할 수 있고, 인스턴스의 필드와 메소드를 접근제어자와 상관없이 사용할 수 있게 된것이다.
public class Member {

    private String name;

    protected int age;

    public String hobby;

    public Member() {
    }

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

    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 +
            ", hobby='" + hobby + '\'' +
            '}';
    }
}

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("제이온", 23, "다라쓰 개발");
        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

2) 인스턴스 생성

  • getConstructor() : 기본 생성자를 통한 인스턴스 생성
  • newInstance() : 인스턴스 동적 생성
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, String.class);
        Member member3 = fullConstructor.newInstance("제이온", 23, "다라쓰 개발");
        System.out.println("member3 = " + member3);
    }
}

// 실행 결과
public Member()
public Member(java.lang.String,int,java.lang.String)
member2 = Member{name='null', age=0, hobby='null'}
member3 = Member{name='제이온', age=23, hobby='다라쓰 개발'}

3) 생성한 인스턴스의 필드와 메서드 접근

  • getDeclaredFields() : 클래스의 인스턴스 변수 모두 가져오기
  • 필드.get() : 필드값 반환
  • 필드.set() : 필드값 수정
    📌 주의 : private 접근제어자 필드에 접근할 때는 setAccessible()의 인지를 true로 넘겨줘야한다.
setAccessible(true)
  • getDeclaredMethod() : 메소드를 가지고 올 수 있다
    📌 주의 : 이때 메서드의 이름과 파라미터 타입을 인자로 넘겨줘야하며,
    private 접근제어자 메서드에 접근할 때는 setAccessible()의 인지를 true로 넘겨줘야한다.
memberClass.getDeclaredMethod("speak", String.class);

마지막으로 invoke() 메소드를 통해 리플렉션 API 얻어 온 메소드를 호출 할 수 있다.

public class Main {

    public static void main(String[] args) throws Exception {
        Member member = new Member("제이온", 23, "다라쓰 개발");
        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);
    }
}

// 실행 결과
제이온
23
다라쓰 개발
Member{name='제이온2', age=23, hobby='다라쓰 개발'}
리플렉션 테스트
비밀번호는 1234입니다.

장단점

  • 장점
    • 런타임 시점에 클래스의 인스턴스를 생성하고 접근 제어자와 관계 없이 필드와 메소드에 접근하여 필요한 작업을 수행할 수 있는 유연성을 가지고 있다.
  • 단점
    • 캡슐화를 저해한다.
    • 런타임 시점에 인스턴스를 생성하므로 컴파일 시점에서 해당 타입을 체크할 수 없다.
    • 런타임 시점에 인스턴스를 생성하므로 구체적인 동작 흐름을 파악하기 어렵다.
    • 단순히 필드 및 메소드를 접근할 때보다 리플렉션을 사용하여 접근할 때 성능이 느리다. (단 모든 상황에서 느리지는 않다)

사용하는 이유

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

단, 캡슐화를 저해하므로 꼭 필요한 상황에서만 사용하는 것이 좋다.


참고

profile
하루 일지 보단 행동 고찰 과정에 대한 개발 블로그

0개의 댓글