[영상후기][10분 테코톡] 파랑, 아키의 리플렉션

박철현·2023년 4월 19일
0

영상후기

목록 보기
99/160

movie

리플렉션이란?

  • 스프링은 어떻게 실행시점에 빈을 주입할 수 있는 건가요?
  • JPA의 Entity는 왜 꼭 기본 생성자를 가져야만 할까요?
    리플렉션
  • 컴파일러가 자바 코드를 바이트 코드로 바꿔줌 -> 클래스 로더는 바이트코드를 읽어 JVM 내 메모리 영역 저장

  • 리플렉션 : JVM영역에 저장된 클래스의 정보를 꺼내와서 필요한 정보들(생성자, 필드, 메서드)을 가져와 사용하는 기술

    • C, C++, Pascal에서는 불가능
  • Class : 실행중인 자바 어플리케이션의 클래스와 인터페이스의 정보를 가진 클래스

    • public 생성자 존재하지 않고, JVM에 의해 자동으로 생성됨
    • 제공 기능
      • 클래스에 붙은 어노테이션 조회
      • 클래스 생성자 조회
      • 클래스 필드 조회
      • 클래스 메서드 조회
      • 부모 클래스, 인터페이스 조회 등
  • 자동으로 생성된 Class 객체 가져오는 방법

    • {클래스타입}.class
    • {인스턴스}.getClass()
    • Class.forName("{전체 도메인 네임}")
  • getMethods vs getDeclaredMethods 주의

    • getMethods : 상위 클래스와 상위 인터페이스에서 상속한 메서드를 포함하여 public인 메서드들을 모두 가져온다.
    • getDeclaredMethods : 접근 제어자와 관계 없이 상속한 메서드를 제외하고 직접 클래스에서 선언한 메서드들을 모두 가져온다.
      (getXXX와 getDeclaredXXX를 잘 구분해서 사용!)

리플렉션의 기능

public class Dog {
	private static final String CATEGORY = "동물" ;
    
    private String name;
    public in age;
    
    private Dog() {
    	this.name = "누렁이";
        this.age = 0;
    }
    
    public Dog(final String name) {
    	this.name = name;
        this.age = 0;
    }
    
    public Dog(final String name, final int age) {
    	this.name = name;
        this.age = age;
    }
}

생성자를 통한 객체 생성

  • 생성자의 파라미터로 구분하여 클래스에 선언된 생성자를 가져올 수 있다.
// Class 객체로 가져옴
Class<?> clazz = Class.forName("org.example.Dog");

// 생성자를 Constructor로 가져옴
Constructor<?> constructor1 = class.getDeclaredConstructor();
Constructor<?> constructor2 = class.getDeclaredConstructor(String.class);
Constructor<?> constructor3 = class.getDeclaredConstructor(String.class, int.class);
  • 가져온 생성자로 무엇을? 객체를 생성할 수 있다! 생성자니깐!
    • 아래 코드 에러 발생 : 기본 생성자는 위 코드에서 private로 정의하였기 때문
Object dog1 = constructor1.newInstance();
  • 접근 제어자가 public이 아닌 경우에 setAceessible 메서드를 이용하면 접근할 수 있다.
    constructor1.setAccessible(true);
    Object dog1 = constructor1.newInstance();
    Object dog2 = constructor2.newInstance("호두");
    Object dog3 = constructor3.newInstance("호두", 5);

필드 정보 조회

  • 필드의 접근제어자, 타입, 네임, 값 등의 정보를 조회할 수 있다.
Object dog = constructor.newInstance("호두", 5);

// 클래스에 정의된 필드를 Field 객체로 받아올 수 있음
Field[] fields = clazz.getDeclaredFields();

for ( Field field : fields) {
	// 필드도 private 접근제어자가 존재할 수도 있기에 설정
	field.setAccessible(true);
    System.out.println(field);
    // 원하는 인스턴스를 넣으면 인스턴스가 가진 field의 값을 가져올 수 있음
    System.out.println("value : " + field.get(dog));
    System.out.println("-------------------------");
}
  • 필드의 접근제어자 / 타입 / 값 출력

필드 값 변경

  • private 필드의 값도 변경할 수 있다.
  • 이런 방식을 통해 필드에 값을 넣거나 변경해주곤 했음
// name 필드를 가져옴
Field field = clazz.getDeclaredField("name");
// private 필드이기에 설정
field.setAccessible(true);
System.out.println("기존: " + field.get(dog));
field.set(dog, "땅콩");
System.out.println("변경: " + field.get(dog));

메서드

public class Dog {
	...
    private void speak(final String sound, final int count) {
    	System.out.println(sound.repeat(count));
    }
    
    public void eats() {
    	System.out.println("사료를 먹습니다. ");
    }
    
    public int getAge() {
    	return age;
    }
}

메서드 정보 조회

  • 메서드의 접근제어자, 리턴 타입, 네임, 파라미터 타입 등의 정보를 가져올 수 있다.
// Method 타입의 객체로 받아오기
Method[] methods = class.getDeclaredMethods();
for (Method method : methods) {
	method.setAccessible(true);
    System.out.print(method);
    System.out.println("\n-------------------");
}
  • private 메서드 호출할 수 있다.
Method method = class.getDeclaredMethod("speak", String.class, int.class);
method.setAccessible(true);
// invoke 메서드로 호출
method.invoke(dog, "멍멍!", 5);
}

리플렉션이 사용되는 곳

리플렉션을 왜 사용할까?

  • 주로 프레임워크나 라이브러리에서 사용함

    • 사람이 코딩할 때 객체의 타입을 모를일이 거의 없음
    • 반면 프레임워크나 라이브러리에서 사용자가 생성한 객체가 어떤 타입인지 컴파일 시점까지 알 수 없음
    • 이러한 문제를 동적으로 해결하고자 리플렉션 사용
  • JPA, Jackson, Mockito, JUNIT, intelliJ의 자동완성 기능 등에서 사용됨

  • 프레임워크나 라이브러리에서 객체의 기본생성자가를 요구하는 경우가 많은데 왜 필요할까?

    • 리플렉션 떄문
@Entity
public class Member {
	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;
    private String name;
    
    protected Member() {
    }
    
    // 생략 ..
}

왜 기본 생성자가 필요한가?

가장 간단한 방법이기 떄문

  • 기본 생성자로 객체를 생성하고 필드를 통해 값을 넣어주는 것이 가장 간단한 방법이기 때문

  • 기본 생성자가 없다면 어떤 생성자를 사용할 지 고르기 어렵다.

    public Member(final String name) {
    	this.name = name;
    }
    
    public Member(final String name, final String email) {
    this.name = name;
    this.email = email;
    }
    
    // 생략 ..

생성자에 로직이 있는 경우 원하는 값을 바로 넣어줄 수 없다.

  • 강아지의 나이를 10으로 넣어주고 싶었는데 생성자의 +1 로직이 있다면? 원하는 값을 바로 넣어주기 힘들 것
public Dog(int age) {
	this.age = age + 1;
}

파라미터들의 타입이 같은 경우 필드와 이름이 다르면 값을 알맞게 넣어주기 힘들다.

public Member(final String displayName, final String myEmail, final String iamgeUrl) {
  this.name = displayName;
  this.email = myEmail;
  this.url = imageUrl;
}

기본생성자를 사용한다면 이 모든 경우의 수들을 고려하지 않아도 됨

@Entity
public class Member {
	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;
    private String name;
    
    public Member() {
    }
    
    // 생략 ..
}
  • 기본 생성자로 객체를 생성한 뒤
  • 필드 이름에 맞춰 알맞은 값을 넣어주면 끝!!
  • 이처럼 기본 생성자를 사용하는 것이 사용하지 않는 것보다 훨씬 간편하기 때문에 많은 라이브러리 혹은 프레임워크에서 기본 생성자를 요구하고 있음

어노테이션

  • 어노테이션은 단지 주석인데 어떻게 동작? Reflection
    • 리플렉션을 통해 어노테이션이 붙어있을 수 있는 클래스나 메서드, 파라미터 정보를 가져온다.
    • 리플렉션의 getAnnotation(s), getDeclaredAnnotation(s)등의 메서드를 통해 원하는 어노테이션이 붙어 있는지 확인한다.
    • 어노테이션이 붙어 있다면 원하는 로직을 수행한다.

간단한 DI 프레임워크 만들기

public class OrderService {
	@Autowired
    OrderRepository orderRepository;
}

public class OrderRepository {
	Map<Integer, Order> orderIdToOrderMap = new HashMap<>();
    
    public Order getById(Integer orderId) {
    	return orderIdToOrderMap.get(orderId);
    }
}

@Target(ElementType.FIELD)
// 런타임 시 호출하는 어노테이션
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
// 의존관계 주입 핵심 담당
public class ApplicationContext {
	public static <T> T getInstance(Class<T> clazz) throws Exception {
    T instance = createInstance(clazz); // 인스턴스 생성
    Field[] fields = clazz.getDeclaredFields();
    for( Field field : fields) {
    // @Autowired 어노테이션 붙어있다면
    	if (field.getAnnotation(Autowired.class) != null) {
        // 필드 타입에 맞는 인스턴스를 생성
        	Object fieldInstance = createInstance(field.getType());
            field.setAccssible(true);
            // 필드 인스턴스를 해당 필드에 주입
            field.set(instance, fieldInstance);
          }
       }
        return instance;
    }
    
  private static <T> T createInstance(Class<T> clazz) throws Exception {
  	return clazz.getConstructor(null).newInstance();
  }
}
public class Application {
	public static void main(String[] args) {
    	ApplicationContext applicationContext = new ApplicationContext();
        OrderService orderService = applicationContext.getInstance(OrderService.class);

리플렉션 단점

일반 메서드 호출보다 성능이 훨씬 떨어진다

  • 컴파일 시점이 아닌 런타임 시점에서 클래스 분석
    • JVM을 최적화 할 수 없기에 성능저하 발생
    • 메서드 호출 10만번 반복
      • 일반 메서드 호출 : 7ms
      • 리플렉션 사용했을 경우 : 170ms
      • 약 24배 차이

컴파일 시점에 타입 체크 불가

  • 리플렉션은 런타임 시점에 클래스 정보를 알게 된다.
    • 컴파일 시점에서 타입 체크 기능을 사용할 수 없음
    • 존재하지 않는 class를 적으면 예외 발생
    • 예외처리 불가
public static void main(String[] args) {
	try {
    	Class<?> clazz = Class.forName("org.example.Dog"); // ClassNotFoundException 발생
        }
    catch (ClassNotFoundException e) {
    	e.printStackTrace();
     }
 }

코드가 지저분해지고 장황해짐

// 일반 메서드 호출
public static void main(String[] args) {
	final Dog dog = new Dog("누렁이");
    dog.speak();
}

// 리플렉션을 사용했을 경우
public static void main(String[] args) {
	try {
      Class<?> clazz = Class.forName("org.example.Dog");
      Object object = clazz.getConstructor(String.class).newInstance("누렁이");
      Method method = clazz.getDeclaredMethod("speak", null);
      method.setAccessible(true);
      method.invoke(object, null);
  	} catch (ClassNotFoundException
          | NoSuchMethodException
          | InvocationTargetException
          | InstantiationException
          | IllegalAccessException e) {
            e.printStackTrace();
          }
}

내부를 노출해서 추상화를 파괴함

  • 모든 클래스 내부 정보를 알 수 있음
  • 접근할 수 없는 private에도 접근 -> 불변성 지킬수도 없음

결론

  • 클래스의 정보를 컴파일 시점에 알아야 하는 특수한 경우가 아니라면 리플렉션 사용을 지양
profile
비슷한 어려움을 겪는 누군가에게 도움이 되길

0개의 댓글

관련 채용 정보