[Java] Reflection은 무엇이고 언제/어떻게 사용하는 것이 좋을까?

mhyun, Park·2022년 7월 25일
3

Reflection이란?

구체적인 Class Type을 알지 못하더라도 해당 Class의 method, type, variable들에 접근할 수 있도록 해주는 자바 API이며, 컴파일된 바이트 코드를 통해 Runtime에 동적으로 특정 Class의 정보를 추출할 수 있는 프로그래밍 기법이다.

Class class = Class.forName("클래스 이름");

정적 바인딩 vs 동적 바인딩

Reflection에 들어가기에 앞서, 정적 바인딩과 동적 바인딩에 대해 간략하게 설명하려 한다.

바인딩이란 프로그램에 사용된 구성 요소의 실제 값 또는 프로퍼티를 결정짓는 행위를 의미한다. 즉, 프로그램에서 사용되는 변수나 메서드 등 모든 것들이 결정되도록 연결해주는 것을 뜻한다. 바인딩을 결정 짓는 시점에 따라 정적 바인딩 , 동적 바인딩으로 나뉘게 되는데

정적 바인딩을 쉽게 대표하는 예제로 오버로딩(overroding)을 들 수 있다. Compile 과정에서 어떤 메서드를 호출할지 결정하기 때문에 코드 작성 단계에서 어떤 메서드를 사용할지 구분할 수 있고 잘못 사용 했을 경우 Complie error를 발생시킨다.

반대로 동적 바인딩을 대표하는 예제로는 오버라이딩(Overriding)을 들 수 있다. 상속을 이용한 부모-자식 관계일 경우 부모에서 정의한 메서드를 자식에서 Overriding 했다 가정했을 때, 해당 메서드를 이용할 경우 Runtime에 어떤 메서드를 호출할지가 정해진다.

이렇게 동적 바인딩은 실행 시간에 Binding 되다보니 정적 바인딩보다는 성능상 오버헤드가 있지만, 동적 바인딩을 통해 상속과 다형성 등 다양한 기능을 사용할 수 있는 장점이 있다.

Reflection은 언제 사용할까?

앞서 설명했던 것을 토대로 생각해보면, Reflection은 Runtime에 Class Type을 모르는채로 객체를 생성하고 이용하기 때문에 동적 바인딩을 제공한다.

이러한 Reflection은 다음과 같이 많은 곳에서 활용될 수 있다.

  • 동적으로 Class를 사용 해야할 경우
    코드 작성 시점에서는 어떠한 Class를 사용해야할지 모르지만 Runtime에 Class를 가져와서 실행해야하는 경우 (Spring Annotation)

  • Test Code 작성
    private 변수를 변경하고 싶거나 private method를 테스트할 경우

  • 자동 Mapping 기능 구현
    IDE 사용 시 Da 입력만해도 이와 관련된 Class 혹은 Method 목록들을 IDE가 먼저 확인하고 사용자에게 제공한다.

  • Jackson, GSON 등의 JSON Serialization Library
    Reflection을 사용하여 객체 필드의 변수명/어노테이션명을 Json key와 mapping 해주고 있다.

  • 정적 분석 tool

  • Hidden API 사용하는 경우
    SDK에 API가 공개되지 않은 경우 Android Studio에서 참조할 수 없어 호출할 수 없지만, 실제로 hidden API가 존재하는 경우 Reflection으로 접근한다. (Android Q 부터 비 SDK 인터페이스 제한 )

지금 말한 Reflection의 활용 주제 중, 가장 기대 효과가 높은 부분은 바로 동적으로 Class를 사용해야 하는 경우 이다.
이러한 경우는 실제로 Framework 기반의 코드에선 빈번히 사용되니 간단한 예를 들어 설명 해보려한다.

한 개발자는 차세대 자동차 개발을 위해 자동차 모델에 따라 적합한 플러그인을 선택하여 특정 기능들을 제공해야하는 Task를 맡았다. 특정 기능들은 시스템 테마, 네비게이션, 주행 모드 등과 같고 이러한 Library는 vendor 회사 및 version 마다 아래처럼 다양하게 지원되고 있었다.

[지원 가능한 전체 feature list]

/* (1) 시스템 테마 */
v1_HYUNDAI_시스템_테마
v2_HYUNDAI_시스템_테마
v1_KIA_시스템_테마

/* (2) 네비게이션 */
v1_TMAP_네비게이션
v2_TMAP_네비게이션
v3_TMAP_네비게이션
v1_KAKAO_네비게이션
v1_NAVER_네비게이션

/* (3) 주행 모드 */
v1_HYUNDAI_컴포트_모드
v1_KIA_컴포트_모드

v1_HYUNDAI_스포츠_모드
v2_HYUNDAI_스포츠_모드
v1_KIA_스포츠_모드

Monitor의 Model 혹은 OS 별로 지원되는 Library는 다르기에 이 개발자는 초기에 if/else를 통해 상황에 따라 적합한 Library 객체를 생성했다고 가정하자.

public class ThemeLibFactory {

	private static final List<String> featureList = SystemProperties.get("lib.features");
    // List.of("v2_HYUNDAI_시스템_테마", "v3_TMAP_네비게이션", "v1_HYUNDAI_컴포트_모드", "v2_HYUNDAI_스포츠_모드");
    
    public BaseLibrary createLib(String featureName) {
    	switch(featureName) {
        	case "v1_HYUNDAI_시스템_테마"
            	return v1_HYUNDAI_시스템_테마();
        	case "v2_HYUNDAI_시스템_테마"
            	return v2_HYUNDAI_시스템_테마();
        	case "v1_KIA_시스템_테마"
            	return v1_KIA_시스템_테마();
        	case "v1_TMAP_네비게이션"
            	return v1_TMAP_네비게이션();
        	case "v2_TMAP_네비게이션"
            	return v2_TMAP_네비게이션();
        	case "v3_TMAP_네비게이션"
            	return v3_TMAP_네비게이션();
        	case "v1_KAKAO_네비게이션"
            	return v1_KAKAO_네비게이션();
        	case "v1_NAVER_네비게이션"
            	return v1_NAVER_네비게이션();
        	case "v1_HYUNDAI_컴포트_모드"
            	return v1_HYUNDAI_컴포트_모드();
        	case "v1_KIA_컴포트_모드"
            	return v1_KIA_컴포트_모드();
        	case "v1_HYUNDAI_스포츠_모드"
            	return v1_HYUNDAI_스포츠_모드();
        	case "v2_HYUNDAI_스포츠_모드"
            	return v2_HYUNDAI_스포츠_모드();
        	case "v1_KIA_스포츠_모드"
            	return v1_KIA_스포츠_모드();
            default
            	throw new IllegalArgumentException("Not supported feature library");
        }
	}
   

이렇게 하나의 Task가 끝나고 세달 뒤...
T map에서 기존 v3 네비게이션을 개선한 새로운 v4 네비게이션을 내놓았다. 해당 Library는 최신 OS에서만 작동할 수 있기에 이 개발자는 else if 문을 추가하여 대응을 마쳤다. 하지만... 또 1년 뒤... v5 Library를 내놓았다..........

Factory 클래스의 가장 큰 문제는 if/else문의 사용이다. 객체 지향 프로그램에선 대량의 if/else문 사용은 객체지향 철학을 어긴 것이기 때문에 이 개발자는 해당 구조를 벗어날 필요가 있다고 생각하여 Reflection을 이용해서 리팩토링을 진행했다.

public class ThemeLibFactory {

	private static final List<String> featureList =  SystemProperties.get("lib.features");
    // List.of("v2_HYUNDAI_시스템_테마", "v3_TMAP_네비게이션", "v1_HYUNDAI_컴포트_모드", "v2_HYUNDAI_스포츠_모드");
                                                            
	public static BaseLibrary getInstance(String featureName) throws Exception {
    	try {
        	Class libClass = Class.forName(PACAKAGE_NAME + featureName);
            Constructor constructor = libClass.getConstructor();
            return (BaseLibrary) constructor.newInstance();
        } catch (Exception e) {
            throw new Exception("Not supported feature library: " + e);
        }
    }
}

해당 예제에서 보는 바와 같이, Constructor 객체를 이용하여 Lib 객체를 생성했다.
이 경우, Controller에서 Class의 이름을 넘겨주어야 하지만, 새로운 lib이 생기더라도 Factory 클래스의 수정 없이 유연하게 확장 가능한 코드가 된 것이다.

Reflection 사용 방법

Reflection은 아래와 같은 정보를 가져올 수 있으며

  • Class/Interface
  • Constructor
  • Method
  • Field

해당 정보들을 통해 (1) 객체 생성 (2) 메서드 호출 (3) 변수 값을 변경할 수 있다.

[1] Class / Interface

public static void main(String[] args) throws Exception {
	// 1. class를 알고 있을 경우
    Class car = Car.class;
    
    // 2. class 이름만 알고 있을 경우
    Class car = Class.forName("com.reflection.test.Car");
    // class.getName() -> com.reflection.test.Car
 
    // 3. Default 생성자를 이용한 객체 생성
    Car realCar = car.newInstance();
    
    // 4. class에 구현된 interface 확인
    Class[] interfaces = car.getInterfaces();
}

[2] Constructor

public static void main(String[] args) throws Exception {
    Class car = Class.forName("com.reflection.test.Car");
    
    // 1. 인자가 없는 생성자 가져오기
    Constructor constructor = car.getDeclaredConstructor();
    
    // 2. String 인자를 가진 생성자 가져오기
    Constructor constructor = car.getDeclaredConstructor(String.class);
    
    // 3. 모든 생성자 가져오기
    Constructor constructors[] = car.getDeclaredConstructors();
    
    // 4. public 생성자만 가져오기
    Constructor constructors[] = car.getConstructors();
    // public com.reflection.test.Car()
	// public com.reflection.test.Car(java.lang.String)
    
    // 5. 생성자를 이용한 객체 생성
    Car realCar = constructor.newInstance();
}

[3] Method

public static void main(String[] args) throws Exception {
    Class car = Class.forName("com.reflection.test.Car");
  
    // 1. 인자가 없는 method 가져오기
    Method method = car.getDeclaredMethod("move");
    
    // 2. String 인자를 가진 method 가져오기
    Method method = car.getDeclaredMethod("move", String.class);
    
    // 3. 모든 method 가져오기
    Method methods[] = car.getDeclaredMethods();
    
    // 4. 상속받은 method와 public method 가져오기
    Method methods[] = car.getMethods();
	// public void com.reflection.test.Car.move()
	// public void com.reflection.test.Car.move(java.lang.String)
    
    // 5. method 호출
    Class realCar = car.newInstance();
    method.invoke(realCar, /*인자*/);
    
    // 6. 접근 제한자를 무시한 method 호출.
    method.setAccessible(true);
    method.invoke(realCar, /*인자*/);
}

[4] Field

public static void main(String[] args) throws Exception {
    Class car = Class.forName("com.reflection.test.Car");
    
    // 1. car 객체에서 name 에 해당하는 field 가져오기
    Field field = car.getDeclaredField("name");
    
    // 2. car + car super 객체를 포함하여 name에 해당하는 field 가져오기
    Field field = car.getField("name");
    
    // 3. car 객체에 선언된 모든 field 가져오기
    Field[] fields = car.getDeclaredFields();
    // private java.lang.String com.reflection.test.Car.name
	// public java.lang.Integer com.reflection.test.Car.type
    
    // 4. car + car super 객체의 모든 public field 가져오기
    Field[] fields = car.getFields();
	// public java.lang.Integer com.reflection.test.Car.age
}

[5] Field 값 변경

public static void main(String[] args) throws Exception {
    Class class = Class.forName("com.reflection.test.Car");
    Constructor constructor = class.getConstructor()
    Car car = constructor.newInstance()
        
    Field field = car.getField("name");
    
    // 1. public field 일 경우
    field.set(car, "아반떼");
    
    // 2. private field 일 경우
    field.setAccessible(true);
    field.set(car, "아반떼");
}

Reflection의 장점과 단점

장점

  • 확장성
    대량의 if/else문을 사용하는 것보다 Reflection을 이용하여 재사용 가능한 컴포넌트로 만들 수 있다.

  • Class 브라우저 및 시각적 개발 환경을 제공
    Reflection을 통해 Method, Property, Constructor를 미리 파악함으로써 사용할 정보를 열거하여 시각적 개발 환경을 구축할 수 있다.

  • 디버거 및 테스트 도구
    디버거는 개인 Property, Method, Constructor를 검사할 수 있어야 한다. 테스트 장치는 Reflection을 사용하여 클래스에 정의된 발견 가능한 세트 API를 체계적으로 호출하여 테스트에서 높은 수준의 코드 커버리지를 보장할 수 있다.

  • 라이브러리 파악
    Java에서 지원하는 라이브러리가 아닌 특정 기업의 라이브러리를 사용하는 경우 해당 라이브러리에 존재하는 클래스 및 메서드를 분석할 수 있다.

단점

  • 컴파일 시점에 가는한 타입 확인이 불가능하여 캄파일 시에 타입 확인이나 예외 검사를 할 수 없다.

  • 클래스,메서드,필드를 접근하여 직접 이용하기 때문에 객체 지향 프로그래밍의 특징인 추상화를 위반한다.

  • 컴파일 에러가 아닌 런타임시에 에러가 발생하기 때문에 코드 운용에 위험이 있다.

마무리

내가 그의 이름을 불러 주기 전에는 그는 다만 하나의 몸짓에 지나지 않았다. 내가 그의 이름을 불러 주었을 때, 그는 나에게로 와서 꽃이 되었다.
– 김춘수, “꽃” -

1. Java 리플렉션 (Reflection)이란?
2. 자바 Reflection이란?
3. 자바 리플렉션에 대한 오해와 진실

profile
Android Framework Developer

4개의 댓글

comment-user-thumbnail
2023년 5월 9일

상세한 내용 및 정리 많은 도움되었습니다! 감사합니다.

1개의 답글
comment-user-thumbnail
2024년 4월 29일

감사합니다. 글 작성시 출처 남기고 참고하겠습니다!

1개의 답글