본문 바로가기

항해 99/Spring

Reflection API

Reflection API

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

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

Reflection API 사용법

.class로 가져오기

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

    // 클래스 리터럴(*.class)로 얻기
    Class<? extends String> cls2 = String.class;
    System.out.println(cls2); // class java.lang.String
}

 

인스턴스(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
}

 

Class.forName("클래스명") 으로 가져오기(동적 로딩)

  • 리터럴 방식과 같이 컴파일된 클래스 파일이 있다면 클래스 파일이 있다면 클래스 이름만으로 Class 객체를 반환 받을 수 있다
  • 가장 메모리를 절약하며 동적으로 로딩할 수 있기 때문에 성능이 좋다.
  • 클래스 도메인을 상세히 적어야 한다, 클래스 파일 경로에 오타가 없는지 꼭 확인할 것(대소문자 실수 등) 만일 Class 객체를 찾지 못한다면 ClassNotFoundExcetoption을 발생시키기 때문에 예외처리가 강제 된다.
  • 보통 다른 클래스 파일을 불러올 때 컴파일 시 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) {}
}

 

로드된 클래스 가져오기

  • 위 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

 

인스턴스 생성

  • 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='다라쓰 개발'}

 

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

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

 

invoke() 메서드를 통해 Reflection 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입니다.

 

장단점

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

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

'항해 99 > Spring' 카테고리의 다른 글

Web Game 코드 설계 정리  (0) 2024.04.23
프로젝트 코드 분석  (0) 2024.04.23
POJO  (0) 2024.04.22
Spring boot 모니터링 with Prometheus, Grafana  (1) 2024.04.20
낙관적 락 & 비관적 락  (0) 2024.04.19