[4주차] 21-28
21 인터페이스는 구현하는 쪽을 생각해 설계하라
이번 아이템은 1.인터페이스의 디폴트 메서드, 2.디폴트 메서드가 내재하고 있는 위험성 그리고 3.디폴트 메서드를 작성할 때 주의할 사항에 대한 내용을 서술합니다.
해당 장을 한 문장으로 표현하면 다음과 같습니다. '디폴트 메서드라는 도구가 생겼더라도 인터페이스를 설계할 때는 여전히 세심한 주의를 기울여야 한다.'
디폴트 메서드
자바 8 이전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 방법이 없었다.
추가된 메서드가 우연히 기존 구현체에 존재할 가능성은 아주 낮기 때문에
인터페이스에 메서드를 추가하면 보통은 컴파일 오류가 발생했다.
자바 8에 와서 기존 인터페이스에 메서드를 추가할 수 있도록 디폴트 메서드가 소개되었다.
디폴트 메서드를 선언하면, 그 인터페이스를 구현한 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다.
interface EffectvieDefaultMethod {
default void testMethod(){
System.out.println("이것은 디폴트드 메서드");
}
}궁금증 - 그렇다면 자바 8의 인터페이스와 추상클래스의 차이점이 있을까?
인터페이스에서 모든 필드는 기본적으로 public static final로 선언되며, 모든 메소드는 public abstract과 public static이다. 반면, 추상클래스에서는 public, static, final이 아닌 필드를 지정할 수 있고, public, protected, private 메소드를 가질 수 있다.
인터페이스는 다중 구현이 가능하지만, 자바에서는 다중상속을 지원하지 않음으로 추상클래스는 단 1개만 상속할 수 있다.
출처:https://yaboong.github.io/java/2018/09/25/interface-vs-abstract-in-java8/
디폴트 인터페이스가 내재하고 있는 위험성
기존 인터페이스에 메서드를 추가할 수 있지만, 모든 기존 구현체들과 매끄럽게 연동되리라는 보장은 없다.
생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어렵다.
대표적으로 멀티스레드 환경에서 Collection을 동기화하는 org.apache.commons.collections4.collection.synchronizedcollection 의 사례가 있습니다.
위 클래스는 Collection 인터페이스에 추가된 디폴트 메소드로 인해 인스턴스를 여러 스레드가 공유하는 환경에서 한 스레드가 removeIf를 호출하면 ConcurrentModificationException이 발생하거나 다른 예기치 못한 결과로 이어지게 되었습니다.
org.apache.commons.collections4.collection.synchronizedcollection 소개 페이지 https://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/collection/SynchronizedCollection.html
디폴트 메서드를 작성할 때 주의사항
기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 피해라.
추가하려는 디폴트 메서드가 기존 구현체들과 충돌하지 않을지 심사숙고하라.
디폴트 메서드는 인터페이스로부터 메서드를 제거하거나 기존 메서드의 시그니처를 수정하는 용도가 아니며, 이런 형태로 인터페이스를 변경하면 반드시 기존 클라이언트를 망가뜨린다.
디폴트 메서드가 유용할 때
새로운 인터페이스를 만드는 경우라면 디폴트 메서드는 표준적인 메서드 구현을 제공하는 데 아주 유용한 수단이며, 그 인터페이스를 더 쉽게 구현해 활용할 수 있게끔 해준다.
22 인터페이스는 타입을 정의하는 용도로만 사용하라
이번 아이템은 1. 인터페이스의 용도, 2. 인터페이스 안티 패턴인 상수 인터페이스, 3. 상수를 올바르게 공개하는 방법에 대해 설명합니다.
인터페이스의 용도
인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다. 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에 얘기해주는 것이다. 인터페이스는 오직 이 용도로만 사용해야 한다.
상수 인터페이스 (안티패턴)
상수 인터페이스는 안티패턴이므로 사용하면 안된다.
상수 인터페이스란 메서드 없이, 상수를 뜻하는 static final 필드로만 가득 찬 인터페이스를 말한다.
클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현에 해당한다. 따라서 상수 인터페이스를 구현하는 것은 이 내부 구현을 클래스의 API로 노출하는 행위다.
상수 인터페이스는 사용자에게 혼란을 주며, 클라이언트 코드나 이 상수들에 종속되게 한다. 다음 릴리즈에서 이 상수들을 더는 쓰지 않게 되더라도 바이너리 호환성을 위해 여전히 상수 인터페이스를 구현하고 있어야 한다.
final이 아닌 클래스가 상수 인터페이스를 구현하면 모든 하위 클래스의 이름공간이 그 인터페이스가 정의한 상수들로 오염되어 버린다.
상수의 올바른 공개
상수를 공개할 목적이라면 다음의 더 합당한 선택지를 고려하자.
특정 클래스나 인터페이스와 강하게 연관된 상수라면 그 클래스나 인터페이스 자체에 추가한다. ex) Integer, Double에 선언된 MIN_VALUE, MAX_VALUE 상수
열겨 타입으로 나타내기 적합한 상수라면 열거 타입(enum)으로 만들어 공개한다.
인스턴스화할 수 없는 유틸리티 클래스에 담아 공개한다.
22
23 SubTyping
두 가지 이상의 의미를 표현할 수 있으며, 그중 현재 표현하는 의미를 태그 값으로 알려주는 클래스를 본 적이 있을 것이다.
예시) 태그 달린 클래스
이러한 클래스는 단점이 한가득이다. 우선 열거 타입 선언, 태그 필드, switch문 등 쓸데 없는 코드가 많다. 여러 구현이 한 클래스에 혼합돼 있어서 가독성도 나쁘다. 메모리 역시 언제나 함께 하니 많이 사용할 수 밖에 없다. 필드를 final로 선언하면 쓰지 않는 필드들까지 초기화 해어야한다. 태그 달린 클래스는 장황하고 오류를 내기 쉽고, 비효율적이다
자바는 이러한 태그달린 클래스보다 다양한 의미의 객체를 표현하는 훨신 나은 수단을 제공하는데, 이를 서브타이핑(SubTyping)이라고 한다.
태그 달린 클래스를 계층 구조로 바꾸는 법
가장 먼저 root가 될 추상 클래스를 정의하고 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언한다. 태그 값에 상관없이 일정한 동작을 하는 메서드들은 루트 클래스로 옮긴다.
다음으로, 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의한다. 우리의 예시에서는 Figure를 확장한 Circle 클래스와 Rectangle 클래스를 만들면 된다. 각 하위 클래스에는 각자의 의미에 해당하는 데이터 필드들을 넣는다. 원에는 반지름(radius)을, 사각형에는 길이(length)와 넓이(width)를 넣으면 된다.
태그 달린 클래스를 계층 구조로 변환
이렇게 함으로써 태그 달린 클래스의 단점을 모두 날려버린다. 간결하고 명확하면서, 각 의미를 독립된 클래스에 담아 관련 없던 데이터 필드를 모두 제거했다. 살아남은 필드는 모두 final이므로 각 클래스의 생성자가 모든 필드를 남깅벗이 초기화하고 추상 메서드를 모두 구현했는지 컴파일러가 확인을 해준다.
태그 달린 클래스를 사용하는 상황은 없다. 새로운 클래스를 작성하는데 태그가 등장한다면 계층구조로 대체하는 방법을 생각해 보자
24 Nested class
중첩 클래스(nested class)란 다른 클래스 안에 정의된 클래스를 말한다. 중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야한다. 중첩 클래스의 종류는 정적 멤버 클래스, 비정적 멤버 클래스, 익명 클래스, 지역 클래스 이렇게 네 가지다. 이 중 첫번째를 제외한 나머지는 중첩 클래스(inner class)에 해당한다. 이번에는 언제 어떤 중첩 클래스를 사용해야하고 왜 사용해야하는지 알아보자
정적 멤버 클래스
정적 멤버 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고는 일반 클래스와 똑같다. 흔히 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 쓰인다. 계산기가 지원하는 연산 종류를 정의하는 열거 타입을 예로 들어서 생각해보자. Operation 열거 타입은 Calculator 클래스의 public 정적 멤버 클래스가 되어야 한다. Calculator의 클라이언트에서 Calculator.Operation.PLUS나 Calculator.Operation.MINUS와 같은 형태로 원하는 연산을 참조할 수 있다.
정적 멤버 클래스와 비정적 멤버 클래스의 차이는 단지 static이 있는지에 대한 여부 뿐이지만 의미상 차이는 의외로 꽤 크다. 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다 그래서 비 정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다. 따라서 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다. 비정적 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없기 때문이다.
비정적 멤버 클래스
비정적 멤버 클래스의 인스턴스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화될 때 확립되며 더 이상 변경할 수 없다.
비정적 멤버 클래스는 어댑터를 정의할 때 자주 쓰인다. 즉, 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용하는 것이다. 예컨대 Map 인터페이스의 구현체들은 보통 자신의 컬렉션 뷰를 구현할 때 비정적 멤버 클래스를 사용한다.
멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만드는 것이 좋다. static을 생략할 시에 바깥 인스턴스로의 숨은 외부 참조를 가지게 되고 가비지 컬렉션 등의 문제가 생길 수 있다.
익명 클래스
익명 클래스는 바깥 클래스의 멤버가 아니다. 멤버와 달리 쓰이는 시점에 선언과 동시에 인스턴스가 만들어 진다. 코드의 어디서든지 만들 수 있고, 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있다
자바 7엑서 람다를 지원하기 전에는 즉석에서 작은 함수나 객체 처리를 하기 위해 주로 사용했었지만 이제 람다에게 자리를 내어 주었다(item42)
지역 클래스
지역 클래스는 네 가지 중첩 클래스 중에서 가장 드물게 사용된다. 지역 클래스는 지역변수를 선언할 수 있는 곳이면 실질적으로 어디서든 선언할 수 있고, 유효 범위도 지역변수와 같다.
중첩 클래스에는 네 가지가 있으며, 각각의 쓰임새가 다르다. 메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길다면 멤버 클래스로 만든다. 멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 바깥으로, 그렇지 않다면 정적으로 만들자.
25
26
27 비검사 경고를 제거하라
제네릭을 사용하면 컴파일러 경고를 볼 수 있다.
컴파일러가 알려주는 비검사 경고는 최대한 제거하는 편이 좋다.
경고를 제거할 수는 없지만 타입이 안전하다고 판단되면 @SuppressWarning("unchecked") 어노테이션을 사용한다.
@SuppressWarning 어노테이션은 최대한 작은 범위에서 사용하는 편이 좋다. 최대한 명확한 비검사 경고를 제공하기 위해서다.
메서드 안의 지역변수에도 할당할 수 있다.
예, ArrayList 클래스의 toArray 메서드 중 타입 캐스팅 을 사용하는 Arrays.copyOf 메서드를 리턴하기 전 지역변수를 선언하고 비검사 경고를 제거할 수 있다.
비검사 경고를 제거할 때 확실한 타입이 검증되지 않은 상태에서 사용하면 컴파일은 되지만 런타임에서 CastClassException 이 날수 있다. 또한 확실한 타입이 검증되었으나 비검사 경고를 제거하지 않으면 다른 오류들을 반환하지 않고 비검사 경고만 반환하기 때문에 자칫 중요한 경고를 알지 못할 수 있다.
비검사 경고를 제거할 때는 주석으로 다음 프로그래머에게 알려줄 이유를 남겨야 한다.
28 배열보다는 리스트를 사용하라
배열은 공변 ( 상위 ( 부모 ) 타입으로 하위 ( 자식 ) 타입을 치환 할 수 있다 ) 이다. 제네릭은 불공변이다.
배열은 우리가 흔히 사용하는 타입처럼 부모 <- 자식 의 좁은 타입으로 교체가 가능하지만 제네릭은 고정된 ( 컴파일 과정에서 확정하는 ) 타입 외에는 사용할 수 없다. ( 교체가 불가하다. )
따라서 배열은 하위 타입의 배열로 구현 할 수 있고, 정의한 타입이 아닌 다른 타입을 사용할 때 런타임에서야 알 수 있지만, 제네릭을 사용하는 리스트는 어차피 고정된 타입 외에는 사용할 수 없으므로 컴파일 전부터 에러를 반환한다.
배열은 런타임에도 자기 자신이 담고 있는 원소의 타입을 인지하고 확인한다. 따라서 자신이 구현한 타입인지 아닌지를 런타임에도 확인할 수 있다. 제네릭은 타입 정보가 런타임때 소거되어 런타임 중에는 알 수 없다. 따라서 컴파일 과정에서만 검사하여 미리 걸러낸다.
이러한 차이점으로 인해 제네릭타입의 배열 타입은 함께 사용할 수 없다.
실체화 불가 타입은 런타임에 컴파일타임보다 더 적은 타입 "정보"를 갖는것을 말한다. 소거 매커니즘 때문에 매개변수화 타입 가운데 실체화될 수 있는 타입은 List<?> 와 Map<?, ?> 같은 와일드 카드 타입뿐이다.
제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는 게 보통은 불가능하다. 제네릭과 가변인수 메서드를 함께 사용할 때 가변인수 매개변수를 담을 배열이 생성되면서 배열이 담을 원소가 실체화 불가 타입 이라면 에러를 반환한다.
타입에 안전하지 않은 배열 보다는 안전한 제네릭을 포함한 리스트를 사용하는 것이 좋다.
실체화 불가 타입은 런타임에 컴파일타임보다 더 적은 타입 "정보"를 갖는것을 말한다.
아무리 찾아도 이펙티브 자바에서 나온 내용 뿐 https://jojoldu.tistory.com/25 아주 좋은 예시지만 이해를 못함 와일드 카드 알 수 없는( 알 필요 없는? ) 매개 변수를 표현하는 제네릭 실제 컴파일때 정의되는 타입에 중점을 두기 보단 그 타입을 가지고 행하는 메서드에 중점을 둠 예를 들어 List<?> 를 선언하고 받는다면 실제 들어오는 List 인터페이스의 메서드를 실행하는데 에 중점을 둘때 사용함
<? super A> 가능 A 의 super 클래스 삽입 가능 -> 그럼 로타입으로 써도 가능하지만 타입의 불변성을 깨고 싶지 않기 때문에 와일드 카드를 사용함.
Last updated