본문 바로가기

CS/JAVA

JAVA의 리플렉션 API

스프링을 공부한다면 의존성 주입(Dependency Injection)에 대해 들어봤을 것이다. @Autowired 애노테이션을 사용해서 생성자 없이 인스턴스를 생성하고, @Controller, @Service , @Repository 등의 애노테이션을 통해 해당 클래스의 인스턴스를 새로 생성하지 않아도 자동으로 스프링 컨테이너에 빈을 생성하여 관리한다.  여기서 사용되는 기술이 Reflection API이다. 스프링(스프링에만 국한된 기술은 아님)은 개발자가 만든 구체적인 클래스에 대한 정보를 알 수 없을텐데 어떻게 의존 주입이 가능한 것일까?  

 

백기선님의 "더 자바, 코드를 조작하는 다양한 방법", 메타코딩의 리플렉션 강의, 우아한 Tech의 [10분 테코톡] 파랑, 아키의 리플렉션 등을 참조 하였습니다.


1. 리플렉션이란

Java는 정적언어로써 컴파일 시점에 타입(자료형)을 결정하게 된다. 따라서 프레임워크나 라이브러리는 개발자가 만든 클래스의 정보를 컴파일 시점까지 알 수 없다. 이는 런타임 시점에서 동적으로 타입을 처리해야 하는 경우에 대해 어려움이 있을 수 있는데, 리플렉션을 통해 해결 가능하다.

리플렉션 API는 주로 프레임워크나 라이브러리에서 사용된다. 리플렉션은 객체를 통해 객체가 속한 클래스에 대한 정보(생성자, 메서드, 필드), 그리고 상위 클래스와 인터페이스에 대한 정보도 알 수 있다. 뿐만 아니라 접근제어자가 private 이든 public 이든 상관 없이 필드나 메서드에 접근하여 조작할 수 있다. 이처럼 Reflection은 외부에서 접근할 수 없는 필드나 메서드에 대해  노출하기 때문에 제한된 형태로 사용해야한다. 

 

정적 언어 VS 동적 언어

→ 정적 언어 : Java, C, C++ 등이  정적 언어에 해당하며 타입을 컴파일 시점에 결정

    - 컴파일 시점에 타입을  결정하기 때문에 동적 언어보다 속도가 빠르다.

    - 타입 에러로 인한 문제점을 빠르게 발견할 수 있다.

→ 동적 언어 : Python, JS 등이 동적 언어에 해당하며 런타임 시점에 타입을 결정 

    - 런타임 시점에 타입을 결정할 수 있기 때문에 선택의 폭 증가

    - 실행 도중 잘못된 타입이 들어옴으로써 문제가 발생할 수 있다.

    - 런타임 시점에 발생하는 오류는 찾기 어려울 수 있음

2. Why? 왜 리플렉션 API를 사용할까?

앞서 리플렉션에 대해 소개하면서 리플렉션을 동적인 문제를 해결하기 위해 사용한다고 했다. "내가 만든 클래스에 대해 잘 아는데 사용할 일이 있을까?" 라고 생각한다면 이해하기 힘들 것이다. 리플렉션은 프로그램 개발에서는 많이 쓰이지 않는다. 따라서 라이브러리를 만든다고 생각해보자. 라이브러리를 이용하는 사용자가 어떠한 클래스를 만들지 구체적으로 알 수 있을까? 라이브러리를 개발하는 입장에서 우리는 구체적인 클래스를 알 수 없다. 따라서 이러한 문제점을 동적으로 해결하기 위해 리플렉션 기술이 존재한다. 

① 스프링 DI

@Service
public class BookService {

	@Autowired
	BookRepository bookRepository;
    
}

스프링을 사용해봤다면 위와 같은 코드를 많이 봤을 것이다. 여기서 @Autowired는 의존 객체의 타입에 해당하는 빈을 주입을 하기 위한 역할을 한다. 생성자를 통해 객체를 주입해주어야 할 것 같지만 런타임 시점에 bookRepository의 인스턴스는 이미 만들어져 있다. 스프링의 BeanFactory에서는 @Autowired가 적힌 필드를 찾아 객체를 주입하는 과정을 진행한다. 아래 AutowiredAnnotationBeanPostProcessor 클래스의 processInjection 메서드에서 객체를 주입한다.

package org.springframework.beans.factory.annotation;

public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor,
		MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
        
        
    public void processInjection(Object bean) throws BeanCreationException {
            Class<?> clazz = bean.getClass();
            InjectionMetadata metadata = findAutowiringMetadata(clazz.getName(), clazz, null);
            try {
                metadata.inject(bean, null, null);
            }
            catch (BeanCreationException ex) {
                throw ex;
            }
            catch (Throwable ex) {
                throw new BeanCreationException(
                        "Injection of autowired dependencies failed for class [" + clazz + "]", ex);
            }
        }
        
        
}

② Dispatcher 서블릿의 필터 [참고]

메타 코딩님의 리플렉션 강의를 보면 좀 더 쉽게 이해할 수 있다. 영상 링크는 위에 표시해두었다.

사용자가 접속하는 URL 주소를 바탕으로 Handler 매핑을 통해 적절한 컨트롤러를 찾아준다. 이러한 역할을 하는 것을 필터라고 하는데, 리플렉션을 통해 필터를 구현할 수 있다.(@RequestMapping 애노테이션으로 접근하는 것이 일반적인 방법이지만 이 또한 리플렉션을 사용) 다양한 기능을 서비스에 맞게 구현하고, 필터에 if-else를 통해 해당 메서드와 매핑 시켜놓았다.  서비스에 기능이 추가되면 해당 필터에서도 매핑을 시켜야 한다. 예를 들어 100개의 기능을 만들었으니 다 매핑하려고 하면 일일이 할 수 있을까? 기능이 추가될 때 마다 필터에도 추가해야한다면 이러한 과정은 정말 비효율적이다. 리플렉션을 통해 서비스의 메서드를 모두 가져오고, 메서드와 이름을 비교하며 해당 기능을 사용할 수 있도록 한다면 코드의 라인은 훨씬 줄어들고, 유지보수 또한 쉬워진다.

 

[스프링의 서블릿에 대해 궁금하다면 참고]

③ @Retention 애노테이션

@Service, @Controller, @Autowired 애노테이션을 관리하는 자바파일들을 보면 @Retention 이라는 애노테이션이 붙어있다. 해당 애노테이션은 애노테이션의 라이프사이클을 언제까지 적용시킬건지에 대해 결정할 수 있다. 리플렉션 API로 접근하여 클래스의 정보 조회가 가능하다.

  • RetentionPolicy.SOURCE : 애노테이션을 컴파일 시점까지 살릴 경우
  • RetentionPolicy.CLASS : 클래스 파일까지만 유지 - 타입체커, IDE 부가기능 사용하려면 필요
  • RetentionPolicy.RUNTIME : 런타임 시점에 애노테이션 정보를 얻을 수 있음. @Service, @Controller, @Autowired가 여기에 해당

④그 외, 리플렉션이 사용되는 곳

- Hibernate, JPA, Jackson, Mockito, JUnit 

 

 3. How? 리플렉션 API는 어떻게 클래스에 대한 정보를 알 수 있을까?

리플렉션은 JVM의 메모리 영역에 저장되어 있는 클래스의 정보를 가져올 수 있다. [JVM에 대해 구체적으로 알아보기]

 

좀 더 구체적으로 설명하자면,

1. JAVA Compiler에서 바이트 코드(클래스 파일)을 생성하고,

2. 변환된 클래스 파일들은 클래스 로더를 통해 JVM의 메모리 영역에 링크된다.

3. 공유 자원인 Method Area에 클래스 정보를 저장하여 언제든지 접근이 가능한 상태

4. 리플렉션은 Method Area에서 클래스에 대한 정보에 접근하여 조회 및 수정한다.

리플렉션으로 가져올 수 있는 정보

  • Class의 이름
  • 해당 Class의 패키지 정보
  • Class의 접근제어자(public, private, synchronized)
  • Class의 필드
  • Class의 생성자
  • Class의 부모 클래스, 인터페이스
  • Class의 메서드
  • Class에 적용된 애노테이션

4. 리플렉션 관련 메서드 살펴보기[오라클 문서 참조]

  1. Class 가져오기

//{클래스타입 이름}.class
Class<?> bookClass = Book.class
//{인스턴스 이름}.getClass()
Book book = new Book();
Class<?> bookClass = book.getClass();
//Class.forName("{클래스 주소}")
Class<?> bookClass = Class.forName("org.example.Book")

  2. Class  내 필드, 메서드 접근하기

 

  ※ getXXX와 getDeclaredXXX의 차이

    → getXXX로 시작하는 메서드들은 해당 클래스의 필드, 메서드 뿐만 아니라 상위 클래스, 인터페이스까지 접근(public만 접근)

    → getDeclaredXXX를 사용하면 해당 클래스의 정보에만 접근(public, private 모두 접근)

 

▷ Book 클래스

package org.example;

public class Book {

    public static String A = "A";

    private String B = "B";

    public Book() {
    }

    public Book(String b) {
        B = b;
    }
    private Book(String a, String B) {
        A = a;
        this.B = B;
    }

    private void c() {
        System.out.println("C");
    }

    public int sum(int left, int right) {
        return left + right;
    }


}

 

▷ Main 함수

package org.example;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

public class App {
    public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        // 클래스 가져오기
        Class<?> bookClass = Class.forName("org.example.Book");

        // 클래스의 생성자 가져오기
          //public
        Constructor constructor1 = bookClass.getConstructor();
        Constructor constructor2 = bookClass.getConstructor(String.class);
          //private
        Constructor constructor3 = bookClass.getDeclaredConstructor(String.class, String.class);
        constructor3.setAccessible(true);

        // 가져온 생성자를 통해 인스턴스 생성
        Book book1 = (Book) constructor1.newInstance();
        Book book2 = (Book) constructor2.newInstance("Book2");
        Book book3 = (Book) constructor3.newInstance("abcd", "def");

        // static 필드 가져오기
        Field a = Book.class.getDeclaredField("A");
        System.out.println(a.get(null));  // static은 인스턴스가 필요 없으므로 null
        a.set(null, "changed A");
        System.out.println(a.get(null));

        /** Console
         * abcd
         * changed A
         */
        
        // private 필드 가져오기
        Field b = Book.class.getDeclaredField("B");
        b.setAccessible(true);  //
        System.out.println(b.get(book1));

        /** Console
         * B
         */
        
        // method 사용하기
        Method c = Book.class.getDeclaredMethod("c");
        c.setAccessible(true);
        c.invoke(book2);

        Method sum = Book.class.getDeclaredMethod( "sum", int.class, int.class);
        System.out.println(sum.invoke(book1, 10, 20));

        /** Console
         * C
         * 30
         */
        
        Arrays.stream(Book.class.getDeclaredMethods()).forEach(System.out::println);

        /** Console
         * private void org.example.Book.c()
         * public int org.example.Book.sum(int,int)
         */
    }
}

5. 리플렉션의 장점과 단점

장점

  • 런타임 시점에 클래스의 인스턴스 생성이 가능
  • 접근 제어자에 관계없이 필드와 메서드, 생성자에 접근가능

단점

리플렉션이 강력한 기술임이 틀림없다. 그렇지만 해당 기술을 사용할 때에는 주의가 필요하다.

  • 런타임 시점에 인스턴스를 생성하기 때문에 컴파일 시점에서 클래스 타입을 확인할 수 없음 → 오버헤드 발생 가능성
  • 클래스 내부 노출로 인해 객체지향의 특징인 캡슐화, 정보의 은닉성 파괴 → 보안이 약해질 수 있음
  • 리플렉션 기술을 통한 필드, 메서드 접근은 일반적인 접근보다 느림

 

 


참고하면 좋은 사이트

 

 

[Spring] 의존성 주입(Dependency Injection, DI)이란? 및 Spring이 의존성 주입을 지원하는 이유

1. 의존성 주입(Dependency Injection)의 개념과 필요성 [ 의존성 주입(Dependency Injection) 이란? ] Spring 프레임워크는 3가지 핵심 프로그래밍 모델을 지원하고 있는데, 그 중 하나가 의존성 주입(Dependency Inj

mangkyu.tistory.com

 

@Autowired의 동작원리

@Autowired란? 의존관계 주입(DI)을 할 때 사용하는 어노테이션(Annotation)이며, 의존 객체의 타입에 해당하는 빈(Bean)을 찾아 주입하는 역할을 한다. 💡 의존관계 주입에 대해서는 IoC(Inversion of Control,

beststar-1.tistory.com

 

Reflection in Java - GeeksforGeeks

Reflection in Java

www.geeksforgeeks.org

 

자바 Reflection이란?

많은 입문용 자바 서적에서 잘 다루지 않는 Reflection이라는 개념에 대해서 알아보려고 합니다.

medium.com

 

[Java/자바] - 리플렉션(Reflection)

리플렉션 Reflection 리플렉션Reflection이란 개념은 저에게는 낯선 용어였습니다. 자바 기본서 2권이 있...

blog.naver.com

 

'CS > JAVA' 카테고리의 다른 글

[JAVA] JDK21의 Virtual Thread  (2) 2024.01.08
[JAVA] Record  (0) 2023.12.16
[JAVA] String 그리고 StringBuffer와 StringBuilder  (0) 2022.08.30
Abstract Class와 Interface  (0) 2022.08.23
Java에서의 Hash  (0) 2022.08.16