티스토리 뷰

728x90
반응형

추상 클래스보다는 인터페이스를 우선하라


  • Java 8버전부터는 인터페이스에도 default 메서드 및 static 메서드를 제공하고 있습니다.
  • default 메서드와 static 메서드는 body를 가질 수 있으며 static 메서드의 경우 인터페이스명.메서드명으로 호출할 수 있습니다.
  • 인터페이스에 정의되는 메서드 중 구현 방법이 명확하고 공통적인 경우라면 default 메서드로 만들게되면 하위 클래스에서 각각 재정의할 필요가 없어집니다.

 

💡예제 코드

  • 아래 예제 코드에서 static 메서드는 구현 클래스에서 재정의할 수 없으며 호출시 인터페이스명.메서드명으로 호출할 수 있습니다.
public interface Calculator {

    default void add(int x, int y) {
        System.out.println("Calculator Call default Method : " + (x + y));
    }

    static void minus(int x, int y) {
        System.out.println("Calculator Call static Method : " + (x - y));
    }
}

public class CalculatorImpl implements Calculator {

    @Override
    public void add(int x, int y) {
        System.out.println("CalculatorImpl Call Override default Method : " + (x + y));
    }
}

public class EffectiveJavaApplication {

    public static void main(String[] args) throws Exception {
        Calculator calculator = new CalculatorImpl();
        calculator.add(10, 20);  // CalculatorImpl Call Override default Method : 30
        Calculator.minus(10, 6); // Calculator Call static Method : 4
    }
}

 

인터페이스의 장점


💡기존 클래스에도 쉽게 새로운 인터페이스를 구현해 넣을 수 있습니다.

  • Food 인터페이스를 구현하는 Apple 클래스가 있습니다. Apple 클래스는 객체간 비교를 통해 정렬할 수 있도록 Comparable을 구현하고자 한다면 implements 위에 인터페이스명만 추가를 하면 쉽게 재정의할 수 있습니다. 
  • 하자민 만약 Food가 추상 클래스였다면 새로운 추상 클래스를 끼워넣어기는 어렵습니다. 자바는 다중 상속을 지원하지 않습니다.
public interface Food {
    
    String getName();
}

public class Apple implements Food, Comparable<Apple> {

    private final String name;
    private final int price;

    public Apple(String name, int price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public int compareTo(Apple o) {
        return Integer.compare(this.price, o.price);
    }
}

 

💡인터페이스는 믹스인(mixin) 정의에 안성맞춤입니다.

  • 믹스인이란 대상 타입의 주된 기능에 선택적 기능을 혼합한다고해서 믹스인이라고 부릅니다.
  • 믹스인 타입정의는 믹스인을 구현한 클래스에 주된 타입외에 특정 행위를 제공한다고 선언하는 효과를 주는데, 위에서 작성한 Apple 하위 클래스의 Comparable이 믹스 인터페이스입니다.
  • 그래서 Apple 클래스는 Food 타입이라는 주된 타입외에 비교하여 정렬할 수 있다고 선언할 수 있습니다. 하지만 추상 클래스로는 믹스인을 정의할 수 없습니다.

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

  • 현실에서 계층으로 구분하기 힘든 개념들을 묶어 제 3의 인터페이스로 정의할 수 있습니다.
  • 가수 인터페이스와 작곡가 인터페이스가 있습니다. 하지만 가수겸 작곡가를 할 수도 있습니다. 그런 경우 SingSongWriter라는 제 3의 인터페이스를 정의하고 Singer, SongWriter를 상속받으면 높은 유연성을 재공할 수 있습니다.
  • 이러한 경우를 인터페이스를 사용하지 않고 클래스를 사용하면 경우의 수가 너무 많이 나올 수 있습니다. 이러한 경우를 조합 폭발(combinatorial exposion)이라는 현상입니다.
public interface Singer {

    default void sing(String name) {
        System.out.println(name + "인 노래를 부릅니다.");
    }
}

public interface SongWriter {

    default void write() {
        System.out.println("작곡을 합니다.");
    }
}

public interface SingSongWriter extends Singer, SongWriter {

    @Override
    default void sing(String name) {
        // 새로운 행위
    }

    @Override
    default void write() {
        // 새로운 행위
    }
}

 

💡래퍼 클래스와 함께 쓰면 기능 향상이 안전하고 강력합니다.

  • 래퍼 클래스를 이용해 Composition으로 사용을 하면 기능 보완 및 확장이 편리하면서 위험성도 없습니다. 하지만, 추상 클래스로 정의할 경우 기능 추가를 할려면 상속밖에 답이 없습니다.

💡제약 사항

  • Object 클래스의 equals, hashCode와 같은 메서드들은 default 메서드로 제공해서는 안됩니다.
  • 인터페이스는 인스턴스 필드를 가질 수 없습니다. 단 public static final 변수를 사용할 수 있으며 해당 키워드는 생략할 수 있습니다.
  • 위에서 말한 정적 멤버는 가질 수 있으나 private 정적은 가질 수 없습니다.
  • 우리가 구현하지 않은 인터페이스에는 디폴트 메서드를 추가할 수 없습니다.

 

추상 골격 구현


  • 인터페이스와 추상 골격 구현을 동시에 작성해서 인터페이스와 추상 클래스의 장점을 모두 가질 수 있습니다.
  • 인터페이스로는 타입을 정의하고, 필요에 따라 default 메서드를 정의합니다.
  • 골격 구현 클래스는 나머지 메서드들까지 구현합니다.
  • 골격 구현 클래스를 확장하는 것만으로 인터페이스 구현의 대부분이 완료되며 이를 템플릿 메서드 패턴이라고 합니다.
  • 자바 라이브러리에선 콜렉션 프레임워크인 AbstractCollection, AbstractSet 등이 골격 구현 클래스입니다.

💡예제 코드

  • Warrior 인터페이스로 타입이 정의되어 있고, AbstractWarrior 추상 클래스를 만들어 equals, hashCode getter, setter들을 만들어 골격을 구현했습니다.
  • attack, guard, applyItem등의 메서드는 사용자가 직접 하위 클래스를 구현해야 합니다.
  • 추상 골격 클래스는 인터페이스에서 구현에 사용될 기반 메서드를 선정해 골격 구현 클래스 내부의 추상 메서드가 됩니다. 그리고 기반 메서드를 통해 직접 구현이 가능한 메서드는 모두 인터페이스의 default 메서드가 되고, 기반 메서드/디폴트 메서드가 되지 못한 equals,hashCode, setter, getter등을 골격 구현 클래스에서 구현하면 됩니다.
public interface Warrior {

    void attack();

    void guard();

    void applyItem(Item item);

    default void taunt() {
        System.out.println("약올리기~");
    }
}

@Setter
@Getter
@ToString
public abstract class AbstractWarrior implements Warrior {
    private String name;
    private int hp;
    private int mp;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Warrior)) return false;
        Warrior that = (Warrior) o;
        return Objects.equals(getHp(), that.getHp())
                && Objects.equals(getMp(), that.getMp())
                && Objects.equals(getName(), that.getName());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getName(), getHp(), getMp());
    }
}

 

💡정리

  • 다중 상속이 필요한 경우 인터페이스를 사용하면 됩니다.
  • 안터페이스가 복잡할 경우 골격 구현을 가이 제공하는 방법을 고려해봐야 합니다.
  • 골격 구현은 가능한 인터페이스의 default 메서드로 제공해서 인터페이스 구현체가 활용하는게 좋습니다.

 

 

참고 자료)

https://catsbi.oopy.io/1a9e0c15-464d-436d-806c-3713e6c4e224

 

 

 

 

 

728x90
반응형