객체를 통해 클래스의 정보를 분석해 내는 프로그래밍 기법
구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드, 타입, 변수들에 접근할 수 있다.
즉, 컴파일 시간이 아니라 런타임에 동적으로 특정 클래스의 정보를 객체화하여 분석 및 추출해낼 수 있는 프로그래밍 기법이다.
좀 더 이해하기 위해 리플렉션을 왜 사용하는지, 언제 사용하는지 먼저 알아보도록 한다.
자바 및 코틀린은 다형성을 활용하여 아래와 같은 객체 생성이 가능하다.
Object obj = new Car(0);
obj.move();
val obj: Any = Car(0)
obj.move()
obj 객체가 Car 클래스의 move 메서드를 사용할 수 있을까? 안 된다. 컴파일 에러가 발생한다.
(물론 Any 혹은 Object에 move 메소드가 구현되어 있고 Car 클래스가 move 메소드를 오버라이딩 했다면 위와 같은 경우는 가능하다.)
자바와 코틀린은 정적 언어로, 컴파일 타임에 타입이 결정된다. obj 객체는 Object / Any 타입으로 결정됐으므로 Object / Any 클래스의 메서드와 인스턴스 변수만을 사용할 수 있다.
생성된 obj 라는 객체는 Object / Any 클래스 타입만 알고 있을 뿐이다. Car 클래스라는 구체적인 타입은 알 수 없다.
이말은 즉슨, 구체적인 클래스를 모르면 해당 클래스의 정보에 접근할 수 없다는 것이다.
이러한 정적 언어로의 한계점을 보완하여 구체적인 클래스를 알지 못해도 런타임 도중 해당 클래스의 정보에 접근할 수 있도록 해주는 것이 Reflection
이다.
리플렉션은 런타임에 다른 클래스를 동적으로 로딩하여 접근할 때, 클래스와 메서드 및 멤버 필드에 대한 정보를 얻어야 할 때 사용하게 될 것이다. 다만, 코드를 작성할 때는 구체적인 클래스를 모를 일이 거의 없기에 실제로 리플렉션을 활용하는 경우는 거의 없다.
하지만 프레임워크나 라이브러리에서는 많이 사용된다. 프레임워크나 라이브러리에서는 사용자가 어떤 클래스를 만들지 예측할 수 없다. 프레임워크에서 구체적이지 않은 객체를 받아 동적으로 해결해 주기 위해 Reflection
을 사용하는 것이다.
java.io.ObjectInputStream
클래스의 readObject()
메서드를 이용하는데, readObject()
는 내부적으로 리플렉션을 이용해 직렬화된 객체의 readObject()
메서드를 호출한다. 리플렉션은 다음과 같은 장점을 갖는다.
다만, 리플렉션은 아래와 같은 단점을 갖고 있다.
성능 오버헤드
리플렉션을 사용함으로써 컴파일 타임이 아닌 런타임에 동적으로 타입을 분석하여 정보를 가져오게 되므로, JVM 최적화를 수행할 수 없다.
런타임에 동적으로 타입을 분석하기 때문에 예기치 못한 문제가 런타임 도중 발생할 수 있다.
다만, 참고 자료에 의하면 아래 내용을 기술하고 있다.
Reflection의 사용에 관한 사실들은 사실 사실이 아니다. 성능, 디버깅, 그리고 복잡성과 관련된 내용들은 잘못 사용된 예에서 파생한 오해들이다.
Reflection은 염려할 만큼의 성능저하를 가져오지 않는다. 대량의 if/else문이나 switch문 대신, 잘 설계된 Reflection은 객체지향 철학을 어기지 않으면서도 더 좋은 성능을 발휘할 수도 있다. (참고 자료의 Reflection에 따른 성능 저하 표 참고)
이 결과를 통해 알 수 있는 사실은 “Reflection에 따른 성능 저하”가 아니라 “성능 측정 결과, Reflection을 사용한 지금 이 경우에는 성능이 저하되는 것을 검증했다”라는 것이다. 최적화 또는 성능 개선(Optimization)시 유의해야 할 점은 반드시 최적화 이전과 이후의 성능을 측정하여, 성능개선이 가시적으로 보일 때에만 적용해야 한다는 것이다.
추상화 위반
Field.setAccessible()
와 Class.getDeclaredField(String name)
, Class.getDeclaredFields()
를 사용하면 직접 접근할 수 없는 private
인스턴스 변수, 메서드에 접근하여 조작할 수 있다. 따라서, 내부를 노출시키게 되어 추상화가 깨지게 된다.디버깅
리플렉션 기술 조사를 진행하면서, Koin에서 의존성을 주입하는 방법 또한 리플렉션 일 수 있겠다는 생각이 들어 찾아 봤으나, 아니었다.
Koin은 다음과 같이 소개하기도 한다.
A pragmatic lightweight dependency injection framework for Kotlin developers: no proxy, no code generation, no reflection.
Koin은 get, inject 등이 reified
로 구현되어 있었다.
reified
reified는 inline func와 조합해서야만 사용할 수 있다. reified type과 함께 인라인 함수가 호출되면 컴파일러는 type argument로 사용된 실제 타입을 알고 만들어진 바이트 코드를 직접 클래스에 대응되도록 바꿔 준다. 따라서,
myVar is T
는 런타임과 바이트 코드에서myVar is String
이 된다.
리플렉션에 대한 기술 조사를 하던 와중, type introspection
을 자주 볼 수 있었다.
Type Introspection은 런타임에 객체의 타입이나 속성을 검사하는 것이라고 설명한다. 객체의 클래스, 메소드, 필드 등의 객체 정보를 조사하는 것이다.
여기까지 보았을 때, Reflection
와 개념이 유사하다 생각되어 헷갈리는 부분이 있었다. (위키백과에서도 헷갈려서는 안 된다고 말하고 있다.)
type introspection을 런타임 시점에 사용되는 자신의 구조와 행위를 관리하는 것이라 한다. (수정할 수 있는 프로세스까지 포함하면 Reflection이라 한다.)
객체의 타입과 시스템의 코드를 확인하는 것은 Reflection이 아니라, Type Introspection 이며, 런타임에 수정할 수 있는 능력까지 하여 Reflection으로 정의한다고 한다.
결론적으로, Reflection은 Type Introspection의 과정에 수정까지 포함된 것이라 할 수 있다.