java 리플렉션이란?

Yellta·2024년 12월 19일
0

java

목록 보기
3/4
post-thumbnail

정의

런타임시에 클래스, 메서드, 필드 등의 정보를 동적으로 확인하거나 수정할 수 있게 해주는 기능
프로그램에서 임의의 클래스에 접근할 수 있다.

즉 Class객체가 주어지면 그 클래스의 생성자, 메서드, 필드에 해당하는 Constructor, Method, Field인스턴스를 가져올 수 있다.
또한 해당 가져온 인스턴스들을 통해서 실제 생성자, 메서드, 필드를 조작할 수도 있다. 이 인스턴스를 이용해서 해당 클래스의 인스턴스를 생성하거나, 메서드를 호출하거나, 필드에 접근할 수 있다는 의미이다.

코드 작성 및 컴파일 시점에 어떤 클래스가 존재하는지 몰라도, 실행중에 클래스 이름을 문자열로 지정해 해당 클래스를 찾고 사용할 수 있다.

일반적인 코드와 리플렉션 비교해보기

일반 코드

//컴파일 시점에 class가 이미 존재하고 있다.
MyClass obj = new MyClass();
obj.sayHello();

리플렉션을 사용한 코드(런타임에 클래스 로드)

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        // 컴파일 시점에 MyClass를 몰라도 런타임에 클래스 이름만 알고 있으면 로드 가능
        String className = "MyClass"; // 클래스 이름을 문자열로 지정

        // Class.forName()을 사용하여 클래스 동적 로드
        //클래스 이름만 알고 있다면? 실행 중에 클래스를 찾고 객체를 생성하거나 메서드 호출 가능
        Class<?> clazz = Class.forName(className);

        // 생성자 호출로 객체 생성
        Object obj = clazz.getDeclaredConstructor().newInstance();

        // 메서드 호출
        clazz.getMethod("sayHello").invoke(obj);
    }
}

리플렉션의 장점

  • 외부 플러그인, 런타임 객체 생성 같은 다양한 동적 작업이 가능하다.

리플렉션의 단점

  • 컴파일타임 타입 검사의 이점을 누릴 수 없다.
    프로그램이 리플렉션 기능을 이용해 존재하지 않거나 접근할 수 없는 클래스에 접근하다면 런타임 에러가 발생한다.

  • 코드가 지저분해진다.

  • 성능이 떨어진다.
    일반 메서드 호출보다 훨씬 느리다.

리플렉션의 장점과 단점을 보고 파악하면 리플렉션은 컴파일 타임에 알 수 없는 클래스를 사용할 때 적합하다.
그렇다면 그런 상황은 어떤게 있을까?

컴파일 타임에 알 수 없는 클래스를 사용하는 경우

동적 로딩, 유연한 객체 생성이 필요한 경우

1. 플러그인 시스템

String pluginClassName = "com.example.MyPlugin"; // 외부 플러그인의 클래스 이름
Class<?> pluginClass = Class.forName(pluginClassName);
Object pluginInstance = pluginClass.getDeclaredConstructor().newInstance();

intellij, vsCode의 IDE 플러그인 시스템, 게임 엔진의 Mod 시스템, 애플리케이션의 확장 모듈 같은 것이 있다.

2. 프레임워크 및 라이브러리

Spring 프레임 워크 :

@Component , @Service, @Repository등의 어노테이션을 스캔하고, 런타임에 객체를 생성해서 의존성 주입을 수행한다.
컴파일 타임에는 어떤 클래스가 등록될 지 몰라도 런타임에 찾아낸다.

Class<?> clazz = Class.forName("com.example.MyService");
Object service = clazz.getDeclaredConstructor().newInstance();

만들어보기

ConstFile

public class ConstFile {  
    private static final String IMCONST = "im const";  
  
    private static String hello() {  
       return "hello I'm in the private method!";  
    }
}

여기서 ConstFile은 외부에서 접근할 수 없도록 private 접근제한자를 통해 생성했다.
private, static (객체 생성과 상관없이 클래스에 직접 속함 ), final 값 변경 불가

즉 외부에서 필드에 접근하거나 값을 읽을 수 없고 마찬가지로 메서드도 외부에서 읽을 수 없다.

ReflectionTest

public class ReflectionTest {  
    public static void main(String[] args) {  
  
       Class<?> constClass = null;  
       try {  
          constClass = Class.forName("com.example.demoTest.reflection.ConstFile");  
       } catch (ClassNotFoundException e) {  
          throw new RuntimeException(e);  
       }  
       try {  
          Constructor<?> constructor = constClass.getDeclaredConstructor();  
          constructor.setAccessible(true);  
  
          Object instance = constructor.newInstance();  
          System.out.println("instance created : " + instance);  
  
          //private field 가져오기  
          Field staticField = constClass.getDeclaredField("IMCONST");  
          staticField.setAccessible(true);  
          String staticValue = (String)staticField.get(null);  
          System.out.println("staticValue : " + staticValue);  
  
          //private 메서드 가져오기  
          Method privateMethod = constClass.getDeclaredMethod("hello");  
          privateMethod.setAccessible(true);  
  
          //메서드 실행해보기  
          String result = (String)privateMethod.invoke(instance);  
          System.out.println(result);  
  
       } catch (InstantiationException e) {  
          throw new RuntimeException(e);  
       } catch (IllegalAccessException e) {  
          throw new RuntimeException(e);  
       } catch (InvocationTargetException e) {  
          throw new RuntimeException(e);  
       } catch (NoSuchMethodException e) {  
          throw new RuntimeException(e);  
       } catch (NoSuchFieldException e) {  
          throw new RuntimeException(e);  
       }  
  
    }  
}

실행 결과

리플렉션을 사용한 코드

staticValue : im const
hello I'm in the private method!
process time = 18 ms

리플렉션을 사용하지 않고 만든 코드

ConstPublic contents = ConstPublic
ConstPublic Method = method public
process time = 10 ms

보면 알다싶이 리플렉션을 사용하지 않은 코드는 10ms초 리플렉션을 사용한 코드는 18ms초인것을 확인할 수 있다.
이는 리플렉션은 런타임시에 클래스를 찾아내고 수행하기 때문이다. 그래서 시간이 조금 더 걸리는 것 성능저하의 원인이 될 수 있다.

여기서 리플렉션을 사용하지 않은 코드는 따로 ConstPublic이라는 class를 만들고 private이 아닌 public으로 설정해서 인스턴스를 생성할 수 있도록 설정했다.

리플렉션 안전하게 사용하기

리플렉션은 런타임에 존재하지 않을 수도 있는 다른 클래스, 메서드를 사용하게 된다. 그래서 존재하지 않는 필드, 메서드를 사용하기 때문에 try~catch()을 통해서 예외를 처리해야한다.

리플렉션은 되도록 객체 생성시에만 사용하고, 생성한 객체를 이용할 때에는 적절한 인터페이스나 컴파일 타임에 알 수 있는 상위 클래스로 형변환해서 사용해야 한다.

사유는 타입 안정성, 성능 때문이다.

예시를 먼저 살펴보자

MyService Interface

public interface MyService {  
    void execute();  
}

MyService 구현체

public class MyServiceImpl implements MyService {  
    @Override  
    public void execute() {  
       System.out.println("executed!!!");  
    }  
}

리플렉션 코드

public class MyServiceReflection {  
    public static void main(String[] args) {  
       String className = "com.example.demoTest.reflection.MyServiceImpl";  
  
       try {  
          Class<?> clazz = Class.forName(className);  
  
          //객체 생성  
          Constructor<?> constructor = clazz.getDeclaredConstructor();  
          Object obj = constructor.newInstance();  
  
          //인터페이스로 형 변환하기  
          if (obj instanceof MyService) {  
             MyService myService = (MyService)obj;  
             myService.execute();  
          } else {  
             throw new IllegalArgumentException("provied class doesn't implement MyService");  
          }  
       } catch (ClassNotFoundException e) {  
          throw new RuntimeException(e);  
       } catch (InvocationTargetException e) {  
          throw new RuntimeException(e);  
       } catch (NoSuchMethodException e) {  
          throw new RuntimeException(e);  
       } catch (InstantiationException e) {  
          throw new RuntimeException(e);  
       } catch (IllegalAccessException e) {  
          throw new RuntimeException(e);  
       }  
    }  
}

Interface, 상위 클래스로 변환해서 사용해야하는 이유

  • 타입 안전성
    변수, 메서드, 객체 등이 컴파일 타임에 올바른 타입을 사용하도록 보장한다. 컴파일 타임에 타입을 알 수 없어서 런타임에 오류가 발생할 수 있다.

  • 리플렉션 최소화
    성능 오버헤드와 가독성 저하를 일으킬 수 있다. 즉 객체 생성 단계에서만 사용하고 이후에는 정적 타입으로 처리한다.

결론

  • 리플렉션을 런타임에 존재하지 않을 수도 있는 다른 클래스, 메서드, 필드의 의존성을 관리할 때 적합하다.
  • 리플렉션을 사용하면 예외가 터지는 상황을 방지하기 위해 try~catch문을 사용해야한다. 이때문에 가독성을 해치기도한다. 잡아야하는 에러는 총 6개이다.
  • 리플렉션은 버전이 여러개 존재하는 외부 패키지, 외부 플러그인 등등에 사용할 수 있다.
  • 리플렉션은 접근하려는 클래스나 메서드가 런타임에 존재하지 않아 오류가 발생하지 않을 수 있다는 사실을 감안해야한다.
  • 리플렉션은 되도록 객체 생성에만 사용하고, 생성한 객체를 이용할 때는 적절한 인터페이스나 컴파일 타임에 알 수 있는 상위 클래스로 형변화해 사용해야 한다. 사유는 타입 안정성때문이다.

참고

이펙티브 자바(책) = 아이템 65 리플렉션

챗지피티

profile
Yellta가 BE개발해요! 왜왜왜왜왜왜왜왜왜왜왜왜왜왜왜왜왜왜왜 가 제일 중요하죠

0개의 댓글