본문 바로가기
개발 공부/Java

이펙티브 자바 아이템 20 - 추상 클래스보다 인터페이스를 우선하라 - 핵심 정리

by 개발인생 2022. 11. 28.
반응형

아이템 20 - 추상 클래스보다 인터페이스를 우선하라 - 핵심 정리

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

추상 클래스와 인터페이스는 자주 비교가 된다.

보통 인터페이스는 타입을 정의 할 때 사용되고

추상 클래스는 인터페이스의 구현체를 일부 기본적으로 제공 할 때 사용한다.

추상 클래스를 상속해 기능을 제공하기보다는 인터페이스를 우선적으로 사용해야한다.

하나의 추상 클래스만 상속 받을 수 있기 때문에 제약이 심해진다.

추상 클래스를 상속받아야 하는 클래스가 다른 클래스를 상속 받았을 수도 있고

여러 클래스가 동일한 클래스를 상속받아야 하는데 둘 간의 계층 구조가 만들어진 경우 추상 클래스 상속이 어려워진다.

인터페이스의 장점

  • 자바 8부터 인터페이스도 디폴트 메서드를 제공할 수 있다.
public interface TimeClient {

    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                        int hour, int minute, int second);
    LocalDateTime getLocalDateTime();

    static ZoneId getZonedId(String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString + "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }

    default ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZonedId(zoneString));
    }
}
public class SimpleTimeClient implements TimeClient {

    private LocalDateTime dateAndTime;

    public SimpleTimeClient() {
        dateAndTime = LocalDateTime.now();
    }

    public void setTime(int hour, int minute, int second) {
        LocalDate currentDate = LocalDate.from(dateAndTime);
        LocalTime timeToSet = LocalTime.of(hour, minute, second);
        dateAndTime = LocalDateTime.of(currentDate, timeToSet);
    }

    public void setDate(int day, int month, int year) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime currentTime = LocalTime.from(dateAndTime);
        dateAndTime = LocalDateTime.of(dateToSet, currentTime);
    }

    public void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime timeToSet = LocalTime.of(hour, minute, second);
        dateAndTime = LocalDateTime.of(dateToSet, timeToSet);
    }

    public LocalDateTime getLocalDateTime() {
        return dateAndTime;
    }

    public String toString() {
        return dateAndTime.toString();
    }

    public static void main(String... args) {
        TimeClient myTimeClient = new SimpleTimeClient();
        System.out.println(myTimeClient);
        System.out.println(myTimeClient.getZonedDateTime("America/Los_Angeles"));
    }
}

디폴트 메서드를 이용해 손쉽게 기능을 확장하고 인터페이스를 진화시킬 수 있다.

또한 static 메서드도 추가가 가능하다.

하지만 인스턴스 필드를 사용 해야하는 경우에는 기본 구현체를 제공할 수 없기 때문에 추상 클래스를 사용해야한다.

  • 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다. (선택적인 기능 추가)
public class SimpleTimeClient implements TimeClient, AutoCloseable {
      // 구현 생략 
}

클래스의 주요한 역활이 있을 때 부가적으로 다른 인터페이스를 implements 함으로써 부가적인 기능들을 추가할 수 있다.

클래스라면 추상 클래스를 하나 이상 상속하기 불가능하기 때문에 믹스인(mixin) 으로 추상클래스를 사용하기에는 많은 제약이 따른다.

  • 계층구조가 없는 타입 프레임워크를 만들 수 있다.

계층 구조가 명확하다면 상속구조로 만들면 되지만 관계가 명확하지 않은 것들이 있다.

가령 사각형 클래스를 상속받는 직사각형, 마름모 등등이 관계가 명확한 계층구조이다.

가수, 작사가 의 관계에는 계층 구조가 있는 것이 아니다.

그러나 가수 중에는 노래도 하면서 작사를 하는 가수가 있을 것이다.

public interface Singer {

    AudioClip sing(Song song);
}
public interface Songwriter {

    Song compose(int shartPosition);
}
public interface SingerSongwriter extends Singer, Songwriter{

    AudioClip strum();
    void actSensitive();
}

인터페이스를 조합하여 새로운 타입을 만들 수도 있다.

  • 래퍼 클래스와 함께 사용하면 인터페이스는 기능을 향상 시키는 안전하고 강력한 수단이 된다.
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)           {
       System.out.println("?????");
       return s.add(e);      }
    public boolean remove(Object o)   { return s.remove(o);   }
    public boolean containsAll(Collection<?> c)
                                   { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c)
                                   {
                                      return s.addAll(c);      }
    public boolean removeAll(Collection<?> c)
                                   { return s.removeAll(c);   }
    public boolean retainAll(Collection<?> c)
                                   { return s.retainAll(c);   }
    public Object[] toArray()          { return s.toArray();  }
    public <T> T[] toArray(T[] a)      { return s.toArray(a); }
    @Override public boolean equals(Object o)
                                       { return s.equals(o);  }
    @Override public int hashCode()    { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}

추상 클래스를 상속하는 방식으로 기능을 향상시킬 때는 캡슐화 가 깨지기 때문에

상위 클래스의 구현에 따라 모든 하위 클래스에 영향이 가게된다.

래퍼 클래스와 함께 인터페이스를 사용하는 경우라면 기능을 확장하기 더욱 안전하다.

인터페이스와 추상 골격 클래스

인터페이스와 추상 클래스를 같이 사용하는 방법이다.

인터페이스는 디폴트 메서드를 구현해 기존의 인터페이스를 진화시킬 수 있다는 장점이 있고,

추상 클래스는 인터페이스의 디폴트 메서드로 구현하지 못하는 메서드들을 필드나 다른 기능을 사용해

구체적으로 구현할 수 있다는 장점이 있다.

추상 클래스의 장점과 인터페이스의 장점을 같이 사용할 수 있다.

인터페이스에 구현할 수 있는 메서드들은 디폴트 메서드로 구현하고 그 외의 기능들은 추상 클래스에 구현하는 방법이다.

이렇게 인터페이스를 구현해 뼈대를 이루고 있는 추상 클래스를 추상 골격 클래스, 스켈레톤 클래스 라고 한다.

// 코드 20-1 골격 구현을 사용해 완성한 구체 클래스 (133쪽)
public class IntArrays {
    static List<Integer> intArrayAsList(int[] a) {
        Objects.requireNonNull(a);

        // 다이아몬드 연산자를 이렇게 사용하는 건 자바 9부터 가능하다.
        // 더 낮은 버전을 사용한다면 <Integer>로 수정하자.
        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;
            }
        };
    }

    public static void main(String[] args) {
        int[] a = new int[10];
        for (int i = 0; i < a.length; i++)
            a[i] = i;

        List<Integer> list = intArrayAsList(a);
        Collections.shuffle(list);
        System.out.println(list);
    }
}

intArrayAsList 메서드는 List 의 구현체를 return 해야한다.

List 의 구현체를 모두 구현하기는 쉽지않다.

하지만 스켈레톤 역할을 하는 추상 클래스인 AbstractList 를 통해 구현한다면

일부분만 재정의할 수 있게되어 손쉽게 구현이 가능해진다.

인터페이스와 추상 클래스의 장점 모두를 가지고 있다.


인터페이스와 추상 클래스를 사용해 다중 상속 비슷하게 사용할 수 있다.

public abstract class AbstractCat {

   protected abstract String sound();

   protected abstract String name();
}
public class MyCat extends AbstractCat{

    private MyFlyable myFlyable = new MyFlyable();

    @Override
    protected String sound() {
        return "인싸 고양이 두 마리가 나가신다!";
    }

    @Override
    protected String name() {
        return "유미";
    }

   public static void main(String[] args) {
      MyCat myCat = new MyCat();
      System.out.println(myCat.sound());
      System.out.println(myCat.name());
   }
}

위와 같이 추상 클래스를 상속받은 클래스가 있다고 해보자.

public interface Flyable {

    void fly();
}
public class AbstractFlyable implements Flyable {

    @Override
    public void fly() {
        System.out.println("너랑 딱 붙어있을게!");
    }
}

인터페이스와 해당 인터페이스를 구현한 추상 클래스를 추가로 만들었다.

이때 인터페이스와 추상 클래스를 활용해 마치 상속을 하나 더 받는 것처럼 하려면

public class MyCat extends AbstractCat implements Flyable {

    private MyFlyable myFlyable = new MyFlyable();

    @Override
    protected String sound() {
        return "인싸 고양이 두 마리가 나가신다!";
    }

    @Override
    protected String name() {
        return "유미";
    }

    @Override
    public void fly() {
        this.myFlyable.fly();
    }

    private class MyFlyable extends AbstractFlyable {
        @Override
        public void fly() {
            System.out.println("날아라.");
        }
    }

   public static void main(String[] args) {
      MyCat myCat = new MyCat();
      System.out.println(myCat.sound());
      System.out.println(myCat.name());
      myCat.fly();
   }
}

위처럼 내부 클래스를 만들고 메서드를 위임하는 방식으로 다중 상속을 받은 것 처럼 구현할 수 있다.

시뮬레이트한 다중 상속 이라고 부른다.

이때 주의할 점은 중간의 추상 클래스들은 상속을 고려해 아이템19 의 내용을 따라야한다.

반응형

댓글