Effective Java - Item 20

Updated:

Effective Java

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

추상 클래스의 한계

추상 클래스(Abstract Class)는 인터페이스와 같이 다른 클래스들에게서 공통으로 가져야하는 메소드들의 원형을 정의하고 그것을 상속받아서 구현도록 하는데 사용한다. 또한 자바 8 부터 인터페이스도 default method를 제공하여 인스턴스 메서드를 구현 형태로 제공할 수 있다.

하지만 추상클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 하며, 계층 구조상 상속받는 여러 클래스의 공통 조상이 되어야 하므로 제약이 있다.

추상 클래스와 인터페이스의 차이점

  • 추상 클래스(단일상속) / 인터페이스(다중상속)
  • 추상 클래스의 목적은 상속을 받아서 기능을 확장시키는 것(부모의 유전자를 물려받는다)
  • 인터페이스의 목적은 구현하는 모든 클래스에 대해 특정한 메서드가 반드시 존재하도록 강제하는 역할(부모로부터 유전자를 물려받는 것이 아니라 사교적으로 필요에 따라 결합하는 관계) 즉, 구현 객체가 같은 동작을 한다는 것을 보장하기 위함

인터페이스의 유연성

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

  2. 인터페이스는 믹스인 정의에 안성맞춤이다.

    믹스인 : 원래의 클래스의 ‘주된 동작’외에도 특정 선택적 행위를 제공
    EX) Comparable, Cloneable, Serializable
    이에 반해 추상 클래스는 단일 상속이며 부모-자식 관계이므로 기존 클래스에 덧씌울 수가 없다.

  3. 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
    (생물 -> 식물, (동물 -> 포오류, 조류))와 같이 타입을 계층적으로 정의하여 구조적으로 잘 표현할 수 있는 개념이 있는 반면, [가수, 작곡가, 가수겸 작곡가] 처럼 계층적으로 표현하기 어려운 개념이 존재한다.
    이런 계층구조가 모호한 개념은 인터페이스로 만들기 적합하다.
public interface Singer {
    AudioClip sing(Song song);
}
public interface SongWiter {
    Song compose(int chartPosition);
}
// 2개의 인터페이스를 확장하여 제 3의 인터페이스를 정의한다.
public interface SingerSongWiter extends Singer, SongWiter {
    AudioClip strum();
    void actSensitive();
}

그렇다면 추상 클래스로 위 개념을 표현하면 어떻게 될까

public abstract class Singer {
    abstract void sing(String s);
}
public abstract class SongWriter {
    abstract void compose(int chartPosition);
}

public abstract class SingerSongWriter {
    abstract void strum();
    abstract void actSensitive();
    abstract void Compose(int chartPosition);
    abstract void sing(String s);
}

추상 클래스는 다중 상속이 되지 않기 때문에, Singer와 Songwriter의 메서드를 가진 새로운 클래스를 만들어 클래스 계층을 표현 할 수 밖에 없다.
속성의 수가 많아지면, 더 많은 조합이 필요하고 결국엔 고도비만 계층구조가 만들어진다.(조합 폭발)

  1. 래퍼 클래스 관용구와 함께 사용한다면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다. 타입을 추상 클래스로 정의해두면 그 타입에 기능을 추가하는 방법은 상속 뿐이다.
    상속해서 만든 클래스는 Wrapper 클래스보다 활용도가 떨어지고 캡슐화가 쉽게 깨진다.

자바 8에서는 default 메서드가 추가되어, 공통된 메서드는 인터페이스 내에서 구현할 수 있다.
하지만 디펄트 메서드에는 제약이 있는데,

  • equals와 hashCode 같은 Object의 메서드는 디폴드 메서드로 제공해서는 안된다.
  • 인터페이스는 인스턴스 필드를 가질 수 없다.
  • public이 아닌 정적 멤버를 가질 수 없다.
  • 자신이 만들지 않은 인터페이스에는 디펄트 메서드를 추가할 수 없다.

인터페이스 + 추상 골격 구현 클래스 = 템플릿 메서드 패턴

인터페이스와 추상 골격 구현 클래스를 함께 제공하는 방법으로 둘 다의 장점을 모두 취하는 방법도 있다.

인터페이스로 타입을 정의하고, 필요시 default 메서드도 제공한다.
이후 골격 구현 클래스는 나머지 메서들 까지 구현한다.

이를 통하여 단순히 골격 구현을 확장하는 것만으로 이 인터페이스를 구현하는 데 필요한 일이 대부분 완료된다.
관례상 인터페이스 이름이 Interface라면 그 골격 구현 클래스의 이름은 AbstractInterface라고 짓는다.

이제 추상 골격 구현 클래스의 사용 이유와 사용법을 알아본다.
Car 인터페이스가 있고 이를 구현하는 소나타와 그랜저가 있다고 하자,

public interface Car{
    void start();
    void accelPedal();
    void breakPedal();
    void stop();
    void process();
}
public class Sonata implements Car{
    int speed = 0;
    @Override
    public void start() {
        System.out.println("start Engine");
    }

    @Override
    public void accelPedal() {
        speed += 10; 
    }

    @Override
    public void breakPedal() {
        speed -= 10; 
    }

    @Override
    public void stop() {
        System.out.print("Stop Engine");
    }

    @Override
    public void process() {
        start();
        accelPedal();
        breakPedal();
        stop();
    }
}

public class Grandeur implements Car{
    int speed = 0;
    @Override
    public void start() {
        System.out.println("start Engine");
    }

    @Override
    public void accelPedal() {
        speed += 20; 
    }

    @Override
    public void breakPedal() {
        speed -= 20; 
    }

    @Override
    public void stop() {
        System.out.print("Stop Engine");
    }

    @Override
    public void process() {
        start();
        accelPedal();
        breakPedal();
        stop();
    }
}

두 구현체 모두 Car 인터페이스를 구현한다. 여기서 둘다 속도를 조절하는 accelPedal()과 breakPedal()을 제외하고 전부 다 똑같이 동작을 한다.
중복 코드를 제거하기 위해 추상클래스로 변환하지 않고 추상 골격 클래스로 변환해보자.

// 추상 골격 구현 클래스
public abstract class AbstractCar implements Car{
    int speed = 0;
    @Override
    public void start() {
        System.out.println("start Engine");
    }
    @Override
    public void stop() {
        System.out.print("Stop Engine");
    }
    @Override
    public void process() {
        start();
        accelPedal();
        breakPedal();
        stop();
    }
}
public class Sonata extends AbstractCar implements Car{
    @Override
    public void accelPedal() {
        speed += 10;
    }

    @Override
    public void breakPedal() {
        speed -= 10;
    }
}

public class Grandeur extends AbstractCar implements Car{
    @Override
    public void accelPedal() {
        speed += 20;
    }

    @Override
    public void breakPedal() {
        speed -= 20;
    }
}

인터페이스의 디폴트 메서드를 사용하지 않고 추상 골격 구현 클래스를 제작하여 중복을 제거하였다.

만약 Car를 구현하는 클래스가 CarManuFacturer라는 제조사 클래스를 상속받아야 하여 추상 골격 클래스 구현을 확장하지 못하는 경우에는 어떻게 해야할까?

public class CarManufacturer{
    public void ManufacturerName() {
        System.out.println("hyundai");
    }
}
public class genesis extends CarManufacturer implements Car{
    InnerAbstractCar iac = new InnerAbstractCar();
    @Override
    public void start() {
        iac.start();			// 추상 골격 클래스에서 구현한 메서드 호출
    }
    
    @Override
    public void accelPedal() {
        iac.accelPedal();
    }

    @Override
    public void breakPedal() {
        iac.breakPedal();
    }	
    
    @Override
    public void stop() {
        iac.stop();
    }

    @Override
    public void process() {
        ManufacturerName();  	// 상속받은 클래스의 메서드 사용
        iac.process();
    }
    // Car에서 중 미구현 된 메서드를 구현한다.
    private class InnerAbstractCar extends AbstractCar{  

        @Override
        public void accelPedal() {
            speed += 30;
        }

        @Override
        public void breakPedal() {
            speed -= 30;
        }
    }	
}

기존에 다른 클래스로 부터 상속을 받았어도, InnerClass를 통해 추상 골격 구현 클래스를 상속받아 사용 할 수 있다. 인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고 각 메서드 호출을 InnerClass의 인스턴스에 전달하여 추상 골격 구현 클래스를 우회적으로 이용하는 방식을 시뮬레이트한 다중 상속이라고 한다.

단순 구현(Simple implementation)

단순 구현은 골격 구현의 작은 변종으로, 상속을 위해 인터페이스를 구현한 것이지만, 추상 클래스가 아니다.
단순 구현은 추상 클래스와 다르게 그대로 써도 되거나 필요에 맞게 확장해도 된다.
EX) AbstractMap.SimpleEntry

정리

일반적으로 다중 구현용 타입은 인터페이스가 가장 적합하다.
복잡한 인터페이스라면 골격 구현을 통해 함께 제공하라.
인터페이스의 default 메서드는 제약이 많기 때문에 가능한 골격 구현으로 제공하자.

출처

Joshua Bloch. Effective Java 3/E. n.p.: 인사이트, 2018년 11월 1일.

Reference

https://jyami.tistory.com/81
https://javabom.tistory.com/22