리플렉션(Reflection)

Reflection이란?

JDK document에 따르면, java.lang.reflect 패키지는 클래스와 객체에 대한 반사적인(reflective) 정보를 얻기 위한 클래스와 인터페이스를 제공합니다. 반사적인 정보란 클래스의 필드, 메소드, 생성자에 대한 정보를 말하며, 이를 활용해 객체를 생성하거나 메소드를 호출하는 등의 작업을 수행할 수 있습니다.

 

프로그래밍에서 Reflection은 반사 현상과 비슷한 개념으로, 코드가 자신의 구조나 동작 방식을 반사하여 프로그램에서 객체를 동적으로 생성하거나 객체의 정보를 얻어내는 등의 작업을 할 수 있게 합니다.

 

https://data-flair.training/blogs/reflection-in-java/

 

결국은 리플렉션은 런타임 시에 클래스의 정보를 분석하고, 해당 클래스의 객체를 생성하거나 메소드를 호출하는 등의 작업을 수행합니다. 즉, 리플렉션을 사용하면 런타임 시에 코드를 동적으로 조작할 수 있습니다.


Relection 톺아보기

위에서 설명했듯이 런타임 시점에서 클래스의 정보를 가져오고, 객체를 생성하거나 메소드를 호출하는 등의 작업을 할 수 있는 기능입니다. 그럼 각각의 기능을 어떻게 사용할 수 있는 것인지 알아보겠습니다. 

 

.class

.class는 클래스 리터럴(class literal)이라고도 불리는 특수한 표기법입니다. 클래스 리터럴은 클래스에 대한 정보를 나타내는 Class 객체를 가져올 수 있는 방법 중 하나입니다.

 

Class객체는 인스턴스 인가?
class를 통해서도 클래스 객체를 가져올 수 있지만, 이는 클래스의 정보(metadata)를 담고 있는 객체를 생성하는 것이며, 클래스의 인스턴스(instance)를 생성하는 것은 아닙니다. 클래스의 인스턴스를 생성하려면 기본 생성자나 다른 생성자를 사용하여 생성해야 합니다.

 

클래스의 이름, 필드, 메소드 등의 정보 가져오기

Class<?> clazz = MyClass.class;
// Class<?> clazz = Class.forName("com.example.MyClass");

String className = clazz.getName(); // 클래스 이름 가져오기
Field[] fields = clazz.getDeclaredFields(); // 클래스 필드 가져오기
Method[] methods = clazz.getDeclaredMethods(); // 클래스 메소드 가져오기

위 코드에서 MyClass는 가져올 클래스의 이름입니다. Class 클래스는 자바의 리플렉션 API 중 가장 기본이 되는 클래스이며, forName() 메소드나 .class를 이용하여 클래스를 로딩하고, 이를 이용하여 클래스의 정보를 가져올 수 있습니다.

 

동적으로 객체 생성하기

Class<?> clazz = MyClass.class;
MyClass instance = (MyClass) clazz.newInstance();

newInstance() 메소드를 호출하고 다운캐스팅을 통해 클래스의 인스턴스를 동적으로 생성할 수 있습니다.

 

메소드 호출하기

Class<?> clazz = MyClass.class;
MyClass instance = (MyClass) clazz.newInstance();
Method method = clazz.getDeclaredMethod("methodName", String.class);
String result = (String) method.invoke(instance, "argument");

 

getDeclaredMethod() 메소드를 이용하여 메소드를 가져오고, invoke() 메소드를 이용하여 메소드를 실행할 수 있습니다. invoke() 메소드는 메소드의 반환값을 Object로 반환하므로, 반환값의 타입에 맞게 캐스팅해주어야 합니다.

 

필드 값 읽기/쓰기

Class<?> clazz = MyClass.class;
MyClass instance = (MyClass) clazz.newInstance();
Field field = clazz.getDeclaredField("fieldName");

// 필드 값 읽기
field.setAccessible(true);
Object value = field.get(instance);

// 필드 값 쓰기
field.set(instance, newValue);

getDeclaredField() 메소드를 이용하여 필드를 가져오고, get() 메소드를 이용하여 필드 값을 읽을 수 있습니다. 필드 값을 쓰기 위해서는 set() 메소드를 이용합니다. setAccessible(true) 메소드를 호출하여 접근 제한자를 무시하고 필드에 접근할 수 있도록 해야 합니다.


리플렉션 활용

리플렉션은 다양한 자바 라이브러리와 프레임워크에서 사용되고 있습니다. 이를 통해 런타임 시점에서 다음과 같은 작업들이 가능해집니다.

 

DI 프레임워크

DI(Dependency Injection) 프레임워크는 객체 간의 의존 관계를 관리하고, 객체 생성과 의존성 주입을 자동으로 처리해주는 프레임워크입니다. 이를 구현하기 위해 리플렉션을 사용하여 객체를 동적으로 생성하고, 필드에 값을 설정합니다.

 

예시를 들어보겠습니다.

public class MyService {
    private MyDependency dependency;

    public void setDependency(MyDependency dependency) {
        this.dependency = dependency;
    }

    public void doSomething() {
        // 의존성 주입된 객체 사용
        dependency.doWork();
    }
}

public class MyDependency {
    public void doWork() {
        // 작업 수행
    }
}

위의 코드에서 MyService 클래스는 MyDependency에 의존하는 클래스입니다. DI 프레임워크를 사용하여 MyService 객체를 생성하고, MyDependency 객체를 주입해보겠습니다.

Class<?> serviceClass = MyService.class;
Object serviceObject = serviceClass.newInstance();

Class<?> dependencyClass = MyDependency.class;
Object dependencyObject = dependencyClass.newInstance();

Field dependencyField = serviceClass.getDeclaredField("dependency");
dependencyField.setAccessible(true);
dependencyField.set(serviceObject, dependencyObject);

Method doSomethingMethod = serviceClass.getDeclaredMethod("doSomething");
doSomethingMethod.invoke(serviceObject);

위의 코드에서는 리플렉션을 이용하여 MyService 클래스와 MyDependency 클래스의 객체를 생성합니다. 그리고 MyService 객체의 dependency 필드에 MyDependency 객체를 주입합니다. 마지막으로 doSomething 메소드를 호출하여 의존성이 주입된 객체를 사용합니다.

 

객체 매퍼

ORM(Object-Relational Mapping)에서는 데이터베이스의 테이블과 자바 객체 간의 매핑 작업이 필요합니다. 이를 위해 리플렉션을 사용하여 자바 객체의 필드와 데이터베이스의 컬럼 간의 매핑을 수행합니다.

public class MyObject {
    private int id;
    private String name;
    private int age;

    // Getter and Setter methods
}

public class MyMapper {
    public MyObject map(ResultSet rs) throws SQLException {
        MyObject obj = new MyObject();

        Field[] fields = MyObject.class.getDeclaredFields();
        for (Field field : fields) {
            String columnName = getColumnName(field);
            Object value = rs.getObject(columnName);
            setFieldValue(obj, field, value);
        }

        return obj;
    }

    private String getColumnName(Field field) {
        // 필드에 대한 컬럼명을 반환하는 로직
    }

    private void setFieldValue(Object obj, Field field, Object value) throws IllegalAccessException {
        field.setAccessible(true);
        field.set(obj, value);
    }
}

위의 예시 코드에서 MyMapper 클래스의 map() 메소드에서는 ResultSet 객체를 매개변수로 받아 MyObject 객체를 생성합니다. 이때 MyObject 클래스의 필드 정보를 얻기 위해 리플렉션을 사용하고, getColumnName() 메소드를 사용하여 필드에 대한 컬럼명을 얻어옵니다. 그리고 setFieldValue() 메소드를 사용하여 MyObject 객체의 필드에 데이터베이스 레코드의 컬럼 값을 할당합니다. 이를 통해 자바 객체와 데이터베이스 레코드 간의 매핑을 수행합니다.

 

로깅

로깅을 위해 자바에서 가장 많이 사용하는 라이브러리 중 하나인 Log4j를 예시로 설명해보겠습니다.

 

Log4j에서는 로그를 출력할 때 로그 레벨, 로그 메시지, 클래스 이름, 메소드 이름 등의 정보를 함께 출력합니다. 이를 위해 Log4j는 리플렉션을 사용하여 클래스 이름과 메소드 이름을 가져옵니다.

 

예를 들어, 아래와 같은 메소드가 있다고 가정해봅시다.

public class MyObject {
    private static final Logger logger = LogManager.getLogger(MyObject.class);

    public void myMethod() {
        logger.info("Hello, world!");
    }
}

위 코드에서 LogManager.getLogger(MyObject.class)는 MyObject 클래스의 이름을 가져와서 해당 이름으로 Logger 객체를 생성합니다.

 

logger.info("Hello, world!")는 info 레벨의 로그를 출력하는 코드입니다. 이때 로그 메시지인 "Hello, world!" 외에도 클래스 이름과 메소드 이름을 함께 출력하려면 아래와 같이 코드를 작성해야 합니다.

StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
String className = stackTrace[2].getClassName();
String methodName = stackTrace[2].getMethodName();
logger.info(className + "." + methodName + " - " + "Hello, world!");

위 코드에서 Thread.currentThread().getStackTrace()는 현재 실행 중인 스레드의 호출 스택을 가져옵니다. 호출 스택에서 2번째 요소가 바로 현재 메소드를 호출한 메소드입니다. 이 메소드의 클래스 이름과 메소드 이름을 가져와서 로그 메시지에 추가하면 됩니다.

 

위 코드는 일일이 호출 스택을 가져와서 클래스 이름과 메소드 이름을 추출해야 하기 때문에 번거롭습니다. 이를 간편하게 처리하기 위해 Log4j에서는 리플렉션을 사용합니다.

 

아래 코드는 Log4j가 리플렉션을 사용하여 클래스 이름과 메소드 이름을 가져와서 로그 메시지에 추가하는 코드입니다.

public void logMessage() {
    StackTraceElement element = Thread.currentThread().getStackTrace()[2];
    logger.log(Level.INFO, "Hello, world!", element);
}

 

자바 빈즈

자바 빈즈(JavaBeans)는 자바에서 재사용 가능한 구성 요소를 만들기 위한 규약입니다. 이 규약에 따라 작성된 클래스를 자바 빈즈라고 부르며, 이 클래스는 캡슐화(encapsulation)되어 있으며, 일반적으로 다음과 같은 특징을 가지고 있습니다.

 

  • 클래스의 속성은 private으로 선언하고, getter/setter 메소드를 사용하여 접근합니다.
  • 클래스는 파라미터가 없는 생성자를 제공합니다.
  • Serializable 인터페이스를 구현하여 직렬화(serialization)가 가능합니다.

 

Serializable ?
직렬화(Serialization)는 자바 객체를 외부 저장소에 저장하거나 네트워크를 통해 전송하기 위해 객체를 바이트 스트림으로 변환하는 과정을 말합니다. 이를 통해 객체를 영속화(Persistence)하거나, 다른 시스템과 통신할 때 객체를 전송할 수 있습니다. 직렬화된 객체는 바이트 스트림 형태로 저장됩니다. 반대로, 직렬화된 바이트 스트림을 역직렬화(Deserialization)하여 객체로 변환할 수도 있습니다.

 

바이트 스트림?
바이트 스트림(byte stream)은 데이터를 바이트(byte) 단위로 읽고 쓰는 입출력 스트림입니다. Java에서는 InputStream과 OutputStream 클래스가 바이트 스트림을 처리하는데 사용됩니다. 바이트 스트림은 주로 이미지, 비디오, 오디오 등의 바이너리 데이터를 처리할 때 사용됩니다.

 

자바 빈즈를 구현할 때 리플렉션을 이용하여 객체의 프로퍼티 정보를 가져오고, 프로퍼티 값을 읽거나 쓰는 작업을 수행할 수 있습니다. 이를 위해서는 클래스의 getter/setter 메소드를 찾아야 합니다.

 

다음은 자바 빈즈의 예시 코드입니다.

public class Person {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public class BeanInfoTest {
    public static void main(String[] args) throws IntrospectionException, IllegalAccessException, InvocationTargetException {
        BeanInfo beanInfo = Introspector.getBeanInfo(Person.class);
        PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
        Person person = new Person();

        for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
            Method setter = propertyDescriptor.getWriteMethod();
            if (setter != null) {
                if (propertyDescriptor.getName().equals("name")) {
                    setter.invoke(person, "John Doe");
                } else if (propertyDescriptor.getName().equals("age")) {
                    setter.invoke(person, 30);
                }
            }
        }

        System.out.println(person.getName());
        System.out.println(person.getAge());
    }
}

PropertyDescriptor 클래스를 이용하여 Person 클래스의 프로퍼티 정보를 가져옵니다. getWriteMethod() 메소드를 이용하여 해당 프로퍼티에 대한 setter 메소드를 가져옵니다.

 

그리고 나서, setter 메소드를 이용하여 객체의 값을 설정합니다. 만약 프로퍼티의 이름이 "name"인 경우, "John Doe"라는 값을 설정하고, 프로퍼티의 이름이 "age"인 경우에는 30이라는 값을 설정합니다.


리플렉션의 장단점

 

리플렉션의 장점

  • 유연성: 리플렉션을 사용하면 컴파일 시간에 알 수 없는 객체의 메소드, 필드, 생성자 등의 정보를 동적으로 확인하고 조작할 수 있습니다. 이를 통해 런타임 시에 유연한 작업을 수행할 수 있습니다.
  • 프레임워크와 라이브러리 개발: 리플렉션은 프레임워크와 라이브러리 개발에 유용합니다. 외부에서 라이브러리의 클래스, 메소드, 필드 등에 접근할 수 있도록 리플렉션을 사용할 수 있습니다.

 

리플렉션의 단점

  • 성능 저하: 리플렉션은 실행 시간에 객체의 정보를 확인하고 조작하기 때문에, 일반적인 코드 작성 방식보다 성능이 떨어질 수 있습니다.
  • 보안 취약성: 리플렉션을 사용하면 객체의 정보를 동적으로 확인하고 조작할 수 있습니다. 이러한 기능은 악의적인 사용자에게 보안 취약점을 제공할 수 있습니다.
  • 가독성 저하: 리플렉션을 사용하면 코드의 가독성이 떨어질 수 있습니다. 특히, 클래스, 메소드, 필드 등의 이름을 문자열로 사용하는 것은 코드의 가독성을 떨어뜨릴 수 있습니다.
  • 예외 처리: 리플렉션을 사용할 때 예외 처리가 중요합니다. 객체에 접근할 수 없는 경우 NoSuchMethodException, NoSuchFieldException 등의 예외가 발생할 수 있습니다. 이러한 예외를 적절하게 처리해야 합니다.

'Language > Java' 카테고리의 다른 글

Optional<T>  (0) 2023.05.18
스트림(Stream)  (0) 2023.05.18
제네릭스<Generics>  (0) 2023.05.03
Collections Framework  (0) 2023.04.26
객체 지향 프로그래밍(OOP)  (0) 2023.04.25