[CS 지식] Reflection

Kim Hyen Su·2024년 12월 2일

면접질문

목록 보기
26/27
post-thumbnail

참고 블로그

사전 지식

바인딩

binding이란 프로그램에 사용된 구성 요소의 실제 값 또는 프로퍼티를 결정짓는 행위를 의미한다. 예를 들면, 함수 호출하는 곳에서 실제 함수가 위치한 메모리를 연결하는 것도 binding이라고 할 수 있다. 즉, 프로그램에서 사용되는 변수, 메서드, 생성자 등 모든 것들이 결정되도록 연결해주는 것을 의미한다.

binding은 결정 시점에 따라 정적 바인딩과 동적 바인딩으로 구분된다.

  • 정적 바인딩 : 컴파일 시점에 결정되며, 프로그램 실행 중에는 변하지 않는다. 컴파일 시점에 결정되기 때문에 잘못 작성 시 컴파일 오류가 발생하게 된다. 대표적인 예로 오버로딩(overloading)이 있으며, 컴파일 시점에 어떤 메서드를 호출할지 결정한다.
  • 동적 바인딩 : 실행(runtime) 시점에 결정되며, 프로그램 실행 중에 변할 수 있다. 결정은 되지 않았지만, 이를 위해 일정 메모리를 차지하기 때문에 메모리 공간이 낭비될 수 있다. 하지만, 유연하게 동작이 가능하다는 장점이 있다. 대표적인 예로는 오버라이딩(overrideing)이 있으며, 동적으로 호출될 메서드를 결정해준다.

Reflection

JVM은 클래스 정보를 클래스 로더를 통해서 읽어와 해당 정보를 JVM 메모리에 저장한다. 이처럼 저장된 클래스의 정보가 클래스를 마치 거울에 투영한 모습과 닮아 있어 리플렉션이라는 이름을 가진다고 한다. 리플렉션을 사용하면, 필드, 생성자, 메소드 등 클래스에 대한 메타 데이터를 읽어오거나 수정할 수 있다.

대표적인 예시가 자바 관련 프레임워크인 Spring에서 어노테이션은 리플렉션을 활용한 예시이다. 즉, Reflection을 통해 실행 시점에 클래스에 어떤 메소드가 붙어 있는지 확인하고 이를 통해 내부적인 동작을 할 수 있게된다.

또한, IDE에서 Getter, Setter, 생성자 등 자동 생성해주는 기능도 자바의 리플렉션을 활용하여 필드 정보를 기반으로 작성된다고 한다.

이러한 Reflection은 접근제어자와 무관하게 클래스의 메타 데이터에 접근이 가능하다.

Reflection 사용법

Class 클래스

reflection의 가장 핵심이 되는 클래스이다. Class 클래스는 java.lang 패키지에서 제공한다. Class 인스턴스를 얻는 방법은 다음과 같이 있다.

// 1. 클래스 내 class 필드
Class<Member> aClass = Member.class;

// 2. Object 클래스 getClass 메서드
Member member = new Member();
Class<? extends Member> bClass = member.getClass();

// 3. Class 클래스 forName 메서드 - ClassNotFoundException 예외 처리
Class<?> cClass = Class.forName("com.example.javawebstudy.Member");

그렇다면, Class 라는 클래스는 우리가 생성해주지도 않았는데 어떻게 존재하는걸까...? 이는 JVM에 의해서 자동으로 생성되며, static 클래스로 생성된다. 따라서, 클래스명으로 직접 메서드를 호출하는 방식으로 사용된다.

getXXX() vs getDeclaredXXX()

Class 객체의 메소드 중 getFields(), getMethods(), getAnnotations() 와 같은 형태와 getDeclaredFields, getDeclaredMethods(), getDeclaredAnnotations() 와 같은 형태로 정의된 메소드가 있다. 이 메소드들은 클래스에 정의된 필드, 메소드, 어노테이션 목록을 가져오기 위해서 사용된다.

두 형태의 차이점은 무엇이 있을까? getXXX() 형태의 메소드는 상속받은 클래스와 인터페이스를 모두 포함한 public 요소들을 가져온다. 반면에, getDeclaredXXX() 메서드는 상속받은 클래스와 인터페이스를 제외하고 해당 클래스에 직접 정의된 내용만 가져온다. 또한, 접근 제어자와 상관없이 모든 요소를 읽어올 수 있다.

Constructor

Class 클래스를 사용하여 생성자를 Constructor 타입으로 가져올 수 있다. Constructor는 java.lang.reflect 패키지에서 제공하는 클래스이며, 클래스 생성자에 대한 정보와 접근을 제공한다. 리플렉션으로 생성자에 직접 접근하고, 객체를 생성하는 예제를 작성해보겠다.

// 1. Class 클래스 getDeclaredConstructor() 메서드 - NoSuchMethodException 예외 처리
Constructor<?> constructor = aClass.getDeclaredConstructor();

// 2. Constructor 클래스 newInstance() 메서드 - InstantiationException 예외 처리
Object o = constructor.newInstance();

// 형변환
Member member2 = (Member) o;
		
System.out.println(member2);

/*
  결과 출력 :
  com.example.javawebstudy.Member@646d64ab
 */

위처럼 Constructor 클래스를 통해서 새로운 인스턴스를 생성할 수 있다.
만약, 파라미터가 존재할 경우 다음과 같이 매개변수가 추가된 형태로 생성자를 불러올 수 있다.

Constructor<?> onlyNameConstructor = aClass.getDeclaredConstructor(String.class);
Constructor<?> allArgsConstructor = aClass.getDeclaredConstructor(String.class, int.class);

Member member = (Member) allArgsConstructor.newInstance("후디", 25);

만약, private으로 정의된 기본 생성자에 접근할 경우에는 다음과 같이 추가 설정을 해줘야 한다.

생성자 객체.setAccessible(true);
Member member = (Member) constructor.newInstance();

Field

reflection을 사용하면 Field 타입의 객체를 획득하여 객체 필드에 직접 접근이 가능하다.

@Test
public void test(){
  Class<Member> aClass = Member.class;
  Member member = new Member("그디", 29);

  for (Field field : aClass.getDeclaredFields()) {
      field.setAccessible(true);
      String fieldInfo = field.getType() + ", " + field.getName() + " = " + field.get(member);
      System.out.println(fieldInfo);
  }

  /*
      class java.lang.String, name = 그디
      int, age = 29
  */
}

일반적으로 테스트 코드 내에서 Mock 객체 세팅을 위해서 사용한다. 다음과 같이 set() 메소드를 통해서 Setter가 없이도 객체의 필드 값을 변경할 수 있다.

Class<Member> aClass = Member.class;
Member member = new Member("그디", 29);

Field name = aClass.getDeclaredField("name");
name.setAccessible(true);
name.set(member, "메디"); // 필드값 변경

System.out.println("member = " + member);
// member = Member{name='메디', age=29}

Method

reflection을 사용하여 Method 타입의 인스턴스를 획득하여 객체 메소드에 직접 접근할 수 있다. 다음과 같이 사용할 수 있다.

Class<Member> memberClass2 = Member.class;
Member member3 = new Member("그디",29);

Method printString = memberClass2.getDeclaredMethod("printString");
System.out.println(printString.invoke(member3)); // 안녕, 자바 내이름은 그디

위처럼 Method 클래스 타입의 invoke() 메서드를 호출하여 인스턴스 내부에 정의된 메소드를 호출할 수 있다.

Annotation

Class<Member> aClass = Member.class;

Entitiy entityAnnotation = aClass.getAnnotation(Entitiy.class);
String value = entityAnnotation.value();
System.out.println("value = " + value);
// 멤버

위처럼 getAnnotation() 메소드에 직접 어노테이션 타입을 명시해주면, 클래스에 정의된 어노테이션을 가져올 수 있다. 어노테이션이 가지고 있는 필드에도 접근할 수 있는것을 확인할 수 있다.

Reflection 장단점

장점

reflection의 큰 장점은 프로그램의 유연성을 높여준다는 점이다. 런타임에 클래스의 정보를 얻거나, 객체를 조작할 수 있기 때문에, 동적으로 코드를 변경하거나 확장하는 것이 가능하게 된다. 특히, 프레임워크나 라이브러리를 활용한 개발 시에 매우 유용하게 사용할 수 있다.

단점

하지만, 그 만큼 큰 단점들이 존재한다. 일반적으로 메소드를 호출하면, 컴파일 시점에 분석된 클래스를 사용하지만 reflection을 활용하는 경우, 런타임에 클래스를 분석하여 메타 데이터를 가져오므로성능적으로 느려질 수 밖에 없다. 또한, 해당 과정은 JVM 최적화를 거치지 못하기 때문이기도 하다. 또한, 컴파일 시점에 타입 체크가 불가능하다. 또한, 접근 제한자와 상관없이 접근이 가능하기 때문에 캡슐화도 깨지게 된다.

따라서, 실제 개발 중에 reflection을 사용할 일이 거의 없다. 일반적으로 테스트 mock(가짜) 객체를 세팅할 때, 사용되기는 한다.

profile
백엔드 서버 엔지니어

0개의 댓글