Reflection API는 java.lang.reflect 패키지에서 제공되는 자바 내장 API로써, 컴파일 시점이 아닌 프로그램의 실행 환경에서 객체 클래스의 정보를 읽어오고 수정할 수 있다.
이렇게 말하면 벌써 거부감 들기 시작할텐데, 우리가 맨날 두들기고 있는 인텔리제이를 보자.
인텔리제이는 어떻게 우리가 작성하고 있는 클래스에 속해있는 메서드나 클래스를 읽어올 수 있는 것일까?
스프링 컨테이너는 어떻게 스프링이 작동하고 있을 때 각 생성자/필드/세터에 의존성을 주입해주는 것일까?
(단, 이는 JDK Dynamic Proxy의 얘기고 현재는 CGLIB 라이브러리를 기본으로 사용하는데 이 방식은 Reflection API가 아닌 Enhancer를 이용한다.)
JPA Entity에는 왜 기본 생성자가 필요할까?
그것이 모두 이 Reflection API라는 녀석 덕분이다. 이제 흥미가 생기지 않는가?
Reflection API를 어떻게 사용해야하는지 알려주려는 글은 아니기 때문에, 아주 간단한 예제로 해당 API의 동작을 확인해보자.
Person 클래스는 생성자의 접근 제어자가 private으로 설정되어 있으며, 필드 변수도 모두 private final이다.
즉, 오직 getPerson으로만 생성할 수 있으며 처음 대입된 필드 변수의 값은 변경할 수 없다.
하지만 난공불락의 요새란 존재하지 않는 법.. 최고존엄 Reflection 앞에서는 다 소용 없다.
(참고로 int는 당연히 클래스가 아니다. int.class == Integer.Type과 동일하다.)
대충 던져놓은 예외들은 무시하고 밑으로 가서 코드를 읽어보자. 메서드에 대해서 짧게 설명하겠다.
모든 클래스 뒤에는 .class로 클래스의 정보를 읽어올 수 있는데, 여기에 getDeclaredConstructor(Object obj, Object obj)를 붙여주면 해당 파라미터 타입에 맞는 Constructor를 찾을 수 있다. 그리고 여기에는 "private" 접근 제어자가 걸려있는 생성자도 포함된다.
그리고 해당 Cosntuctor의 접근제어자가 private일 경우 추가적으로 setAccesible 메서드를 통해 private 메서드에 대한 접근 권한을 true로 설정해주어야 한다.
(아마도 실수로 private 생성자 혹은 필드에 접근하는 것을 예방하기 위함인 것 같다.)
그렇게 뽑아낸 Constructor<Person> 인스턴스에 newInstance("주호민", 13)를 넣어주니.. 띠용?
우리가 원하는대로 private 생성자를 그냥 무시하고 인스턴스를 생성해버렸다.
이것을 설명하기 이전에 두 번째 사례를 보자.
방금 생성한 Person(name=주호민, age=13) 인스턴스의 클래스 정보를 getClass()로 가져와서 getDeclaredField를 사용하고 가져올 필드의 변수명을 입력해줬다.
그리고 이번에도 필드의 접근 제어자가 private이니 setAccessible을 true로 바꿔주고, field를 적용 시킬 Obj와 Value만 입력해준다면?
그렇다.. 우리가 알고 있는 불변도 결국에는 전부 어떻게든 망가트리려면 망가트릴 수 있는 불변이였던 것이다.
private 생성자와 private final 변수만을 가지고 있던 Person은 허무하게도 Person 클래스에 대한 단 한 번의 호출도 없이 인스턴스 생성, 필드값 변경까지 Reflection이라는 녀석한테 탈탈 털려버렸다.
대체 어떻게 된 일일까? 어디서 굴러먹다온 녀석이길래 이런 일이 가능한 것일까?
클래스 로더에 자바 코드가 로딩될 때, 컴파일 시 바이트코드로 변경되어 저장되는 것은 모두 알고 있을 것이다. 이 때 클래스 정보는 Native Memory영역의 Metaspace(구 Heap 메모리의 PermGen)에 저장되는데, Reflection은 이 Metaspace 영역에 .class(), .getClass(), .getName() 메서드로 접근하고 수정할 수 있다.
(참고 : 아직 Heap영역이라고 작성해놓은 포스트가 많은데, JAVA8부터 PermGen이 Metaspace로 변경되며 Native Memory 영역에 들어가게 되었고 클래스의 메타 데이터는 Metaspace에 저장된다.)
그러면 클래스 입장에서는 뒤통수가 얼얼하지만 우리한테는 너무 편한 거 아닌가요?
쩝.. 강한 힘에는 강한 책임이 따르는 법, 최소한으로 반드시 필요한 곳에 사용하는 것은 우리의 삶의 질을 2배로 높여주겠지만 마구잡이로 남용한다면 혼세마왕이 되는 것이다. 그 이유는 다음과 같다.
1. Reflection이 생성하는 객체에 대한 정보는 JVM에 캐시되어 있지 않기 때문에 초기 생성에 많은 자원이 들어간다.
(단, 한 번 캐시된 이후로는 일반 객체와 큰 차이가 없다.)
2. Reflection이 생성하는 객체, 혹은 수정 사항에 대한 문제는 컴파일러 단계에서 체크할 수 없기 때문에 프로그램 실행 도중 클라이언트 환경에서 Runtime Exception을 일으킨다.
3. private protected같은 접근 지시자를 무시하여 원치 않는 오류를 일으킬 수 있으며, (대표적으로 Singletone Pattern의 기능을 완전히 망가트릴 수 있다.) 코드의 전체 흐름을 완전히 무시하기 때문에 로직이 변경될 경우 관련된 모든 코드를 수정해줘야 한다.
4. Runtime 시 Java 보안 관리자에게 일부 권한을 부여 받는데 이는 보안 취약점이 될 수 있다.
이렇게 보니 완전 심각하고 절대 사용해서는 안될 API처럼 보일 수도 있지만, 아래의 주요 Reflection 사용처들을 보면 꼭 그렇지만은 않다고 느껴질 것이다.
1. Android에서 Hidden Method를 호출할 때.
2. 테스트 코드 작성을 위해 private 변수를 변경하고 싶을 때.
3. 써드파티 라이브러리의 변수를 변경하고 싶을 때.
4. 의존성 주입.
5. MVC Model에서 View로 넘어온 객체 binding 시 사용.
6. Entity 클래스에 Setter가 없을 때 리플렉션 사용.
7. Jackson, Log4j, Apache Commons BeanITils, Intellij
8. 스프링의 동적 프록시
어떤가? 이제 다시 Reflection에 대한 신뢰도가 올라가지 않는가?
프로그래밍 세계에서 과한 권한을 가진 명령어는 위험하지만 그 만큼 잘 사용하면 엄청나게 유용할 수 있다는 것을 명심하자.
자바의 권위자 중 한 명인 꼽을만한 조슈아 블로크도 그의 저서인 EffectiveJava 3/E에서 "Reflection API는 아주 제한되게 사용할 때 그 이점을 누릴 수 있다"고 언급하였다.
Reflection API의 정확한 사용법이 궁금하다면 아래 링크를 확인해보면 좋을 것이다.
https://www.baeldung.com/java-reflection
'Language & Framework > Java' 카테고리의 다른 글
이펙티브 자바 (2) 생성자에 매개 변수가 많으면 빌더를 고려하라 (+ 자바에서 freeze 구현하기) (0) | 2022.11.12 |
---|---|
이펙티브 자바 (1) 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2022.11.01 |
Interface와 Abstract의 차이는 무엇인가? - - 22/08/07 내용 추가 (0) | 2022.07.13 |
자바 예외 (2) java.io.InvalidClassException: InputAndOutput.Child; no valid constructor (0) | 2022.06.22 |
자바 예외 (1) java.io.NotSerializableException (0) | 2022.06.21 |