Java Reflection이란?
1. 정의
힙 영역에 로드된 Class 타입의 객체를 통해
원하는 클래스의 인스턴스, 인스턴스의 필드, 메소드를 접근 제어자와 상관없이 사용할 수 있도록 지원하는 API를 말한다.
❓ 여기서 로드된 Class란?
JVM의 클래스 로더에서 클래스 파일에 대한 로딩을 완료한 후, 해당 클래스의 정보를 담은 Class 타입의 객체를 생성하여 메모리의 힙 영역에 저장해 둔 것을 의미한다.
2. 사용 방법
Reflection 사용에 앞서, 힙 영역에 로드된 Class 타입의 객체를 가져와야 한다.
1) 클래스.class로 가져오기
2) 인스턴스.getClass()로 가져오기
3) Class.forName(”클래스명”)으로 가져오기
3. 장점 및 단점
장점
- 런타임 시점에서 클래스의 인스턴스를 생성하고, 접근 제어자와 관계 없이 필드와 메소드에 접근하여 필요한 작업을 수행할 수 있는 유연성을 가지고 있다.
단점
- 캡슐화를 저해한다.
- 런타임 시점에서 인스턴스를 생성하므로 컴파일 시점에서 타입을 체크할 수 없다.
- 런타임 시점에서 인스턴스를 생성하므로 구체적인 동작 흐름을 파악하기 어렵다.
- 단순히 필드 및 메소드를 접근할 때 보다 리플렉션을 사용하여 접근할 때 성능이 느리다.
Reflection을 사용하는 이유
규모가 작은 콘솔 단계에서는 개발자가 충분히 컴파일 시점에 프로그램에서 사용될 객체와 의존관계를 모두 파악할 수 있다.
하지만 프레임워크와 같이 큰 규모의 개발 단계에서는 수많은 객체와 의존관계를 파악하기 어렵다.
✅ 이때 Reflection을 사용하면, 런타임 도중에 동적으로 클래스를 만들어서 의존관계를 맺어줄 수 있다.✅
가령 Spring의 BeanFactory를 보면 @Controller
, @Service
, @Repositiory
등의 어노테이션만 붙이면 BeanFactory에서 알아서 해당 어노테이션이 붙은 클래스를 생성하고 관리해 주는 것을 알 수 있다.
비록 개발자가 BeanFactory에 해당 클래스를 알려준 적이 없지만 Reflection 덕분에 가능하다.
따라서, 런타임 시 해당 어노테이션이 붙은 클래스를 탐색하고 발견한다면, Reflection을 통해 해당 클래스의 인스턴스를 생성하고 필요한 필드를 주입하여 Bean Factory에 저장하는 식으로 사용이 된다.
JPA에서의 Reflection
그럼 이제 Reflection과 관련해서 조금 더 깊게 들어가보자.
1. JPA에서 기본 생성자가 있어야 하는 이유
JPA에선 엔티티 생성을 위해 기본 생성자를 만들 것을 강제한다.
Almost all frameworks require a default(no-argument) constructor in your class because these frameworks use reflection to create objects by invoking the default constructor
거의 모든 프레임워크에서의 클래스에서 기본 생성자가 필요하다.
그 이유는 Reflection이 기본 생성자를 통해 객체를 생성하기 때문이다.
위의 문장을 해석한 내용이다.
❓ 그렇다면 왜 Reflection을 사용하기 위해 기본 생성자가 반드시 필요할까?
이유는 비슷한 과정 속에서 찾을 수 있다.
웹 요청으로 들어오는 @RequestBody를 객체(DTO)로 바인딩하는 과정을 순서대로 살펴보자.
-
요청이 들어오면 Spring은 Http body에 있는 JSON 데이터를 DTO로 바인딩(역직렬화) 해준다.
-
바인딩 할 DTO의 기본 생성자가 존재하지 않는다면 정상적으로 바인딩이 되지 않는 예외가 발생한다.
예외가 발생하는 이유가 뭘까?
이는 앞에도 말했듯이 @RequestBody
바인딩 방식이 기본 생성자를 통해 객체를 생성한 후 Java Reflection을 이용해 필드 값을 집어넣어 주는 방식이기 때문이다.
위에서 상술한 것 처럼 Reflection은 클래스 이름만 알면 생성자, 필드, 메서드 등 클래스의 모든 정보에 접근이 가능하다.
하지만, 가져올 수 없는 정보 중 하나가 바로 생성자의 인자(매개변수) 정보들이다.
따라서 Reflection으로 생성할 객체에 모든 필드를 받는 생성자가 있더라도 Reflection은 해당 생성자를 호출할 방법이 없다.
따라서, Reflection은 기본 생성자로 객체를 생성한 뒤, 필드 값을 매핑한다.
결론적으로 JPA에서 기본 생성자를 강제하는 이유는 기본 생성자가 존재하지 않는다면 데이터베이스에서 조회해 온 값을 엔티티로 만들 때 객체 생성 자체에 실패하기 때문이다.
2. 기본 생성자를 private으로 할 수 없는 이유
이유는 엔티티 객체의 생성 자체만을 놓고 생각하면 안된다.
JPA에는 연관된 엔티티 정보를 실제로 필요한 순간에 조회해오는 지연 로딩(Lazy loading)이라는 방식이 있다.
지연 로딩은 프록시 객체를 일단 넣어두고 필요한 순간에 엔티티를 조회한 뒤 프록시 객체가 원본 엔티티를 참조하도록 한다.
그런데, 이 프록시 객체라는 것도 결국 원본 엔티티를 상속해서 만든다.
❓ 그런데 여기서 원본 엔티티의 기본 생성자가 private이라면?
해당 엔티티를 상속한 프록시 객체 를 만들 수 없다.
그 이유는 당연하게도 상속한 객체의 생성자는 반드시 부모 객체의 생성자 super
를 호출해야 하는데, private이면 상속받은 클래스에서 호출할 수 없다.
이렇기 때문에 엔티티 클래스의 생성자는 private일 수 없게 되는 것이다.
Leave a comment