[아이템64 - 객체는 인터페이스를 사용해 참조하라]
인터페이스를 사용해 참조하자
적합한 인터페이스만 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하자. 프로그램이 유연해진다.
선언 타입과 구현 타입을 동시에 바꿀 수 있으니 변수를 구현 타입으로 선언해도 되지 않을까?
클라이언트에서 기존 타입에서만 제공하는 메소드를 사용했거나, 기존 타입을 사용해야하는 다른 메소드에 그 인스턴스를 넘긴다면
새로운 코드로 교체를 했을 경우, 컴파일 되지 않는다. 변수를 인터페이스 타입으로 선언하면 이럴 일이 전혀 없다.
객체의 실제 클래스를 사용해야할 상황은 오직 생성자를 생성할 때 뿐이다. 코드로 자세히 보면 아래와 같다.
// 좋지않은 예, 클래스를 타입으로 사용
LinkedHashSet<Integer> integerSet = new LinkedHashSet<>();
// 좋은 예, 인터페이스를 타입으로 사용
Set<Integer> integerSet = new LinkedHashSet<>();
// 이를 기반으로, 다른 구현체로 교체도 가능하다.
integerSet = new HashSet<>();
코드 제일 아래를 보면, 인터페이스로 선언했기 때문에 새 클래스의 생성자 혹은 다른 정적 팩토리를 호출해주기만 하면 된다.
integerSet을 사용하던 다른 코드들은, 구현체 클래스의 존재를 애초부터 몰랐으니 이 변화에 영향을 받지 않는다.
그렇다면 구현 타입을 왜 변경하는걸까? 원래보다 성능이 좋거나, 새로운 기능을 제공하기 때문이다.
HashMap을 참조하던 변수가 있을 때, EnumMap으로 바꾸면 속도가 빨라지고 순회 순서도 키의 순서와 같아진다. 단, 키가 열거 타입일 때만 사용 가능하다, 이와 상관없는 LinkedHashMap으로 바꾸면 성능은 비슷하게 유지하면서 순회 순서 예측이 가능하다.
인터페이스 참조 주의점
원래의 클래스가 인터페이스의 일반 규약 이외의 특별한 기능을 제공하며
주변 코드가 이 기능에 기대어 동작한다면 새로운 클래스도 반드시 같은 기능을 제공해야 한다.
예를들어 주변 코드들이 LinkedHashSet이 따르는 순서 정책을 가정하고 동작하는 상황에 HashSet으로 바꾸면 문제가 된다.
LinkedHashSet이 반복자의 순회 순서를 보장하는 것과 달리 HashSet은 반복자의 순회 순서를 보장하지 않기 때문이다.
참조할 적합한 인터페이스가 없는 경우
1. 값 클래스인 경우
값 클래스는 여러 가지로 구현될 수 있다고 생각하고 설계할 일이 거의 없어서 final인 경우가 많고 상응하는 인터페이스 별도 존재도 드물다. 이런 값 클래스는 매개변수, 변수, 필드, 반환 타입 등으로 사용해도 무방하다. ex) String, BigInteger ...
2. 클래스 기반으로 작성된 프레임워크가 제공하는 객체인 경우
이런 경우라도 가능한 특정 구현 클래스보다는 보통 추상 클래스인 기반 클래스를 참조하는게 좋다.
ex) OutputStream(abstract class)를 포함한 java.io 패키지의 여러 클래스
3. 인터페이스에 없는 특별한 메소드를 제공하는 클래스들인 경우
클래스 타입을 직접 사용하는 경우는 추가 메소드를 꼭 사용해야 하는 경우로 최소화 해야하고, 남발해서는 안된다.
ex) PriorityQueue 클래스는 Queue 인터페이스에 없는 comparator 메소드를 가짐
가능한 주어진 객체를 표현할 적절한 인터페이스가 있는지 찾고
없다면 클래스 계층구조 중 필요한 기능을 만족하는 가장 덜 구체적인 상위 클래스를 타입으로 사용하자.
[아이템65 - 리플렉션보다는 인터페이스를 사용하라]
리플렉션(java.lang.reflect): 구체적인 클래스 타입을 몰라도, Constructor, Method, Field 인스턴스에 접근하게 해주는 API
class 객체가 주어지면 그 클래스의 Constructor, Method, Field 인스턴스를 가져올 수 있고, 이 인스턴스들로 멤버 이름, 필드 타입, 메소드 시그니처도 가져올 수 있다. 그리고 각각에 연결된 실제 생성자, 메소드, 필드를 조작할 수 있고 해당 클래스의 인스턴스 생성, 메소드 호출, 필드 접근도 가능하다.
즉, 리플렉션을 이용하면 컴파일 당시에 존재하지 않던 클래스도 이용할 수 있다.
리플렉션의 단점
- 컴파일타임 타입 검사가 주는 이점을 누릴 수 없다.
- 존재 할지 모르는 멤버나 접근할 수 없는 멤버를 호출시 런타임 오류가 발생해서 예외검사의 이점도 누릴 수 없다.
- 코드가 지저분하고 읽기 어려워진다.
- 일반 메소드 호출보다 리플렉션을 통한 메소드 호출은 느려서 성능이 떨어진다.(접근 권한 확인, 메소드 카피 등)
코드 분석 도구, 의존관계 주입 프레임워크에서 리플렉션을 써야하는 애플리케이션이 있지만, 단점이 명확해 리플렉션 사용은 줄고있다.
리플렉션은 제한된 형태로만 사용해야 단점을 피하고 이점만 취할 수 있다.
컴파일 타임에 이용할 수 없는 클래스 사용은, 적절한 인터페이스나 상위 클래스를 이용할 수는 있다.
리플렉션은 인스턴스 생성에만 쓰고, 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자.
리플렉션의 집합 예시
리플렉션으로 생성/메소드/ 필드를 활용하는 코드는 아래를 참조하자!
Set 구현체를 공격하고 조작해보며 Set 규약을 잘 지키는지 검사하는 제네릭 집합 테스터, 제네릭 집합 성능 분석 도구가 될 수 있다.
이 예는 리플렉션의 단점 두 가지를 보여준다.
1. 인스턴스 생성시, 런타임에 여섯 가지 예외를 던진다. 리플렉션 없이 생성하면 컴파일 타임에 잡아낼 수 있었다.
2. 인스턴스 생성까지 1줄이면 끝났을 인스턴스 생성을 클래스 이름만으로 인스턴스 생성을 위해 25줄이 된다.
각각을 잡지않고 모든 리플렉션 예외의 상위 클래스인 ReflectiveOperationException(자바 7이상)을 잡아 코드를 줄일 수 있다.
두 가지 단점은 인스턴스 생성 부분에만 국한이고, 객체가 만들어진 후엔 Set 인스턴스 사용때와 똑같다.
그리고 클래스 정의시 강제 형변환을 하기 떄문에 비검사 형변환 경고가 뜬다. Set 구현체를 확장하지않은 어떤 클래스를 넣어도 성공은 하지만 예외는 발생하지 않고 인스턴스를 생성하려할 때 ClassCastException을 던진다.
정리
리플렉션은 런타임에 존재하지 않을 수 있는 다른 클래스, 메소드, 필드와 의존성 관리때 적합하다.
[ex) 가장 오래된 버전만 지원하게 컴파일 후 이후 버전은 리플렉션으로 접근하는 방식]
복잡한 특수 시스템 개발에 필요한 강력한 기능은 많지만 단점도 많기에 컴파일 타임에 알 수 없는 프로그램 작성시에만 쓰자.
그리고 인터페이스나 상위 클래스로 형변환해서 사용하자
* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.