티스토리 뷰

(Effective Java) 규칙20. 추상 클래스 보다는 인터페이스를 우선하라


추상 클래스, 인터페이스


  • 자바 언어에는 여러 가지 구현을 허용하는 자료형을 만드는 방법이 두가지 포함되어 있음.

    • 인터페이스, 추상클래스(abstract class)
  • 이 두 방법의 분명한 차이는 추상 클래스는 구현된 클래스를 포함할 수 있지만 인터페이스는 아니라는 것임.

    • 자바 1.8 부터는 'default' 메서드를 통해 인터페이스에도 구현을 포함시킬 수 있음
  • 좀 더 중요한 차이는 추상 클래스를 자료형으로 사용하기 위해서는 반드시 계승이 필요하다는 것이다.

    • 인터페이스는 포함된 모든 메서드를 정의하고 인터페이스가 규정하는 일반 규약을 지키기만 하면됨
    • 자바는 다중 상속(multiple inheritance)를 허용하지 않기 때문에 추상클래스를 사용하게 되면 자료형으로 사용하는데 많은 제약이 발생하게 됨

인터페이스의 이점


이미 있는 클래스를 개조해서 새로운 인터페이스를 구현하도록 하는 것은 간단하다.

  • 필요한 메서드가 다 있는지 확인해서 없으면 추가한 다음 implements절을 추가하는 것이 전부

    • 예) 자바 플랫폼에 Comparable 인터페이스가 추가된 이후, 많은 기존 클래스들이 Comparable을 구현하도록 개조됨
  • 인터페이스는 믹스인(mixin)을 정의하는 데 이상적이다.

    • 믹스인이란?
      • 클래스가 "주 자료형(primary type)" 이외에 추가로 구현할 수 있는 자료형으로, 어떤 선택적 기능을 제공한다는 사실을 선언하기 위해 쓰임
      • 예) Comparable은 자기 객체는 다른 객체와의 비교 결과에 따른 순서를 갖는다고 선언할 때 쓰는 믹스인 인터페이스
    • 자료형의 주된 기능에 선택적인 기능을 "혼합(mix in)"할 수 있도록 함

인터페이스는 비 계층적인(nonhierarchical) 자료형 프레임워크(type frame-work)를 만들 수 있도록 한다.

  • 어떤 것들은 자료형 계층(type hierachy)으로 잘 정리되지만, 계 안에 잘 들어맞지 않는 것들도 있음

  • 예) 가수를 표시하는 인터페이스와 작곡가를 표현하는 인터페이스

      public interface Singer {
              AudioClip sing(Song s);
      }
      public interface Songwriter {
              Song compose(boolean hit);
      }
    • 문제는 가수 가운데 작곡가인 사람도 있음
    • 추상 클래스 대신 인터페이스를 사용했기 때문에 아무런 문법적 문제 없이 Singer와 Songwriter를 확장한 또 다른 인터페이스를 추가할 수 있음
      public interface SingerSongwriter extends Singer, Songwriter {
              //새로운 메서드를 추가할 수 있음
              AudioClip strum();
              void actSensitive();
      }

인터페이스를 쓰지 않으려면 필요한 속성(attribute) 조합마다 별도의 클래스를 만들어 거대한 클래스 계층이 필요함

  • 필요한 속성이 n개가 있다면 지원해야 하는 조합의 가짓수는 2^n개에 달할 것임

    • 이런 문제는 조합 폭증(combinatorial explosion) 이라는 이름으로 알려져 있음
  • 공통된 행위(behavior)를 모델링하는 자료형이 없으므로, 인자 자료형만 다른 메서들이 다수 포함된 거대한 클래스들이 자리하게 됨

인터페이스를 사용하면 포장 클래스 숙어(wrapper class idiom)을 통해 안전하면서도 강력한 기능 개선이 가능하다.

  • 추상 클래스를 사용해 자료형을 정의하면 프로그래머는 계승 이외의 수단을 사용할 수 없음 (규칙 16)

  • 추상 골격 구현 클래스를 중요 인터페이스마다 두면, 인터페이스의 장점과 추상 클래스의 장점을 결합할 수 있음

    • 추상 골격 구현 클래스란?
      • 인터페이스를 구현하는 데 필요한 노력을 최소화하기 위한 골격구현을 제공하는 클래스임
    • 인터페이스로는 자료형을 정의하고, 구현하는 일은 골격구현 클래스에 맡기면 됨
      • 예) 컬렉션 프레임워크에는 인터페이스별로 골격 구현 클래스들이 하나씩 제공됨
        • AbstractCollection, AbstractSet, AbstractList, AbstractMap 등
    • 골격 구현의 강력함을 아주 잘 보여주는 예
      //골격 구현 위에서 만들어진 완전한 List 구현
      static List<Integer> intArrayAsList(final int[] a) {
              if (a == null) 
                      throw new NullPointerException();
              return new AbstractList<Integer>() {
                      public Integer get(int i) {
                                  return a[i]; //자동 객체화
                      }
      
                      @Override public Integer set(int i, Integer val) {
                                  int oldVal = a[i];
                                  a[i] = val;     //자동 비객체화
                                  return oldVal;  //자동 객체화
                      }
      
                      public int size() {
                                  return a.length;
                      }
              };
      }     
      • int 배열을 Integer 객체의 리스트처럼 볼 수 있도록 하는 어댑터(Adapter) 패턴 적용사례이기도 함
        • 그러나 int, Integer 사이의 변환 과정 때문에 성능이 좋진 않음
      • 정적 팩터리 메서드를 통해 반환되는 객체의 클래스가 숨겨진, 외부에서는 접근이 불가능한 익명 클래스 (anonymous class)라는 점에 주의
  • 골격 구현 클래스의 이점

    • 추상 클래스를 자료형 정의 수단으로 사용했을 때 만족해야 하는 심각한 제약사항들을 따르지 않아도 추상 클래스를 구현할 수 있도록 돕는 다는 것
      • 골격 구현 클래스가 있다면 해당 클래스를 사용해 인터페이스를 구현하는 것이 가장 분명한 프로그래밍 방법임
      • 골격 구현 클래스를 상속하도록 기존 클래스를 변경할 수 없다면, 인터페이스를 직접 구현해도 됨
  • 골격 구현 클래스의 예제 (Map.Entry 인터페이스에 대한 골격 구현 클래스)

    • 인터페이스를 살펴보고 다른 메서드를 구현할 때 쓸 기본 메서드를 추림
      • 이때 추린 메서드들은 추상 메서드로 선언하고, 나머지 메서드들은 실제로 구현해서 넣음
      //골격 구현
      public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {
        //기본(primitive) 메서드
        public abstract K getKey();
        public abstract V getValue();
      
        //변경 가능 맵에 들어갈 Entry는 이 메서드를 재정의해야 한다.
        public V setValue(V value) {
          throw new UnsupportedOperationException();
        }
      
        //Map.Entry.equlas의 일반 규약 구현
        @Override
        public boolean equals(Object o) {
          if (o == this)
            return true;
          if (!( o instanceof Map.Entry))
            return false;
          Map.Entry<?, ?> arg = (Map.Entry) o;
          return equals(getKey(), arg.getKey()) && equals(getValue(), arg.getValue());
        }
        private static boolean equals(Object o1, Object o2) {
          return o1 == null ? o2 == null : o1.equals(o2);
        }
        //Map.Entry.hashCode의 일반 규약 구현
        @Override
        public int hashCode() {
          return hashCode(getKey()) ^ hashCode(getValue());
        }
        private static int hashCode(Object obj) {
          return obj == null ? 0 : obj.hashCode();
        }
      }
    • 골격 구현 클래스는 계승 용도로 설계하는 클래스이므로, 규칙 17의 모든 지침을 따라야 함
  • 골격 구현의 작은 변종 가운데 하나인 간단 구현(simple implementation)

    • 대표적인 예로 AbstractMap.SinpleEntry
      public static class SimpleEntry<K,V> implements Entry<K,V>, java.io.Serializable{
              private final K key;
              private V value;
      
              public SimpleEntry(K key, V value) {
                  this.key   = key;
                  this.value = value;
              }
      
              public SimpleEntry(Entry<? extends K, ? extends V> entry) {
                  this.key   = entry.getKey();
                  this.value = entry.getValue();
              }
      
              public K getKey() {
                  return key;
              }
      
              public V getValue() {
                  return value;
              }
      
              public V setValue(V value) {
                  V oldValue = this.value;
                  this.value = value;
                  return oldValue;
              }
      
              public boolean equals(Object o) {
                  if (!(o instanceof Map.Entry))
                      return false;
                  Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                  return eq(key, e.getKey()) && eq(value, e.getValue());
              }
      
              public int hashCode() {
                  return (key   == null ? 0 :   key.hashCode()) ^
                         (value == null ? 0 : value.hashCode());
              }
      
              public String toString() {
                  return key + "=" + value;
              }
          }
    • 간단 구현은 인터페이스를 구현한 클래스라는 점, 계승을 고려해 만들어진 클래스라는 점에서 골격 구현과 같지만 추상 클래스가 아니라는 점에서 다름
    • 실제로 동작하는 구현체 가운데 가장 간단한 형태

결론


  • 인터페이스는 다양한 구현이 가능한 자료형을 정의하는 일반적으로 가장 좋은 방법이다.
    • 인터페이스를 통해서 강력한 유연성을 얻을 수 있음
  • 다만, 유연하고 강력한 API를 만드는 것보다 개선이 쉬운 API를 만드는 것이 중요한 경우는 예외
    • 이런 경우에는 추상 클래스를 사용해야 하는데, 그 단점은 잘 이해하고 있어야 함
    • 중요한 인터페이스를 API에 포함시키는 경우에는 골격 구현 클래스를 함께 제공하면 어떨지 심각하게 고려해봐야 함
  • public 인터페이스는 극도로 주의해서 설계 해야 함
반응형
댓글