java-squid/effective-java

[아이템 65] 리플렉션보다는 인터페이스를 사용하라

Closed this issue · 12 comments

[아이템 65] 리플렉션보다는 인터페이스를 사용하라

리플렉션의 자바 문서: Class Class <T>
https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html

리플렉션 성능에 관한 글입니다. 가볍게 볼만해요
https://yjacket.tistory.com/73

한번 언급했던 것 같은데, Spring에서는 사용되는 어노테이션들이 이 리플렉션을 기반으로 동작하는 걸로 알고 있습니다.
그에 대한 흐름을 아래 포스팅에서 조금 파악할 수 있을듯 합니다!
https://sas-study.tistory.com/271

p374.. 이 프로그램을 컴파일 하면... 비검사 형변환 경고가 뜬다. 하지만 Class<? extends Set> 으로 형변환은 심지어 명시한 클래스가 Set을 구현하지 않았더라도 성공할 것이다.

  1. 왜 성공하는 걸까요?
  2. 왜 인스턴스를 생성하려 할 때 오류가 발생할까요?

현재 백기선님 더 자바 강의를 보고있는데 https://sas-study.tistory.com/271 링크와 똑같습니다.
호눅스가 추천하셨던 백기선님 강의 더 자바, 코드를 조작하는 다양한 방법 좋으니 만약 여유가 있으시면 꼭 보는걸 추천합니다.

리플렉션 정리(백기선님 강의)

리플렉션 사용시 주의해야할 사항

  • 지나친 사용은 성능 이슈가 발생. (잘못 적용시)
  • 컴파일 타임에 확인되지 않고 런타임 시에만 발생하는 문제를 만들 수 있다. (잘못 적용시)
  • 접근 지시자를 무시할 수 있다. (의도적으로 한다면)

사용되는 곳

한번 언급했던 것 같은데, Spring에서는 사용되는 어노테이션들이 이 리플렉션을 기반으로 동작하는 걸로 알고 있습니다.
그에 대한 흐름을 아래 포스팅에서 조금 파악할 수 있을듯 합니다!
https://sas-study.tistory.com/271

와 진짜 좋은 글이네요. 한번 리플렉션을 이번주에 간단하게나마 예시로 공부해봐야겠다는 생각이드네요.

저번에 David 가 리플렉션을 이용해서 Annotation 이 동작한다고 들었던것 같은데, 이번 기회에 공부해봐도 좋겠네요.

리플렉션을 이용해서 Annotation 정보를 가져오거나 조작할 수 있다고 생각할 수 있군요

p374.. 이 프로그램을 컴파일 하면... 비검사 형변환 경고가 뜬다. 하지만 Class<? extends Set> 으로 형변환은 심지어 명시한 클래스가 Set을 구현하지 않았더라도 성공할 것이다.

  1. 왜 성공하는 걸까요?
  2. 왜 인스턴스를 생성하려 할 때 오류가 발생할까요?

질문 뒤늦게 확인해서 답변 올립니다.

Class<? extends Set<String>> cl = (Class<? extends Set<String>>) Class.forName("java.lang.String"); // 1번

Constructor<? extends Set<String>> cons = cl.getDeclaredConstructor(); // 2번

Set<String> s = cons.newInstance(); // 3번

위 코드의 런타임에서 왜 1번과 2번은 성공하고 3번은 실패하는 걸까요?

저는 인터페이스가 구현하기를 요구하는 메서드가 인스턴스 내에 존재하느냐의 유무로 달라진다고 이해했습니다.

가령 Class.forName("java.lang.String") 은 비록 Class<? extends String> 타입을 반환할 테지만,
명시적으로 형변환을 하고 있고, 그에 대해 Class 클래스가 가져야 하는 필드와 메서드를 모두 가지고 있으므로,
런타임에서의 실행에서도 에러를 일으키지 않습니다.
성공을 왜 하느냐에 대한 이해는 제너릭을 전부 지우니 더 직관적으로 이해가 가더군요.

Class cl = Class.forName("java.lang.String"); // 1번
Constructor cons = cl.getDeclaredConstructor(); // 2번

위 코드는 정상작동합니다.
제너릭이 없어서 허전한것 말고는, 코드 상으로도 이해해보려 했을 떄 전혀 문제될게 없어보입니다.
기존의 1번과 2번 코드는 제너릭을 명시하고, 명시적인 형변환 코드를 추가했을 뿐입니다.
즉 컴파일 타임에서의 명시를 강화했을 뿐이지, 로직상으로 달라진게 하나도 없습니다.
그리고 왜 정상작동하는지에 대해서는 너무나 직관적이고 명확합니다.
가능한 타입에, 가능한 인스턴스를 할당했을 뿐입니다.

다만, 3번의 실패는 이야기가 다릅니다.
1번과 2번은 제너릭이 다를 뿐 인터페이스는 동일합니다.(Class<? extends Set> 과 Class<? extends String> 둘 다 어쩄든 Class)
3번은 제너릭이 아니라 인터페이스가 아예 다릅니다. (String 과 Set 은 진짜 아예 다르다)

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.util.Set (java.lang.String and java.util.Set are in module java.base of loader 'bootstrap')

즉 성공과 실패여부의 차이는 아래와 같다고 이해했습니다.

  • 제너릭만 다르다 -> 성공
  • 클래스 혹은 인터페이스가 다르다 -> 실패

추가: 컴파일 타임에서의 타입 캐스팅

Class cl = Class.forName("java.util.HashSet"); // 1번

Constructor<? extends Set> cons = cl.getDeclaredConstructor(); // 2번

Set s = cons.newInstance(); // 3번

위 코드는 정상 작동합니다.
Constructor<? extends Set> 에서 <? extends Set> 제너릭을 제거하면,
cons.newInstance() 를 했을 떄 컴파일 타임 중 캐스팅이 실패하므로 명시를 해주어야 합니다.

// 실패
Class cl = Class.forName("java.util.HashSet");
Constructor cons = cl.getDeclaredConstructor();
Set s = cons.newInstance();

// 성공
Class cl = Class.forName("java.util.HashSet");
Constructor cons = cl.getDeclaredConstructor();
Set s = (Set) cons.newInstance();

즉 컴파일 타임 중의 타입 캐스팅은 명시만 한다면, generic 이 없거나 이상하더라도,
타입 캐스팅으로 인한 에러는 발생하지 않는 것을 알 수 있습니다.

참고

https://docs.oracle.com/javase/9/docs/api/java/lang/Class.html
newInstance() 는 deprecated 되었으니 생성자를 통해서 만드는것을 추천합니다.

Class 에도 newInstance 있다는거 처음 알았는데 감사합니다!