본문 바로가기
공부했던 내용

Reflection과 Annotation

by UpperLeaf 2020. 8. 10.

Reflection이란?

객체를 통해 클래스의 정보를 분석해내는 프로그램 기법입니다.

기본적으로 제공하는 자바의 API이다. 자바의 Reflection은 클래스, 인터페이스, 메서드들을 찾을 수 있고, 객체를 생성하거나 변수를 변경할 수 있고, 메서드를 호출할 수 도 있습니다.

class Person{

    public String name;
    private int age;

    public Person() {
    }

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

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }
}

 

다음과 같이 간단한 String형 name변수와 int형 age변수를 가진 Person이 있다고 했을때 해당 클래스의 Reflection을 얻는방법은 여러가지가 있습니다.

첫번째로는 class.forName("path/Person")
두번째로는 인스턴스 person.getClass()
세번째로는 Person.class

이렇게 3가지의 경로로 Reflection을 얻을 수 있습니다.

 

Class<? extends Person> personClass = person.getClass();

 

저는 이중에서 2번째의 케이스를 선택했습니다. 1번째와 3번째의 케이스는 Class Raw타입을 사용하거나 캐스팅을 사용해야하는데 뭔가 제 생각은 깔끔한것 같지 않아서 특별한 상황이 아닌이상 Raw타입을 사용하지도 않고 캐스팅도 하지않는 이 방법이 가장 좋았던것 같아요

 

Reflection객체를 얻었다면 해당 Reflection을 통해 여러가지를 할 수 있습니다!

첫번째로는 클래스의 내부 정보를 가져올 수 있습니다.

 

for(Field field : personClass.getFields()){
        System.out.println(field.getName());
}

 

getFields()메서드는 클래스 내의 선언된 필드들 즉 변수들의 정보를 가져옵니다. 이때 접근제어자가 public인 경우의 필드만 가져오고, 상속으로 생긴 변수까지 전부 가져오게 됩니다.

 

for(Field field : personClass.getDeclaredFields()){
        System.out.println(field.getName());
}

 

getDeclaredFields()메서드는 클래스 내에서 선언한 변수들의 정보를 가져옵니다. 상속으로 생긴 변수들은 가져오지 않으며 private접근제어를 가진 변수까지 가져오는것을 확인할 수 있습니다.

 

for(Method method : personClass.getDeclaredMethods()){
        System.out.println(method.getName());
        for(Parameter parameter : method.getParameters()){
                System.out.println(parameter.getName());
        }
}

 

Method의 정보도 가져올 수 있습니다. getMethods()와 getDeclaredMethods가 존재하며 차이점은 Field와 비슷하게 동작합니다.

Method객체는 자신의 메서드가 가지고있는 파라미터에 대한 정보도 가지고 있으며, method.getParameters()를 통해서 파라미터들을 가져올 수 있습니다.

 

Reflection은 메서드 호출까지 가능하고 다음과 같이 메서드를 호출할 수 있습니다.

 

personClass.getDeclaredMethod("setAge", int.class).invoke(person, 20);
System.out.println(personClass.getDeclaredMethod("getAge").invoke(person));

간단하게 set메서드와 get메서드를 가져와서 invoke라는 함수를 통해 호출이 가능합니다.
invoke함수는 인자로 메서드를 호출할 객체의 인스턴스 obj와 해당 메서드의 파라미터로 전달할 Object... args를 인자로 전달받습니다.

 

그렇기 때문에 set메서드의 경우 Integer 객체로 20을 전달해주었고, get메서드의 경우에는 아무것도 전달해주지 않았습니다.

 

Annotation이란?

어노테이션은 자바5부터 등장한 기능입니다. 흔히 자바를 공부해보셨다면 @Override 또는 @Deprecated와 같은 어노테이션을 보신적이 있을거에요

Annotation이 정확하게 무슨일을 하는지는 몰라도 @Override 또는 @Deprecated의 역할을 보면 Annotation이 어떤역할을 하는지 약간은 감을 잡으실 수 있습니다.

 

@Override같은 경우 상속받은 클래스의 메서드를 오버라이딩 한다는 의미를 가집니다.
@Deprecated같은 경우 더이상 사용되지 않는 문법으로 사용하지 않은것을 권장하는 어노테이션입니다.

 

@Override같은 경우 만약 오버라이딩 대상이 아닌 메서드에 붙힐경우 컴파일에러를 발생시키지만 런타임에서는 코드자체가 존재하지 않습니다. "즉 프로그래머에게 이 메서드는 오버라이딩 된 메서드야! " 라는 정보를 알려주는 메타데이터라고 할 수 있고, 실수를 피하기 위한 어노테이션이라고 생각할 수 있습니다.

 

@Deprecated는 사용할 수는 있지만 사용자에게 이 문법은 위험한 문법이며 사용하지 않는걸 추천한다는 의미를 가진 일종의 메타 데이터라고 할 수 있습니다.

 

Annotation은 위와같이 프로그래머에게 여러가지 정보를 줄 수 있고 컴파일 에러도 체크할 수 있지만 기능은 여기서 끝이 아닙니다. 위의 어노테이션의 경우에는 compile타임에만 작동하는 어노테이션이지만 바이트코드까지 살릴수도 있고 심지어 Runtime에서도 Annotation이 살아서 동작할 수 있습니다.

 

Annotation은 아까도 말했다시피 일종의 메타데이터인데 이것을 왜 사용할까요? 컴파일에러를 찾아내고 정보를 주는기능도 충분히 좋은기능이지만 제 생각에 이것을 사용하는 근본적인 이유는 바로 비지니스 로직과 다른 부가적인 로직들의 분리에 있다고 생각합니다.(AOP)

 

부가적인 로직들은 런타임에서 사용되는 어노테이션을 이용해서 코드를 작성하고 비지니스 로직에서는 해당 로직만 작성할 수 있도록 하는게 가장 큰 장점이 아닐까 싶습니다.

 

자바 Spring Boot 프레임워크에서는 ApplicationContext라는 것이 존재합니다. ApplicationContext는 Bean으로 설정된 오브젝트의 생성과 유지, 관리, 소멸을 관리하고, 심지어 오브젝트의 의존성까지 관리하게 됩니다.

 

즉 ApplicationContext는 프로그래머가 직접 객체를 생성하지 않고도 객체를 주입해주는 클래스이며 내부에는 Reflection을 활발하게 사용해서 객체를 생성하고 주입하고 있을것 같습니다.

 

예제를 하나 보겠습니다

 

아래는 StringInjector라고 하는 어노테이션이며 해당 어노테이션을 통해 클래스에 String값을 주입하려고 합니다. 값을 할당하지 않았을시에는 default로 String Injector라는 문자열이 할당됩니다.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface StringInjector {
    String value() default "String Injector";
}

Reflection을 이용해서 객체를 생성할때 어노테이션을 이용해서 값을 할당할 수 있습니다. Reflection으로 객체를 생성하기 위한 ContextContainer라는 클래스를 정의합니다.

public class ContextContainer {

    public <T> T get(Class<? extends T> clazz) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
        T instance = clazz.getDeclaredConstructor().newInstance();
        return invokeAnnotations(instance);
    }

    private <T> T invokeAnnotations(T instance) throws IllegalAccessException {
        for(Field field : instance.getClass().getFields()){
            StringInjector stringInjector = field.getAnnotation(StringInjector.class);

            if(stringInjector != null){
                field.setAccessible(true);
                field.set(instance, stringInjector.value());
            }
        }
        return instance;
    }
}

이때 class는 반드시 기본생성자가 필요합니다. 기본생성자가 없을시 NoSuchMethodException이 발생하게 되고 이것은 스프링에서도 마찬가지입니다. 스프링에서 빈은 기본생성자로 객체를 생성한뒤 객체에 값을 할당하는 식으로 객체가 생성되는것으로 알고 있습니다.

위에서 get메서드가 바로 클래스의 인스턴스를 생성해주는 메서드입니다. get메서드는 생성하려고 하는 클래스의 인스턴스를 생성하는 메서드이며 아래의 invokeAnnotations는 instance의 필드에서 Annotation이 있는지 확인한뒤 특정 Annotation이 있다면 그 필드에 Annotation의 값을 할당하는 방식으로 작동합니다.

'공부했던 내용' 카테고리의 다른 글

Spring Cloud Gateway란?  (0) 2021.02.10
부동소수점  (0) 2021.02.08