📚 Reflection이란?
Java Reflection이란 짧게 요약하면 런타임 중, 어플리케이션의 클래스 및 객체에 관련된 정보에 액세스 할 수 있게 해주는 언어이자 JVM의 기능 입니다.
보통의 프로그램은 실행 시 Input을 받아 Output을 반환합니다.
반대로 Reflection으로 프로그램 작성 시 Input과 내부 소스 로직을 모두 입력값으로 간주해 그걸 분석하고 Output을 반환합니다.
위 이유로 인해 Reflection을 이용하면 강력한 라이브러리,프레임워크,소프트웨어를 설계 할 수 있습니다.
- Java Reflection은 언어이자 JVM의 기능 중 하나이며, 런타임 시
classes
와objects
를 추출할 수 있습니다. - Reflection API를 통해 다양한 소프트웨어를 Flexible하게 컴포넌트를 연결하고, 소스코드를 수정하지 않고 새로운 프로그램 흐름을 만들 수 있습니다.
- 또, Reflection을 이용해 다목적 알고리즘을 작성할 수 있습니다. 실행하고 있는 클래스와 객체에 따라 이 알고리즘을 쉽게 조정하거나 변경할 수 있습니다.
📚 Use Cases
Spring
- Spring의
@Configuration, @Bean, Dependency Injection
등이 있습니다. - 예를 들어
@Configuration
을 정의한 Config 클래스의 메서드에@Bean
을 붙이면 런타임 시 해당 Bean을 객체로 만들어 다른 객체의 생성자에 해당 Bean 객체가 필요할 떄 주입해주는 역할을 합니다. - Google Guice
Json Serialization/Deserialization Library
- 라이브러리에 사용해 프로토콜 간 변환을 실행할 때에도 Reflection이 사용됩니다.
- Jackson Library
- Gson Library
- 입력값으로 Json이 들어오면 위 라이브러리들은 Reflection을 사용해 클래스를 확인하고 필드를 전부 분석후 간단히 객체로 변환 해줍니다. (반대의 경우도 마찬가지)
- 그래서 우리는 아주 흔하게 사용하는
ObjectMapper.readValue(json, Person.class)
와ObjectMapper.writeValueAsString(person)
과 같이 간단하게 1줄의 메서드로 직렬화/역직렬화를 사용하고 있는 것입니다.
JUnit
Java Reflection으로 동작하는 대표적인 Use Cases는 유닛 테스트에 자주 사용하는 JUnit
이 있습니다.
이해하기 쉽게 아래와 같은 Reflection으로 동작하는 테스트 클래스가 있다고 가정해 봅시다.
public class CarTest {
@Before
public void setUp(){}
@Test
public void testDrive(){}
@Test
public void testBrake(){}
}
위 클래스를 Reflection을 사용하지 않고 만들면 클래스에 main() 메서드를 먼들어 수동으로 클래스를 인스턴스화 하고,
메서드 각각을 수동으로 설정하고 호출해야 합니다.
public class CarTest {
public void setUp(){}
public void testDrive(){}
public void testBrake(){}
...
public static void main(String[] args) {
CarTest carTest = new CarTest();
carTet.setUp();
carTest.testDrive();
carTest.testBrake();
}
}
Reflection은 위 Use Cases들뿐 아니라 아주 다양하게(Logging Frameworks, ORM, Web Frameworks 등등) 사용되고 있습니다.
📚 Reflection 문제점
위에서 알아본것과 같이 Reflection은 매우 강력한 기능이지만 아래와 같은 단점들도 있습니다.
성능 문제
Reflection은 런타임에 메서드나 필드에 접근하기 위해 추가적인 처리 단계를 거칩니다. 따라서 일반적인 코드 호출보다 느립니다.
이러한 성능 저하로 인해, Reflection은 빈번히 호출되는 코드에서 사용하기 부적합합니다.
안정성 감소
Reflection은 컴파일 타임이 아니라 런타임에 코드 구조에 접근하기 때문에, 컴파일 시점에 에러를 잡을 수 없습니다.
만약 접근하려는 메서드나 필드의 이름이 변경되거나 삭제되면, 런타임 에러가 발생할 가능성이 높아집니다.
캡슐화 위반
Reflection을 사용하면 private 필드나 메서드에 접근할 수 있습니다. 이는 객체지향 프로그래밍의 중요한 원칙인 캡슐화(encapsulation) 를 위반합니다.
잘못 사용하면 클래스의 내부 구현에 의존하게 되어 유지보수가 어려워질 수 있습니다.
보안 문제
Reflection은 보안 관리자(Security Manager)가 설정된 환경에서는 제한될 수 있습니다. 잘못된 접근으로 인해 민감한 데이터가 노출될 위험이 있습니다.
예를 들어, 악의적인 코드가 Reflection을 통해 private 데이터를 조작하거나 읽을 수 있습니다.
가독성과 유지보수성 저하
Reflection으로 작성된 코드는 일반 코드에 비해 이해하기 어렵고, 디버깅이 복잡합니다.
특히 팀 작업에서 Reflection을 남용하면 코드의 유지보수가 매우 어려워질 수 있습니다.
동일성 문제
Reflection은 클래스나 객체의 구조를 기반으로 동작하기 때문에, 특정 JVM 구현이나 클래스 로더에 따라 동작이 달라질 수 있습니다.
이는 코드의 플랫폼 독립성을 저해할 수 있습니다.
📚 Reflection API Entrypoint
어플리케이션의 클래스와 객체를 확인할 수 있도록 Reflection 로직을 작성할 수 있는 Class<?>
객체입니다.
Class 타입의 객체는, 객체의 런타임에 관한 정보가 있거나 앱에 있는 특정한 클래스가 존재합니다.
그리고 어떤 메서드와 필드를 포함하는지, 어떤 클래스를 확장하는지, 어떤 인터페이스를 실행하는지도 알 수 있습니다.
이 Class 객체를 얻을 수 있는 방법과, Java의 Wildcard를 복습해 보겠습니다.
📚 Class<?> 객체를 얻는 3가지 방법
Object.getClass()
첫번쨰 방법은 객체 인스턴스의 getClass() 메서드로 얻을 수 있습니다.
아래 코드에서는 Java의 Class 객체를 사용하여 런타임에 특정 객체의 클래스 타입을 가져옵니다.
이를 통해 각 객체의 런타임 타입(runtime type) 정보를 얻을 수 있습니다.
String str = "some-string";
Car car = new Car();
Map<String, Integer> sortedMap = new TreeMap<>();
Class<String> strClass = str.getClass();
Class<Car> carClass = car.getClass();
Class<?> mapClass = sortedMap.getClass();
위 코드에서 특이사항은 sortedMap
변수입니다.
Map<String, Integer> 타입으로 선언되었지만 실제 구현체는 TreeMap 이므로 mapClass
는 TreeMap 클래스 타입을 참조합니다.
즉, 제네릭 타입 정보인 <String, Integer>는 런타임 시 삭제되므로 Class<?>
로 표현됩니다.
런타임 결과 : mapClass.getName() -> "java.util.TreeMap"
또 1가지 유의해야할 점은 Primitive Type은 객체 클래스에서 상속되지 않으므로, getClass()를 호출할 수 없으며 따라서 변수 인스턴스에서 타입 정보를 수 없습니다.
타입명에 ".class" 추가
.class
를 사용하여 Class 객체를 얻는 방법은 Java에서 정적(static)으로 특정 타입의 런타임 클래스 정보를 가져오는 가장 간단하고 직관적인 방법입니다..
이 메서드는 클래스의 인스턴스가 없을떄 주로 사용됩니다. 이 말은 Primitive Type의 타입 정보도 가져올 수 있다는 의미입니다.
Class<String> strClass = String.class;
Class<Car> carClass = Car.class;
Class<?> mapClass = TreeMap.class;
// Primitive Type
Class<Integer> intClass = int.class;
Class<Void> voidClass = void.class;
// 익명, 중첩 Class
Class<?> anonymousClass = new Object() {}.getClass();
Class<OuterClass.InnerClass> innerClass = OuterClass.InnerClass.class;
Class.forName()
Class.forName() 메서드는 Java에서 런타임에 클래스 이름을 문자열로 제공하여 해당 클래스의 Class 객체를 로드하고 반환하는 데 사용됩니다.
이 메서드는 리플렉션(reflection) 과 함께 동적 클래스 로딩에 자주 사용됩니다.
즉, forName()을 사용해 패키지명을 포함한 클래스 경로에서 동적으로 클래스를 찾을 수 있습니다.
아래 코드처럼 패키지명을 포함한 클래스 경로를 forName의 파라미터로 넣어주어 클래스 정보를 알 수 있고,
내부 클래스의 정보에 접근하려면 $
기호를 사용해 아래 $Engine
과 같이 상위 클래스와 하위 클래스 사이에 기호를 사용하면 됩니다.
Class<?> strType = Class.forName("java.lang.String");
Class<?> catType = Class.forName("vehicles.Car");
Class<?> engineType = Class.forName("vehicles.Car$Engine");
이 방법에서도 Primitive Type에 대한 Class 객체를 얻을 수 없으며, 만약 시도한다면 Runtime에서 ClassNotFoundException이 나게 됩니다.
그리고 3가지 방법중 이 방법에 Class 객체를 얻는 방법 중 가장 위험한 방법일 수 있습니다.
보통 이 방법을 사용하는 경우는, 인스턴스를 확인하거나 만들려는 타입이 별도의 config 파일이나 custom 파일에서 전달될 떄 이 방법을 사용합니다.
그래서 이걸 사용하면 소스코드를 변경하거나 리컴파일 없이 yml이나 xml 파일만 수정해도 어플리케이션의 동작을 변경할 수 있습니다.
또 다른 경우는, 확인하려는 클래스가 프로젝트가 없고, 코드를 컴파일할 떄 해당 클래스가 없을떄 사용합니다.
예를 들면 실행중인 앱의 ClassPath에 외부에 있는 클래스를 불러와서 사용하는 앱과 분리해 별도의 라이브러리를 구축하는 등의 경우입니다.
📚 클래스 타입(Class)과 와일드카드(Class<?>)의 차이
제네릭 클래스의 상위/하위 관계
Java 제네릭은 다음과 같은 규칙을 따릅니다
- 제네릭 클래스 간에는 상하위 관계가 없다
- Class
와 Class - 하지만, Class
은 Class<?>의 하위 타입으로 간주됨.
와일드카드의 역할
- Class<?>는 모든 제네릭 클래스 타입의 상위 타입으로 작동.
- 따라서, Class
, Class 등 모든 클래스 타입이 Class<?>로 대입 가능.
Class<String> stringClass = String.class;
Class<?> anyClass = stringClass; // 가능: Class<String>은 Class<?>의 하위 타입
// 반대는 불가
Class<?> wildcardClass = String.class;
// Class<String> specificClass = wildcardClass; // 컴파일 에러
Class<T>
기본 개념
- Class
는 제네릭 타입 T를 명시하여 특정 타입의 클래스 객체를 나타냅니다. - 컴파일러는 제네릭 타입을 통해 타입 안전성(type safety)을 보장하며, 타입이 정확히 T임을 보장합니다.
타입 안전성의 의미
- Class
를 사용하면 컴파일러가 잘못된 타입 사용을 방지합니다. - 예를 들어, Class
은 String 타입에만 한정되므로 다른 타입(예: Integer)과의 혼동이 없습니다.
Class<?>
기본 개념
- Class<?>는 모든 클래스 타입을 허용하는 와일드카드 제네릭 타입입니다.
- 어떤 타입이든 허용되지만, 타입 정보는 불확실하므로 제네릭의 상속 관계를 명확히 구분하지 못합니다.
예시
Class<?> anyClass = String.class; // String 클래스
anyClass = Integer.class; // Integer 클래스
anyClass = TreeMap.class; // TreeMap 클래스
제한점
- Class<?>는 타입을 알 수 없는 상태로 선언되므로, 컴파일러는 타입 안정성을 보장하지 않습니다.
- 예를 들어, Class<?> 타입에서는 특정 타입으로의 안전한 캐스팅을 컴파일러가 허용하지 않습니다.
와일드카드를 사용해 클래스를 메서드로 전달
매개변수가 특정한 클래스와 인터페이스만 실행하도록 제한하는 방법입니다.
public List<String> findAllMethods(Class<? extends Collection> clazz) {}