Skip to content

Latest commit

 

History

History
493 lines (366 loc) · 12.1 KB

20_추상_클래스_보다는_인터페이스를_우선하라_신선영.md

File metadata and controls

493 lines (366 loc) · 12.1 KB

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

🎊 추상 클래스와 인터페이스

공통점

  • 인스턴스로 생성이 불가능하다.
  • 선언부만 있는 추상 메서드를 갖는다.
    • 인스턴스 메서드를 구현 형태로 제공할 수 있다.

차이점

1. 목적

  • 추상 클래스
    • 추상 클래스를 상속받아 기능을 이용하고 확장시켜야한다.
  • 인터페이스
    • 구현을 강제해서 구현 객체가 같은 동작을 하도록 보장한다.

2. 상속

  • 추상 클래스

    • 단일 상속만 지원하기 때문에, 다중 클래스 상속이 불가능하다.

      // Amy에 Student을 추가로 상속 받고 싶으면? -> 불가능!
      public class Amy extends Person {
        // ...
      }
      
      abstract class Student {
        // ...
      }
  • 인터페이스

    • 구현해야하는 메소드만 구현하면 어떤 클래스를 상속하던 간에 같은 타입으로 취급한다.

    • 여러 인터페이스를 상속이 가능하다.

      // Amy에 Student을 추가로 상속 받고 싶으면? -> implements 구문만 추가하면 됨!
      public class Amy extends Person implements Student {
        // ...
          
      }
      
      interface Student {
        // ...
      }

3. 기존 클래스의 확장

  • 추상 클래스

    • 기존 클래스에 끼워넣기 어렵다.
    // 만약 여기서 추상 클래스 Person을 확장해야한다면?
    public class Amy extends Student {
      // ...
    }
    
    public class Jason extends Student {
      // ...
    }
    
    abstract class Student {
    	// ...
    }
    
    // 이렇게??
    public abstract class Person {
      public abstract void walk();
    }
    
    abstract class Student extends Person {
    	// ...
    }
    
    // Student를 상속 받았던 객체들은 모두 walk()이라는 메소드의 구현을 해야한다.
    public class Amy extends Student {
      // ...
    }
  • 인터페이스

    • 손쉽게 인터페이스를 구현해 넣을 수 있다.
    // 만약 여기서 추상 클래스 Person을 확장해야한다면?
    public class Amy extends Student {
      // ...
    }
    
    public class Jason extends Student {
      // ...
    }
    
    abstract class Student {
    	// ...
    }
    
    // 원하는 객체만 구현을 할 수 있다.
    public class Amy extends Student implements Person {
      // ...
    }

4. 믹스인 정의

특정 클래스의 주 기능에 **추가적인 기능을 혼합(mixed-in)**하는 것이다.

  • 추상클래스

    • 단일 상속만 지원하기 때문에 믹스인이 들어갈만한 자리가 없다.
  • 인터페이스

    • 손쉽게 구현이 가능하다.
      • ex. Comparable, Clonable, Serializable
    public class Amy extends Student implements Comparable {
    
      @Override
      public int compareTo(Object o) {
        return 0;
      }
    }
    // 만약 Comparable이 추상 클래스라면?
    public abstract class Comparable<T> {
      public abstract int compareTo(T o);
    }
    
    // 들어갈 자리가 없다! 😱
    public class Amy extends Student {
    	// ...
    }

5. 계층구조

👧 Amy라는 객체가 Student인 동시에 Intern인 경우에

  • 추상클래스

    • 복잡한 계층구조일 경우에 많은 조합이 필요하고, 고도 비만 계층으로 이어질 수 있다.
    public abstract class Student {
        // ...
    }
    
    public abstract class Intern {
        // ...
    }
    
    // 단일 상속만 가능하기 때문에 인턴인 동시에 학생인 추상 클래스가 필요하다.
    public abstract class StudentIntern {
        // ...
    }
    
    public class Amy extends SutdentIntern {
    	// ...
    }
  • 인터페이스

    • 계층구조가 없는 타입 프레임워크를 만들 수 있다.
    public interface Student {
        // ...
    }
    
    public interface Intern {
        // ...
    }
    
    // 문제 없다.
    public class Amy implements Student, Intern {
    	// ...
    }

6. 기능 추가

  • 추상 클래스
    • 기능을 추가하는 방법이 상속밖에 없다. → 활용도가 떨어지고 깨지기도 쉽다 (래퍼 클래스보다)
  • 인터페이스
    • 래퍼 클래스와 함께 사용하면 상속보다 안전하고 강력하게 기능을 향상시킬 수 있다. (w. item18)

👻 default 메서드

구현 내용이 있는 메소드로, 구현 방법이 명확하다면 인터페이스에서 사용이 가능하다.

public interface Student {
  public void study();
  public default void syaHello() {
    System.out.println("안녕하세요");
  }
}

주의사항

  1. @impleSpec을 붙여 문서화하면 좋다. (w. item19)
  2. Object 메서드 (ex. equals, hashCode)를 디폴트 메서드로 제공하면 안된다.
  3. 인터페이스는 인스턴스 필드를 가질 수 없고, public이 아닌 정적 멤버도 가질 수 없다.
    • Java 9 이후부터는 private static 메서드도 구현이 가능하게 변경되었다.

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

템플릿 메서드 패턴

인터페이스 + 골격 구현 클래스

추상 클래스처럼 구현을 도와주는 동시에, 추상클래스로 타입을 정의할 때 따라오는 심각한 제약에서 자유롭다.

참고한 예제 코드

// 인터페이스
public interface Phone {
  void booting();
  void greeting();
  void shutdown();
  void process();
}
public class IPhone implements Phone {

  @Override
  public void booting() {
    System.out.println("booting ...");
  }

  @Override
  public void greeting() {
    System.out.println("I am iPhone");
  }

  @Override
  public void shutdown() {
    System.out.println("shut down ...");
  }

  @Override
  public void process() {
      booting();
      greeting();
      shutdown();
  }
}
public class GalaxyPhone implements Phone {
  @Override
  public void booting() {
    System.out.println("booting ...");
  }

  @Override
  public void greeting() {
    System.out.println("I am galaxy phone");
  }

  @Override
  public void shutdown() {
    System.out.println("shut down ...");
  }

  @Override
  public void process() {
    booting();
    greeting();
    shutdown();
  }
}

iPhoneGalaxyPhone 모두 같은 인터페이스(Phone)를 구현하고 있고, booting()shutdown()은 같은 동작을 하고 있다.

여기서 추상골격 구현 클래스을 이용하면 중복 코드를 제거할 수 있다.

// 추상골격 구현 클래스 (보통 Abstract~의 네이밍을 사용한다)
public abstract class AbstractPhone implements Phone {

	// 같은 동작을 하는 메소드를 여기에 정의한다.
  @Override
  public void booting() {
    System.out.println("booting ...");
  }

  @Override
  public void shutdown() {
    System.out.println("shut down ...");
  }

  @Override
  public void process() {
    booting();
    greeting();
    shutdown();
  }
}
public class IPhone extends AbstractPhone implements Phone {

  @Override
  public void greeting() {
    System.out.println("I am iPhone");
  }
}

Untitled

실행결과

public class GalaxyPhone extends AbstractPhone implements Phone {
  @Override
  public void greeting() {
    System.out.println("I am galaxy phone");
  }
}

중복 제거 성공!

🧐 개인적인 궁금점!

그냥 Phonedefault를 사용해서 메소드를 구현하면 안되나?

외부에서의 호출을 막고 싶다면 추상클래스로 사용하는게 맞는 것 같다. 추상클래스를 사용하면 protected, private 제어자를 지정할 수 있기 때문이다. 인터페이스는 기본적으로 변수필드는 public static final 이며, 모든 메소드는 public abstract 이므로 인터페이스로 구현할 경우, 템플릿 메소드 내부에서만 호출되어야 할 메소드들이 public 제어자에 의해 의도치 않은 사용처에서 호출될 위험이 있다.

출처

👨‍👨‍👧‍👧 시뮬레이트한 다중 상속

만약 iPhone에서 PhoneManufacturer라는 제조사 클래스를 상속받아야해서 추상골격 구현 클래스를 상속 받지 못한다면?

public class PhoneManufacturer {
  public void printManuFacturer() {
    System.out.println("Made by Apple");
  }
}
// 골격 구현을 확장한 클래스
public class InnerAbstractPhone extends AbstractPhone {

  @Override
  public void greeting() {
    System.out.println("I am iPhone");
  }
}
public class IPhone extends PhoneManufacturer implements Phone {
  InnerAbstractPhone innerAbstractPhone = new InnerAbstractPhone(); // 내부 클래스로 정의

  @Override
  public void booting() {
    innerAbstractPhone.booting();
  }

  @Override
  public void greeting() {
    innerAbstractPhone.greeting();
  }

  @Override
  public void shutdown() {
    innerAbstractPhone.shutdown();
  }

  @Override
  public void process() {
    printManuFacturer();
    innerAbstractPhone.process();
  }
}

Untitled 1

위와 우회적 골격 구현을 사용할 수 있다. 이와 같은 방식을 **시뮬레이트한 다중 상속(simulated multiple inheritance)**이라고 한다.

골격 구현 작성 방법

  1. 인터페이스를 보며 다른 메서드들의 구현에 사용되는 기반 메서드 선정
  2. 기반 메서드들을 사용해 구현할 수 있는 메서드들을 디폴트 메서드로 제공
  3. 기반 메서드나 디폴트 메서드로 만들지 못한 메서드는 해당 인터페이스를 구현하는 골격 구현 클래스에서 작성
  4. 설계, 문서화 필수!

😶 단순 구현

상속을 위해 인터페이스를 구현했으나 추상클래스가 아닌 것

ex. AbstractMap.SimpleEntry

골격 구현으로 만든 추상 클래스

참고

// 골격 구현 클래스
abstract class CustomAbstractMapEntry<K, V> implements Map.Entry<K, V> {
  private K key;
  private V value;

  public CustomAbstractMapEntry(final K key, final V value) {
    this.key = key;
    this.value = value;
  }

  abstract void printKey();

  abstract void printValue();

  @Override
  public K getKey() {
    return key;
  }

  @Override
  public V getValue() {
    return value;
  }

  @Override
  public V setValue(final V value) {
    return this.value = value;
  }
}

// 골격 구현 클래스를 상속받은 클래스
public class CustomMapEntry<K, V> extends CustomAbstractMapEntry<K, V> {

  public CustomMapEntry(K key, V value) {
    super(key, value);
  }

  @Override
  void printKey() {

  }

  @Override
  void printValue() {

  }
}
@Test
public void 단순구현_골격구현_비교() {
  String key = "key";
  String value = "value";

  CustomMapEntry<String, String> customMapEntry = new CustomMapEntry<>(key, value);
  AbstractMap.SimpleEntry<String, String> simpleEntry = new SimpleEntry<>(key, value); // 단순구현 -> 추상 메소드를 구현할 필요가 없다.

  Assert.assertEquals(customMapEntry.getKey(), simpleEntry.getKey());
  Assert.assertEquals(customMapEntry.getValue(), simpleEntry.getValue());
}

Untitled 2

🐣 맺으며

일반적으로 다중 구현을 할 때에는 인터페이스가 가장 적합하다.

만약 복잡한 경우일 때에는 골격 구현을 고려해보자!

단, 골격 구현은 가능한 인터페이스의 default 메서드 제공해야하며, 그 인터페이스를 구현한 모든 곳에서 사용하는 것이 좋다.