[아이템 20] 추상 클래스보다는 인터페이스를 우선하라

gang_shik·2022년 4월 16일
0

Effective Java 4장

목록 보기
6/11
  • 자바가 제공하는 다중 구현 메커니즘인터페이스추상 클래스임, 이렇게 두 메커니즘은 모두 인스턴스 메서드를 구현 형태로 제공할 수 있음

  • 둘의 가장 큰 차이는 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 점임

  • 자바는 단일 상속만 지원하니, 추상 클래스 방식은 새로운 타입을 정의하는 데 커다란 제약을 안게됨 반면 인터페이스가 선언한 메서드를 모두 정의하고 그 일반 규약을 잘 지킨 클래스라면 다른 어떤 클래스를 상속했든 같은 타입으로 취급됨

  • 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있음

    • 인터페이스가 요구하는 메서드를 (아직 없다면) 추가하고 클래스 선언에 implements 구문만 추가하면 끝임
  • 반면 기존 클래스 위에 새로운 추상 클래스를 끼워넣기는 어려운 게 일반적임

    • 추상 클래스는 계층구조상 두 클래스의 공통 조상이어야함 이는 클래스 계층구조에 커다란 혼란을 일으킴

    • 새로 추가된 추상 클래스의 모든 자손이 이를 상속하게 되는 것임, 적절하지 않은 상황에도 강제로 함

  • 인터페이스는 믹스인(mixin) 정의에 안성맞춤임

    • 믹스인이란 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래의 주된 타입 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 줌

    • 예를 들어 Comparable 은 자신을 구현한 클래스의 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스임

    • 이처럼 대상 타입의 주된 기능에 선택적 기능을 혼합(mixed in)한다고 해서 믹스인이라 부름(추상 클래스는 믹스인을 정의할 수 없음)

  • 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있음

    • 계층적으로 정의하기 어려운 상황에서 이에 대해서 둘 다 구현을 해도 되고 이 둘 다를 확장해서 제 3의 인터페이스를 정의하는 등 유연성이 생김
  • 같은 구조를 클래스로 만들려면 가능한 조합 전부를 각각의 클래스로 정의한 고도비만 계층구조가 됨 속성이 n개면 지원할 조합이 2^n개가 되는 조합 폭발이 생김

    • 이는 거대한 클래스 계층구조에는 공통 기능을 정의해놓은 타입이 없으니, 자칫 매개변수 타입만 다른 메서드들을 수없이 많이 가진 거대한 클래스를 낳을 수 있음
  • 래퍼 클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 됨

  • 인터페이스의 메서드 중 구현 방법이 명백한 것이 있다면, 그 구현을 디폴트 메서드로 제공해 프로그래머들의 일감을 덜어줄 수 있음

  • 디폴트 메서드에도 물론 제약은 있음

    • 인터페이스가 equalshashCode 같은 Object의 메서드를 정의하고 있지만, 이들을 디폴트 메서드로 제공해서는 안됨

    • 또한 인터페이스는 인스턴스 필드를 가질 수 없고 public이 아닌 정적 멤버도 가질 수 없음(private 정적 메서드 예외)

    • 마지막으로 만들지 않은 인터페이스에는 디폴트 메서드를 추가할 수 없음

  • 인터페이스와 추상 골격 구현 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취하는 방법도 있음

    • 인터페이스로는 타입을 정의하고 필요하면 디폴트 메서드 몇 개도 함께 제공함

    • 그리고 골격 구현 클래스는 나머지 메서드들까지 구현함

    • 이렇게 해두면 단순히 골격 구현을 확장하는 것만으로 이 인터페이스를 구현하는 데 필요한 일이 대부분 완료됨, 이게 바로 템플릿 메서드 패턴

추상 골격 구현 클래스

추상 골격 구현 클래스 & 템플릿 메소드 패턴

추상 골격 클래스는 인터페이스의 사용과 추상 클래스의 사용을 섞어서 이 두 가지의 장점을 모두 취하는 방법임

예를 들어 메소드에 대한 기본적인 인터페이스가 있다면 이 인터페이스를 추상 클래스에서 구현하여 메소드를 구현함

그리고 하위 클래스에서 추상 클래스를 상속받은 private inner class를 만들면 이 하위 클래스는 추상 클래스에 호출을 위임함으로써 이에 대한 인터페이스를 무엇이든 구현하여 활용할 수 있음

추상 클래스만 상속해서 생기는 문제와 인터페이스만 구현해서 생기는 문제에 대해서 상호보완적으로 적용을 시킬 수 있음

추상 골격 구현 클래스 예시

추가로 이런 방식이 템플릿 메소드 패턴이라고 하였는데 템플릿 메소드 방식을 보게 된다면

템플릿 메소드 방식은 상위 클래스 쪽에 템플릿에 해당하는 메소드가 정의되어 있고, 그 메소드의 정의 안에는 추상 메소드가 사용되고 있음

그리고 추상 메소드를 실제로 구현하는 것은 하위 클래스이고 하위 클래스에서 메소드를 구현하면 구체적인 처리가 결정됨

그래서 아이템 20에서 언급한대로 인터페이스로 타입을 정의하고 필요하면 디폴트 메소드 몇 개도 함께 하고 골격 구현 클래스가 나머지 메소드들까지 구현하다고 했는데 이 방식이 템플릿 메소드 패턴과 유사함을 알 수 있음

템플릿 메소드 패턴


  • 예시

  • List 구현체를 반환하는 정적 팩터리 메서드로, AbstractList 골격 구현으로 활용함

static List<Integer> intArrayList(int[] a) {
		Objects.requireNonNull(a);

		// 다이아몬드 연산자를 이렇게 사용하는 건 자바 9부터 가능함
		// 더 낮은 버전을 사용한다면 <Integer>로 수정하자
		return new AbstractList<>() {
				@Override public Integer get(int i) {
						return a[i]; // 오토박싱(아이템6)
				}

				@Override public Integer get(int i, Integer val) {
						int oldVal = a[i];
						a[i] = val; // 오토언박싱
						return oldVal;
				}

				@Override public int size() {
						return a.length;
				}
		};
}
  • 이 예는 int 배열을 받아 Integer 인스턴스의 리스트 형태로 보여주는 어댑터이기도 함

  • int 값과 Integer 인스턴스 사이의 변환 때문에 성능은 그리 좋지 않음, 이 구현에서 익명 클래스 형태를 사용했음

  • 골격 구현 클래스의 장점은 추상 클래스처럼 구현을 도와주는 동시에, 추상 클래스로 타입을 정의할 때 따라오는 심각한 제약에서는 자유롭다는 점에 있음

  • 골격 구현을 확장하는 것으로 인터페이스 구현이 거의 끝나지만, 꼭 이렇게 해야 하는 것은 아님

  • 구조상 골격 구현을 확장하지 못하는 처지라면 인터페이스를 직접 구현해야함

  • 또한 골격 구현 클래스를 우회적으로 이용할 수도 있음

    • 인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고 각 메서드 호출을 내부 클래스의 인스턴스에 전달하는 것임

    • 이 방식을 시뮬레이트한 다중 상속이라함

  • 골격 구현 작성은 상대적으로 쉬움

    • 인터페이스를 잘 살펴 다른 메서드들의 구현에 사용되는 기반 메서드들을 선정함, 이 기반 메서드들은 골격 구현에서는 추상 메서드가 될 것임

    • 그 다음, 기반 메서드들을 사용해 직접 구현할 수 있는 메서드들을 모두 디폴트 메서드로 제공함(단, equalshashCode 와 같은 Object 메서드는 디폴드 메서드로 제공하면 안됨)

    • 만약 인터페이스의 메서드 모두가 기반 메서드와 디폴트 메서드가 된다면 골격 구현 클래스를 별도로 만들 이유는 없음

    • 기반 메서드나 디폴트 메서드로 만들지 못한 메서드가 남아 있다면, 이 인터페이스를 구현하는 골격 구현 클래스를 하나 만들어 남은 메서드들을 작성해 넣음

    • 골격 구현 클래스에는 필요하면 public이 아닌 필드와 메서드를 추가해도 됨

시뮬레이트한 다중 상속?

시뮬레이트한 다중 상속

자바는 기본적으로 단일 상속만을 지원하기 때문에 여러개를 다중 상속을 받을 수 없음

하지만 이것을 인터페이스는 여러개를 구현할 수 있기 때문에 이를 바탕으로 마치 다중 상속인 것처럼 보이게 할 수 있음, 그리고 실제로 이처럼 활용해 다중 상속처럼 쓰면서 장점만을 취할 수 있음

마치 위의 예시에서 인터페이스를 구현하면서 추상 클래스를 활용해 상속 받는 방식이 이와 같다고 볼 수 있음

여기서 또 시뮬레이트한 다중 상속을 영어로 보면 simulated multiple inheritance인데 이 simulate가 단순하게 시뮬레이션, 모의 실험만 생각하지만 ~인 체하다, 가장하다, 가장된처럼 마치 그런것처럼 보인다는 의미가 있음

그래서 시뮬레이트한 다중 상속은 다중 상속을 못하지만 마치 다중 상속한 것처럼 보이게 하는 것을 의미하고 실제로 그 장점을 활용함에 의미가 있음


  • 예시

  • Map.Entry 인터페이스를 보면 getKey, getValue는 확실히 기반 메서드이며, 선택적으로 setValue도 포함할 수 있음, Object 메서드들은 디폴트 메서드로 제공해서는 안되므로 해당 메서드들은 모두 골격 구현 클래스에 구현함, toString도 기반 메서드를 사용함

public abstract class AbstractMapEntry<K,V>
				implements Map.Entry<K,V> {

		// 변경 가능한 엔트리는 이 메서드를 반드시 재정의해야 함
		@Override public V setValue(V value) {
				throw new UnsupportedOperationException();
		}

		// Map.Entry.equals의 일반 규약을 구현함
		@Override public boolean equals(Object o) {
				if (o == this)
						return true;
				if (!(o instanceof Map.Entry))
						return false;
				Map.Entry<?,?> e = (Map.Entry) o;
				return Objects.equals(e.getKey(), getKey())
						&& Objects.equals(e.getValue(), getValue());
		}

		// Map.Entry.hashCode의 일반 규약을 구현함
		@Override public int hashCode() {
				return Objects.hashCode(getKey())
						^ Objects.hashCode(getValue());
		}

		@Override public String toString() {
				return getKey() + "=" + getValue();
		}
}
  • 골격 구현은 기본적으로 상속해서 사용하는 걸 가정하므로 아이템 19에서 이야기한 설계 및 문서화 지침을 모두 따라야함

  • 단순 구현(simple implementation)은 골격 구현의 작은 변종으로, AbstractMap.SimpleEntry가 좋은 예임

    • 단순 구현도 골격 구현과 같이 상속을 위해 인터페이스를 구현한 것이지만, 추상 클래스가 아니란 점이 다름

    • 동작하는 가장 단순한 구현임, 그대로 써도 되고 필요에 맞게 확장해도 됨

profile
측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 개선시킬 수도 없다

0개의 댓글