본문 바로가기

독서찰기(讀書札記)/이펙티브 자바

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

[Why]

  • 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다.
    자바는 단일 상속만 지원하니, 추상 클래스 방식은 새로운 타입을 정의하는 데 커다란 제약을 안게 되는 셈이다.
  • 인터페이스가 선언한 메서드를 모두 정의하고 일반 규약을 잘 지킨 클래스라면 다른 어떤 클래스를 상속했든 같은 타입으로 취급된다.
    • 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다. 클래스 선언에 implements 구문만 추가하면 된다.
    • 기존 클래스 위에 새로운 추상 클래스를 끼워넣기는 일반적으로 어렵다.
  • 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다.
  • 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
    • e.g.) Singer 인터페이스, Songwriter 인터페이스, SingerSongwriter 인터페이스
    • 같은 구조를 클래스로 만들려면 가능한 조합 전부를 각각의 클래스로 정의한 고도비만 계층구조가 만들어질 수 있다.
      조합 폭발(combinatorial explosion)

 

※ 믹스인(mixin) : 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래의 '주된 타입' 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다. 예컨대 Comparable은 자신을 구현한 클래스의 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스다. 이처럼 대상 타입의 주된 기능에 선택적 기능을 '혼합(mixed in)'한다고 해서 믹스인이라 부른다.

[When]

  • 자바가 제공하는 다중 구현 메커니즘은 인터페이스와 추상 클래스, 이렇게 두 가지다.
  • 자바 8부터 인터페이스도 디폴트 메서드(default method)를 제공할 수 있게 되어 이제는 두 메커니즘 모두 인스턴스 메서드를 구현 형태로 제공할 수 있다.

[How]

  • 래퍼 클래스 관용구(아이템18)와 함께 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다.
  • 인터페이스의 메서드 중 구현 방법이 명백한 것이 있다면, 그 구현을 디폴트 메서드로 제공해 프로그래머들의 일감을 덜어줄 수 있다.
    • 디폴트 메서드를 제공할 때는 상속하려는 사람을 위한 설명을 @implSpec 자바독 태그를 붙여 문서화해야 한다. (아이템19)
    • 디폴트 메서드의 제약
      • equals, hashCode를 디폴트 메서드로 제공해서는 안 된다.
      • 인스턴스 필드를 가질 수 없고 public이 아닌 정적 멤버도 가질 수 없다(단, private 정적 메서드는 예외다).
      • 내가 만든 인터페이스가 아니면 디폴트 메서드를 추가할 수 없다.
  • 인터페이스추상 골격 구현(skeletal implementation) 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취하는 방법도 있다. 바로 템플릿 메서드 패턴이다.
    • 관례상 인터페이스 이름이 Interface라면 그 골격 구현 클래스의 이름은 AbstractInterface로 짓는다. SkeletonInterface가 더 적절했을지 모르지만, 이미 Abstract를 접두어로 쓰는 형태가 확고히 자리잡았다.
    • e.g.) AbstractCollection, AbstractSet, AbstractList, AbstractMap : 핵심 컬렉션 인터페이스의 골격 구현
static List<Integer> intArrayAsList(int[] a) { 
    Objects.requireNonNull(a); 
    
    return new AbstractList<>() { 
        @Override 
        public Integer get(int i) { 
            return a[i]; // 오토박싱(아이템6) 
        } 
        
        @Override 
        public Integer set(int i, Integer val) { 
            int oldVal = a[i]; 
            a[i] = val; // 오토언박싱 
            return oldVal; // 오토박싱 
        } 
        
        @Override public int size() { 
            return a.length; 
        } 
    }; 
}

// 위 코드는 완벽히 동작하는 List 구현체를 반환하는 정적 팩터리 메서드로, AbstractList 골격 구현으로 활용했다.
// 이 예는 int 배열을 받아 Integer 인스턴스의 리스트 형태를 보여주는 어댑터(Adapter)이기도 하다.
// 익명 클래스(아이템24) 형태를 사용했음에 주목하자.

 

  • 골격 구현 클래스를 우회적으로 이용할 수도 있다.
    • 인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고, 각 메서드 호출을 내부 클래스의 인스턴스에 전달하는 것이다.
    • 아이템18에서 다룬 래퍼 클래스와 비슷한 이 방식을 시뮬레이트한 다중 상속(simulated multiple inheritance)이라 한다.
  • 단순 구현(simple implementation)은 골격 구현의 작은 변종으로, 골격 구현과 같이 상속을 위해 인터페이스를 구현한 것이지만 추상 클래스가 아니란 점이 다르다. 이러한 단순 구현은 그대로 써도 되고 필요에 맞게 확장해도 된다.