책/Effective Java

인터페이스가 추상 클래스보다 우선인 이유

728x90
반응형

자바가 제공하는 다중 구현 메커니즘 

- Interface (자바 8부터 default 메서드 제공) 

- Abstract Class 

 

인터페이스와 추상 클래스 

공통점 

  • 선언 내용은 존재, 하지만 구현 내용은 없음(추상 메서드를 가진다)
  • 인스턴스 생성이 불가하다. 

각각의 목적 

  • 인터페이스 
    • Has - A 관계 : 함수의 껍데기만 존재하여 구현을 강제화 함 
    • 이를 통해 구현 객체가 같은 동작을 하도록 보장
    • 다중 상속 - 여러개의 인터페이스를 구현이 가능하다. 
    • 인스턴스 필드를 가질 수 없고, public이 아닌 정적 멤버도 가질 수 없다(자바 9 부터 private static 메서드 구현 가능) 
  • 추상 클래스
    • IS - A 관계 : 추상 클래스를 상속하여 해당 클래스의 기능을 이용하고 또는 기능을 추가한다. 
    • 다중 상속 - 여러 클래스를 상속 받을 수 없다. (다이아몬드 문제) 
      • 추상 클래스가 정의한 타입을 구현하는 클래스 - 반드시 추상 클래스의 하위 클래스가 된다. 
      • Java는 단일 상속만 지원하므로 추상 클래스 방식은 새로운 타입을 정의하는데 큰 제약사항을 안고 간다.
        → 하나의 클래스가 이미 다른 클래스를 상속받을 때, 추상 클래스를 상속받는 새로운 타입 정의는 X 
타입 - 구현해야하는 메서드의 집합 정도로 이해 

 

기본적인 인터페이스와 추상 클래스의 차이를 넘어, 인터페이스를 왜 우선시 해야하는 지 살펴보도록 하자 

 

# 인터페이스는 기존 클래스에 손쉽게 구현(추가) 가능 

 

  • 인터페이스에서 요구하는 메서드 추가 및 정의 & implements 추가를 통해 기존 클래스에서도 쉽게 구현 가능
    • ex) Comparable , Iterable, AutoCloseable 인터페이스 추가도 쉽게 구현 
      public class Xonmin extends Human implements WorkingOut, Developable{
      
      }​
  • 하지만 추상클래스는 앞서 설명한 듯이, 기존 클래스에 끼워넣는 것은 어려움 
    • 두 클래스가 같은 추상클래스를 확장해야할 때, 해당 추상클래스는 두 클래스 모두에게 조상이어야한다. 
      → 클래스 계층 구조의 혼란 야기 (새로 추가된 추상 클래스의 모든 자손은 해당 타입을 모두 정의해야하는 상황 발생 가능성)

 

# 인터페이스는 Mixin(믹스인) 정의에 안성맞춤 

 

믹스인 : 클래스가 구현할 수 있는 타입, 
              믹스인을 구현한 클래스에 원래 '주된 타입'외에도 특정 선택적 행위를 제공할 수 있는 효과 
public class Camera implements Comparable{

	@Override
    public int compareTo(Object o){ 	//mixin
    	...
    }
    
    public void shutter(){ // Camera 클래스의 주된 타입 
    	...
    }

}
  • 이처럼 인터페이스를 통해 기존의 클래스 성질에 추가적으로 기능을 제공할 수 있다. 

하지만 추상 클래스는 믹스인을 정의할 수 없다. 

왜냐하면 기존 클래스에 덧씌울 수 없는데, 클래스는 두 부모를 가질 수 없고(다중 상속 불가), 클래스 계층 구조에는 믹스인을 삽입하기에는 합리적 위치가 없다. 

 

본문 이전 아이템인 컴포지션을 이용해서는 가능하다. 

클래스 내부에 추상 클래스를 private으로 가지고 내부에서  abstract 메서드를 재정의 후, 해당 클래스 메서드 내에서 재정의한 메서드를 사용하면 된다. 

public class Camera extends Electronics {

  private C c = new C() { //내부 정의 
    @Override
    int compareTo(Object o) {
      return 0;
    }
  };
  
  public int compareTo(Object o){ //컴포지션 사용
    return c.compareTo(o);
  }
  
  public void shutter(){} //주된 타입
}

abstract class C{

  abstract int compareTo(Object o); 
  
}

 

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

 

타입을 계층적으로 정의하면 수많은 개념을 구조적으로 표현하기 쉽지만, 현실적으로는 계층을 구분하기 어려운 개념도 존재한다.

책에 나온 가수와 작곡가, 싱어송라이터의 계층 구조를 예시로 보자 

 

  • 작곡 가능한 가수, 작곡 불가능한 가수 등 해당 계층을 이분법으로 나누어 클래스를 작성한다고 가정
    • 클래스의 수가 너무 많아진다. → 속성이 n개 일 때 2^n 개 만큼 클래스가 나뉜다. 
  • 앞서도 계속 언급했듯이 클래스 구조로 만든다면 '가수'라는 추상 클래스와 '작곡가'라는 추상 클래스 두가지 모두를 싱어송라이터 클래스는 상속 받을 수 없다. 

interface를 사용한다면 해당 구조를 쉽게 나타낼 수 있다. 

public interface Singer  {
    AudioClip sing(Song s);
}

public interface Songwriter  {
    Song compose(int chartPosition);
}


public interface SingerSongwriter extends Singer, Songwriter  {
    AudioClip strum();
    void actSensitive();
}

 

# Wrapper 클래스와 Interface의 시너지 

 

  • 래퍼 클래스와 인터페이스를 함께 사용한다면 인터페이스의 기능을 향상, 더욱 안전하고 강력하게 하는 방법
  • 타입을 추상클래스로 정의한다면, 그 타입에 기능을 추가하는 방법은 상속 뿐
    • 상속해서 만든 클래스는 래퍼 클래스보다 활용도가 떨어지고 규칙이 깨지기 더욱 쉽다. 
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s= s;}
    
    public void clear() {s.clear();}
    public boolean contains(Object o) { return s.contains(o);}
    public boolean isEmpty() { return s.isEmpty();}
    public int size() { return s.size();}
    public Iterator<E> iterator() { return s.iterator(); }
    public boolean add(E e) { return s.add(e); }
    public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
    
    ...

}

 

# 인터페이스 - default 메서드

 

만약 인터페이스 메서드 중 구현 방법이 명확한 것이 있다면, default 메서드 제공이 가능하다.

 

default 메서드를 제공할 때 규칙 

  • 상속하는 클래스를 위해 @implSpec 자바독 태그를 붙여 문서화
  • equals , hashCode 같은 Object 메서드는 디폴트 메서드로 작성해선 안된다
public interface MetaInterface {

  //static 사용
  public static boolean isNegative(int i){
    return i > 0;
  }

  //default 사용
  public default boolean isOne(int i){
    return isNegative(i) && i==1;
  }
}

 

# TemplateMethod 패턴 [ Interface 와 추상 골격 구현 클래스 제공 ]

 

인터페이스와 추상 골격 구현(skeletal implementation) 클래스를 함께 제공하여 인터페이스와 추상 클래스 장점을 모두 사용 가능

명명 관례 : 인터페이스 이름 - Interface , 골격 구현 클래스 이름 - AbstractInterface

 

  • 인터페이스로 타입을 쉽게 정의 
  • 필요하다면 디폴트 메서드 제공
  • 골격 구현 클래스 - 나머지 메서드들 구현 

이를 통해서 인터페이스를 구현하는데 개발자가 해야하는 대부분의 일이 완료가 된다. 

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

	protected AbstractList(){
    }
}
AbstractList에서 List 인터페이스를 대부분 구현하고 구현하지 않은 메서드는 추상 메서드로 남겨놨다. 
set(int index , E element) 메서드는 기본 구현체를 만들었고 구현 내용은 늘 Exception이 던져지도록 만들었다. 
이를 통해 list에 addAll() 을 하며 클라이언트가 set을 구현해야하는 수고를 덜어줄 수 있다. 
static List<Integer> intArrayAsList(int[] a)  {
    Objects.requireNonNull(a);

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

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

      @Override
      public int size()  {
        return a.length;
      }
    };
  }

 

# 골격 구현 작성 단계(방법)

  1. 인터페이스 내에 다른 메서드들의 구현에 사용되는 기반 메서드를 선정(골격 구현에서는 추상 메서드가 된다.)
    public interface Interface {
    
      public boolean equals();
    
      public int getSize();
    
      public boolean isEmpty();
      
    }​
  2. 기반 메서드들을 사용하여 직접 구현할 수 있는 메서드를 모두 디폴트 메서드로 제공 (단 Obejct 메서드 제외)
    public interface Interface {
    
      public boolean equals();
    
      public int getSize(); //기반 메서드
    
      public default boolean isEmpty(){  //기반 메서드를 통해 만든 default 메서드
        return getSize() > 0;
      }
    }​
    이 때,  인터페이스 메서드 모두가 기반 메서드와 디폴트 메서드가 된다면 골격 구현 클래스를 별도로 만들 이유 X 
  3.  기반 메서드나 디폴트 메서드를 통해 만들지 못한 메서드가 있다면, 이 인터페이스를 구현하는 골격 클래스 생성
    남은 메서드를 작성하여 넣는다. 이 때 구현 클래스가 필요하면 public이 아닌 필드와 메서드를 추가해도 된다. 
    public abstract class AbstractAInterface implements Interface{
    
      @Override
      public boolean equals(Object obj) {
        
        ...
        
        return ..;
      }
    }​


 

  • Map.Entry 인터페이스 
    • getKey, getValue - 기반메서드
    • setValue - 선택적 포함 가능 
    • Object 메서드 - 디폴트 메서드로 제공해선 안되므로, 모두 골격 구현 클래스에서 구현
      public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {
      	
          //변경 가능한 엔트리는 이 메서드를 반드시 재정의해야 한다.
          @Override 
          public V setValue(V value)  {
          	throw new UnsupportedOperationException();
          }
          
          @Override
          public boolean equals(Object o)  {// Map.Entry.hashCode 규약 구현.
          	if(o == this)
              	return false;
              if(!(o instanceof Map.Entry))
              	return false;
              Map.Entry<?, ?> e = (Map.Entry) o;
              return Objects.equals(e.getKey(), getKey())
              	&& Objects.equals(e.getValue(), getValue());
          }
          
         
          @Override 
          public int hashCode()  {  // Map.Entry.hashCode 규약 구현.
          	return Objects.hashCode(getKey())
              	^ Objects.hashCode(getValue());
          }
          
          @Override
          public String toString()  {
          	return getKey() + '=' + getValue();
          }
      }​
결론

일반적으로 다중 구현용 타입에 적합한 방법 - 인터페이스 
복잡한 인터페이스라면 골격 구현을 함께 제공하는 방법 또한 고려할 것 
골격 구현은 가능한 인터페이스의 디폴트 메서드를 통해 제공하고 그 인터페이스를 구현한 모든 곳에서 활용하도록 지향
인터페이스에 걸려있는 구현의 제약으로 골격 구현을 추상 클래스로 제공하는 경우가 더 흔함 
728x90
반응형