자바 리플렉션

황희윤·2023년 11월 24일

리플렉션(Reflection)

컴파일 시간이 아닌 실행 시간에 클래스에 대한 정보를 동적으로 가져와서 사용하는 기술

  • 자바에서 컴파일러가 소스 코드를 바이트코드로 바꾸면 클래스 로더(class loader)가 바이트코드를 해석해서 JVM 내 메모리 영역에 저장한다. 리플렉션은 JVM 메모리 영역에 저장된 클래스의 정보를 꺼내서 필요한 정보들(필드, 메서드, 생성자)을 사용하는 기술이다.

  • 컴파일 에러가 아닌 런타임 에러가 발생하기 때문에 상당한 주의가 필요하며 되도록 사용을 하지 않는것이 좋습니다.

  • 대표적으로는 Spring 프레임워크의 어노테이션 같은 기능들이 리플렉션을 이용하여 프로그램 실행 도중 동적으로 클래스의 정보를 가져와서 사용한다.

  • Spring은 리플렉션을 이용하여 런타임 시에 필요한 Bean을 동적으로 생성한다.

  • 프레임워크나 라이브러리에서 리플렉션이 많이 사용되는 이유는 개발자가 직접 작성한 코드에서는 객체의 타입을 쉽게 파악할 수 있지만, 프레임워크나 라이브러리에서는 컴파일 시점에 객체의 타입을 확인하기 어렵다. 따라서 런타임 실행 중에 동적으로 해결하기 위해서 리플렉션을 사용한다.

  • 자바에서는 java.lang.Class가 대표적인 예시

  • C나 C++, 파스칼은 리플렉션을 지원하지 않는다.

Class 클래스

  • Class는 실행 중인 자바 어플리케이션의 클래스와 인터페이스의 정보를 가진 클래스다.

  • Class 객체는 JVM에 의해 자동으로 생성된다. 따라서 public 생성자가 없다.

// Object 클래스의 getClass() 메서드를 호출하면 Class 클래스형의 객체를 가져올 수 있다.
String s = new String("안녕하세요");
Class c = s.getClass();

// 여기서 객체 c는 Class 클래스형의 객체다. 즉, c는 String.class를 뜻한다.
Class c = s.getClass();
Class c = String.class;

// s의 Class 클래스는 String.class다.

Class 가져오기

1. {클래스타입}.class

Class<?> catClass = Cat.class;

2. {인스턴스}.getClass()

Cat myCat = new Cat("나비");
Class myCatClass = myCat.getClass();

3. Class.forName("{전체 도메인 네임}")

Class<?> catClass = Class.forName("org.example.cat");
  • Class 클래스는 각각의 클래스에 대한 정보를 가져오는 메서드를 제공한다.

  • 이 메서드가 제공하는 정보는 두 가지다.

  1. 클래스의 클래스형에 관한 정보
    • "클래스가 어떤 인터페이스를 구현하는가?"
    • "어떤 클래스를 상속받는가?"
  2. 클래스의 멤버에 관한 정보
    • "클래스가 인터페이스나 배열을 가지고 있는가?"
    • "어떤 객체가 어떤 클래스를 인스턴스화한 것인가?"
  • 자바에서는 모든 .class 파일 하나마다 java.lang.Class 객체 하나씩이 생성된다. 이 Class 객체는 모든 .class 파일의 정보를 가지고 있다.

  • 모든 .class 파일은 이 클래스를 처음 사용하는 시점에 동적으로 클래스 적재기(class loader)를 통해 JVM에 적재된다.

  • 처음 사용하는 시점이라면 해당 .class 파일에서 static을 처음으로 사용하는 때를 말하는데, 생성자클래스(static) 메서드다.

  • 그래서 new 연산자를 사용하게 되면 적재된다고 보면 된다. 이를 동적 로딩이라 한다.

  • .class 파일의 정보와 Class 객체는 JVM에 런타임 데이터 영역(runtime data area)의 메서드 영역에 저장된다.

  • 이들 정보를 java.lang.reflect 패키지에 정의된 클래스들을 통해 접근하게 해준다.

Class 클래스의 활용

  • Class 클래스를 사용하여 객체의 정보를 분석하는 기술이 리플렉션이다.

  • 거의 대부분의 리플렉션은 프로그램 내부적으로 사용된다.

  • Class 클래스는 실행 중에 클래스의 정보를 분석한다.

Class의 기능들

  • 클래스에 붙은 어노테이션 조회
  • 클래스 생성자 조회
  • 클래스 필드 조회
  • 클래스 메서드 조회
  • 부모 클래스, 인터페이스 조회

Reflect 클래스

  • java.lang.reflect 패키지에서는 속성, 메서드, 생성자 같은 class들이 있고, 생성자를 사용해 새로운 객체를 생성하고, getter 메서드와 setter 메서드를 사용해 속성값을 읽거나 수정할 수 있다.

  • 매개 변수(parameter), 반환형, 접근 제어자 등 class에 관련된 모든 정보를 가져 올 수 있다.

  • 심지어 private로 캡슐화(encapsulation)된 불변(immutable)의 것까지도 setAccessible(true)를 통해 접근 가능하다.


리플렉션 활용

메서드

  • getMethod : 해당 클래스의 메서드 정보를 반환
  • getComponentType : 배열의 컴포넌트 형태를 표현하는 Class를 반환
  • getField : 해당 클래스의 속성 정보를 반환
  • getInterface : 객체가 표현하는 클래스 또는 인터페이스에 의해 구현되는 인터페이스를 반환
  • getSuperclass : 해당 클래스의 상위 클래스에 대한 정보를 반환
  • getName : Class 객체가 표현하는 요소(클래스, 인터페이스, 배열 클래스, 원시적 형태, 또는 void)의 이름을 문자열로 반환

어노테이션

  • 리플렉션을 통해 어노테이션을 붙일 수 있는 클래스나 메서드, 파라미터 정보를 가져온다.

  • 리플렉션의 getAnnotations, getDeclaredAnnotations 등의 메서드를 통해 어노테이션의 유무를 확인한다.

  • 어노테이션이 붙어 있다면 해당 로직을 수행한다.

  • Spring에서는 @Autowired만 붙이면 쉽게 객체에 해당하는 의존성을 주입(Dependency Injection)을 해줄 수 있습니다.


리플렉션 단점

  • 내부 노출 : 리플렉션을 사용하면 접근 지시자를 무시할 수 있기 때문에 추상화가 깨지고 플랫폼 업그레이드 시 동작이 변경될 수 있다.

  • 성능 저하 : 리플렉션을 통한 작업은 일반 작업보다 훨씬 느리다.

  • 컴파일 불가 : 컴파일시 타입 검사예외 검사를 할 수 없어 런타임에 문제가 발생할 수 있다.


리플렉션 사용 예시

public class Cat {
	private final String name;
    private int age;
    
    public Cat(String name, int age) {
    	this.name = name;
        this.age = age;
    }
    
    public String getName() {
    	return name;
    }
    
    public int getAge() {
    	return age;
    }
    
    public void setAge(int age) {
    	this.age = age;
    }
    
    private void meow() {
    	System.out.println("Meow!");
    }
    
    private static void meow2() {
    	System.out.println("Meow2!");
    }
}

import java.lang.reflect.Field;

public static void main(String[] args) throws Exception{
	Cat myCat = new Cat("나비", 6);
    Field[] myCatFields = myCat.getClass().getDeclaredFields();
    
    // Cat 클래스의 속성 찾기
    for(Field field : myCatFields)
    	System.out.println(field.getName());
    // name
    // age
    
    // Cat의 이름이 private final이지만 변경하기
    for(Field field : myCatFields)
    	if(field.getName().equals("name")){ // 이게 reflection을 사용하지 말아야 하는 이유이기도 한데, 다른 개발자가 아무런 경고 없이 Cat 클래스 속성 name을 nickname으로 바꾸면 코드가 동작하지 않는다.
        	field.setAccessible(true); // 이게 없으면 바뀌지 않는다.
        	field.set(myCat, "네로"); // checked exception
        }
        
    // Cat의 함수 호출하기
    Method[] myCatMethods = myCat.getClass().getDeclaredMethods();
    
    for(Method method : catMethods){
    	if(method.getName().equals("meow")){
        	method.setAccesible(true); // 만약 meow 메서드가 public이였으면 필요없음
        	method.invoke(myCat);
        }
    }
    
    for(Method method : catMethods){
    	if(method.getName().equals("meow2")){
        	method.setAccesible(true); 
        	method.invoke(null); // static은 객체를 생성한 후 호출할 필요없이 바로 호출하면 되서 null로도 작동한다.
        }
    }
}

getMethods VS getDeclaredMethods

getMethods

  • public 메서드들만 불러온다.

  • 상위 클래스와 상위 인터페이스에서 상속한 메서드들을 포함해 모든 메서드들을 불러온다.

getDeclaredMethods

  • 접근 제어자 상관 없이 모든 메서드들을 불러온다.

  • 직접 클래스에서 선언한 메서드만을 불러온다.


profile
HeeYun's programming study

0개의 댓글