Reflection이란

이우길·2022년 3월 10일
1

Back To Java

목록 보기
2/4
post-thumbnail

Java Reflection 정의

Java코드는 컴파일 되면 바이트 코드(.class)로 변환이 된다.

또한 JVM은 해당 바이트 코드를 내부적으로 인터프리터와 JIT를 이용하여 네이티브 코드로 변환하여 읽어 나간다.

때때로는 애플리케이션 실행 중에(런타임) 일부 class, 인터페이스, 필드, 메서드 등 변경을 해야할 때가 필요하다.

이렇게 애플리케이션 실행 중 코드를 변경할 수 있도록 Java에서는 Reflection를 제공해준다.

추가적으로 Reflection을 이용하면 런타임에 객체를 동적으로 생성할 수도 있다.

이미 Spring, Hibernate, Lombok 등 많은 프레임워크에서 Reflection 기능을 사용하고 있다.

요약

Reflection은 런타임에 클래스의 동작을 검사하거나 조작할 때 사용되는 프로세스이다.


Java.lang.Class

Java에서 제공하는 Reflection을 이용하기 위해 살펴봐야 할 라이브러리는 Java.lang.Class이다.

Java8 Java.lang.Class에서 해당 라이브러리가 지원하는 API를 볼 수 있다.

그럼 어떻게 Java.lang.Class를 얻어올 수 있을까? 방법은 3가지가 존재한다.

.class

인스턴스가 존재하지 않을때 사용자 정의 타입(클래스)에 .class를 붙여 Class 객체를 얻어올 수 있다.

public class Member{
	private String name;
    private int age;
}

// ...

public static void main(String[] args){
	Class<Member> memberClass = Member.class;
    // ...
}

위와 같이 타입을 통해 Class객체를 얻어올 수 있다.

Object.getClass()

인스턴스가 존재할 경우 Object.getClass를 이용하여 Class 객체를 얻어올 수 있다.

모든 Class는 Object를 상속받고 있기 때문에 해당 method는 제공된다.

public class Member{
	private String name;
    private int age;
}

// ...

public static void main(String[] args){
	Member member = new Member();
    member.getClass();
    Class<? extends Member> memberClass = member.getClass();
    // ...
}

Class.forName()

패키지명이 포함된 클래스명을 통해서도 Class 객체를 얻어올 수 있다.

// package: com.example.Member
public class Member{
	private String name;
    private int age;
}

// ...

public static void main(String[] args){
	Class<?> memberClass =Class.forName("com.example.Member");
    // ...
}

하지만 위와 같은 경우 Class를 찾지 못한다면 ClassNotFoundException를 발생 시키기 때문에 예외처리가 강제된다.

요약

Class객체를 얻어오는 방법은 3가지가 있다.

  1. .class
  2. .getClass()
  3. Class.forName() (ClassNotFoundException 예외 처리)

Hello Reflection

이번 글에서는 Reflection을 통해 인스턴스를 생성하고 method를 실행하는 방법만을 담으려고 한다.

Reflection을 이용한 인스턴스 생성하기

이전에는 Class 객체에서 제공해주는 newInstance()를 통해 Class 객체에서 바로 인스턴스를 생성할 수 있었다.

하지만 현재 newInstance()는 Java9부터 Deprecated되었다.

@CallerSensitive
@Deprecated(since="9")
public T newInstance(){
	//...
}
    

이로 인해 인스턴스를 생성하는 순서는 다음과 같아졌다.

  1. Class 객체 얻어오기
  2. 해당 Class에 대한 생성자 얻어오기
  3. 생성자를 통해 인스턴스 생성

코드로 나타내면 다음과 같다. (기본 생성자와 매개변수가 있는 생성자 둘 다 이용이 가능하다.)

public class Member {

    private String name;
    private int age;

    public Member() {
    }

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

먼저 위와 같은 class가 있다고 할 때 Reflection을 이용하여 기본생성자를 호출하여 인스턴스를 생성하는 로직은 다음과 같다.

//...
public static void main(String[] args){
	Class<Member> memberClass = Member.class;
    Constructor<Member> noArgConstructor = memberClass.getConstructor();
    Member member = noArgConstructor.newInstance();
}

기본 생성자이기 때문에 getConstructor()를 할 때 파라미터로 어떠한 type도 지정해주지 않았다. 지정해주지 않으면 Member Class기본 생성자가 호출 된다.

그럼 다음으로 매개변수가 있는 생성자를 호출할 때를 확인해보자.

public static void main(String[] args){
	Class<Member> memberClass = Member.class;
    Constructor<Member> argConstructor = memberClass.getConstructor(String.Class, int.Class);
    Member member = argConstructor.newInstance("leewoooo", 27);
}

위의 코드와 같이 매개변수가 있는 생성자를 Reflection을 이용하여 가져올 때 파라미터의 타입을 지정해주어야 한다.

공통적으로는 만약 해당하는 생성자를 찾지 못하면 NoSuchMethodException이 발생된다.


메소드 실행하기

Reflection을 이용하여 메소드를 실행하는 것도 인스턴스를 생성하는 것과 비슷한 프로세스를 갖게 된다.

  1. Class 객체 얻어오기
  2. 해당 Class에서 method 얻어오기
  3. 얻어온 method를 invoke() API를 이용하여 실행하기

Reflection의 대상이 되는 Class에 다음과 같은 method가 있다고 가정해보자.

public class Member {
    //...
    public int sum(int left, int right){
        return left + right;
    }
    
    public static int staticSum(int left, int right){
        return left + right;
    }
}

인스턴스를 생성할 때 처럼 Class 객체를 얻어와서 method를 얻어온다.

//...
public static void main(String[] args){
	Class<Member> memberClass = Member.class;
    Method sum = memberClass.getMethod("sum", int.class, int.class)
}

getMethod()를 실행할 때 필요한 것은 아래와 같다.

  1. method 명
  2. method의 매개변수 타입들

이미 위에 예제 코드에서 본 것 처럼 매개변수가 있는 생성자를 얻어올 때와 동일하다.

만약 매개변수가 없는 메소드라면 메소드 명만 입력해주면 되며 해당 정보로 메소드를 찾지 못할 경우 동일하게 NoSuchMethodException가 발생한다.

실행은 Method 에서 제공하는 invoke()를 호출하여 실행하면 된다. 여기서 static 메소드와 static이 아닌 method로 나뉘게 되는데

static 메소드는 invoke()를 실행 할 때 해당 Class타입의 Object 즉 인스턴스가 필요 없지만 static이 아닌 method는 실행할 때 인스턴스가 필요하다.

// ...
 Method sum = memberClass.getMethod("sum", int.class, int.class);
       	// 실행하기 (실행할 때 static method가 아닌 이상 인스턴스를 지정해야 한다.)
        // 만약 인스턴스를 지정하지 않을 경우 NullPointerException가 발생한다.
        int result = (int) sum.invoke(memberWithAge, 1, 2);
        System.out.println("result = " + result); // 3

        Method staticSum = memberClass.getMethod("staticSum", int.class, int.class);
        int staticResult = (int) staticSum.invoke(null, 1, 2);
        System.out.println("staticResult = " + staticResult); // 3

요약

Reflection을 이용하여 Class 객체를 이용하여 해당 인스턴스를 생성도 할 수 있으며 메소드도 실행할 수 있다.

생성자를 얻어올 때, 메소드를 얻어올 때 매개변수가 있다면 매개변수의 타입을 지정해야 한다.

만약 찾으려는 생성자나 메소드가 존재하지 않을 경우 NoSuchMethodException가 발생한다.

static 메소드가 아닌 경우 invoke()를 실행할 때 첫번 째 인자로 인스턴스를 넣어줘야 한다.


마무리

Reflection은 정말 강력한 기능이다.

Reflection을 이용하여 필드를 조회할 수도 있으며 조회한 필드에 대한 값을 변경하거나 조작할 수도 있다. 또한 class, method, field에 대한 Annotation을 조회할 수도 있으며 현재 class의 부모 혹은 구현하고 있는 interface도 조회가 가능하다. (필요한 기능들이 있다면 API를 참조하여 활용)

이번 글에서 인스턴스 생성 및 메소드에 대한 사용법을 알아본 이유는 스프링 프레임워크에서도 DI를 할 때 Reflection을 이용하여 객체를 생성하고 주입해주는 과정을 간략하게 이해하기 위해서 이다.

강력한 기능이니 만큼 잘 알고 써야하는게 중요한데 다음과 같은 이슈가 발생할 수 있다.

  • 지나치게 사용하면 성능상의 이슈가 발생할 수도 있다. (필요한 경우에만 사용할 것)

  • 코드를 작성할 때 접근지시자를 정의하였지만 무시되는 경우도 발생(default는 public 필드만 접근이 가능하지만 setAccessible(true)로 public 이외 필드도 접근이 가능하다.)

위에 작성한 이슈말고도 다른 이슈들도 있지만 어떠한 특정 기술을 적용하고자 할 때 사이드 이펙트 등 고려할 사항들을 잘 판단하여 사용하자 :)


참조

https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html

https://medium.com/edureka/java-reflection-api-d38f3f5513fc

profile
leewoooo

0개의 댓글