본문 바로가기

Reading Record/이펙티브자바

아이템 [20] - 추상 클래스 보다는 인터페이스를 우선하라

자바에는 인터페이스와 추상 클래스를 제공한다. 또한 자바 8 부터는 인터페이스에 default method를 제공하게 되어 인퍼테이스와 추상 클래스 모두 인스턴스 메소드를 구현 형태로 제공할 수 있게되었다.
그렇다면 둘의 차이는 무엇일까? 추상 클래스를 상속받아 구현하는 클래스는 반드시 추상 클래스의 하위 타입이 되어야한다는 점이다. 즉, 자바에서는 단일 상속만 지원 하기 때문에 한 추상 클래스를 상속받은 클래스는 다른 클래스를 상속받을 수 없게되는 것이다. 이와 반대로 인터페이스는 구현해야할 메소드만 올바르게 구현한다면 어떤 다른 클래스를 상속했던 간에 같은 타입으로 취급된다.

인터페이스에는 다음과 같은 장점이 있다.

  1. 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다.
  2. 인터페이스는 믹스인 정의에 안성맞춤이다.
  3. 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
  4. 래퍼 클래스 관용구와 함께 사용한다면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다.

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

기존 클래스에 새로운 인터페이스를 구현하려면, 간단하게 해당 인터페이스를 클래스 선언문에 implements 구문으로 추가하고 인터페이스가 제공하는 메소드들을 구현하기만 하면 끝이다.
반면에 기존 클래스에 새로운 추상 클래스를 상속하기에는 어려움이 따른다. 두 클래스가 같은 추상 클래스를 상속하길 원한다면 계층적으로 두 클래스는 공통인 조상을 가지게 된다. 만약 두 클래스가 어떠한 연관도 없는 클래스라면 클래스 계층에 혼란을 줄 수 있다.

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

믹스인이란 어떤 클래스의 주 기능에 추가적인 기능을 혼합한다 하여서 믹스인이라고 한다. 그러므로 믹스인 인터페이스는 어떤 클래스의 주 기능이외에 믹스인 인터페이스의 기능을 추가적으로 제공하게 해주는 효과를 준다. 
추상 클래스가 믹스인 정의에 맞지않은 이유는 기존 클래스에 덧씌울 수 없기 때문이다. 자바는 단일 상속을 지원하기 때문에 한 클래스가 두 부모를 가질 수 없고 부모와 자식이라는 클래스 계층에서 믹스인이 들어갈 합리적인 위치가 없다.

믹스인 인터페이스엔 대표적으로 Comparable, Cloneable, Serializable 이 존재한다. 

public class Mixin implements Comparable {
	@Override
	public int compareTo(Object o) {
    	return 0;
    }
}

위의 코드와 같이 Comparable을 구현한 클래스는 같은 클래스 인스턴스 끼리는 순서를 정할 수 있는 것을 알 수 있다.

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

현실의 개념 중에는 동물과 포유류, 파충류, 조류 와 같이 타입을 계층적으로 정의하여 구조적으로 잘 표현할 수 있는 개념이 있는가 하면 가수와 작곡가, 그리고 가수겸 작곡가(SingerSongWriter) 같이 계층적으로 표현하기 어려운 개념이 존재한다.
이런 계층구조가 없는 개념들은 인터페이스로 만들기 편하다.

가수, 작곡가 인터페이스가 존재한다. 또한 가수겸 작곡가도 존재한다. 가수와 작곡가를 인터페이스로 정의하였으니 사람이라는 클래스가 두 인터페이스를 구현해도 전혀 문제가 되지 않는다.

public class People implements Singer, SongWriter {
    @Override
    public void Sing(String s) {

    }
    @Override
    public void Compose(int chartPosition) {

    }
}

또한 두 인터페이스를 확장하고 새로운 메소드까지 추가한 인터페이스 또한 정의할 수 있다.

public interface SingerSongWriter extends Singer, SongWriter {
    void 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 클래스를 둘다 상속할 수 없어 SIngerSongWriter라는 또 다른 추상 클래스를 만들어서 클래스 계층을 표현할 수 밖에 없다. 만약 이런 Singer 와 SongWriter와 같은 속성들이 많이 있다면, 그러한 클래스 계층구조를 만들기 위해 많은 조합이 필요하고 결국엔 고도비만 계층구조가 만들어질 것이다. 이러한 현상을 조합 폭발이라고 한다.

4. 래퍼 클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상키는 안전하고 강력한 수단이 된다.

타입을 추상 클래스로 정의해두면 해당 타입에 기능을 추가하는 방법은 상속 뿐이다. 상속해서 만든 클래스는 래퍼 클래스보다 활용도가 떨어지고 쉽게 깨진다.
래퍼 클래스에 대해선 아래 링크를 참고한다.

아이템 [18] - 상속보다는 컴포지션을 사용하라

 

아이템 [18] - 상속보다는 컴포지션을 사용하라

1. 구체클래스 상속의 위험성 다른 패키지의 구체 클래스를 상속하는 일은 위험하다 (인터페이스 상속말고, 구현 상속에서) 메서드 호출과 달리 상속은 캡슐화를 깨뜨린다. - 상위 클래스에 따라 하위클래스의 동..

javabom.tistory.com

 


Java8 부터는 인터페이스에서 디폴트 메소드 기능을 제공해 개발자들이 중복되는 메소드를 구현하는 수고를 덜어줄 수 있게 되었다. 하지만 디폴트 메소드에도 단점은 존재한다. Object의 equals, hashcode 같은 메소드는 디폴트 메소드로 제공해서는 안 된다. 또한 public이 아닌 정적 멤버도 가질 수 없다. 또한 본인이 만들지 않은 인터페이스에는 디폴트 메소드를 추가할 수 없다. 

한편, 인터페이스와 추상 골격 구현 클래스를 함께 제공하면 인터페이스와 추상 클래스의 장점을 모두 가져갈 수 있다..
인터페이스로는 타입을 정의하고 필요한 일부 디폴트 메소드를 구현한다, 추상 골격 구현 클래스는 나머지 메소드들 까지 구현한다. 이렇게 하면 추상 골격 구현 클래스를 확장하는 것만으로 인터페이스를 구현하는데 대부분 일이 완료된다. 이는 템플릿 메소드 패턴과 같다. 이런 추상 골격 구현 클래스를 보여주는 좋은 예로는 컬렉션 프레임워크의 AbstractList, AbstractSet 클래스이다. 이 두 추상 클래스는 각각 List, Set 인터페이스의 추상 골격 구현 클래스이다.

그럼 이제 추상 골격 구현 클래스를 한번 만들어보자. 앞으로 나올 예제는 다음 사이트를 참고하였다.
Favor Skeletal Implementation in Java

 

Favor Skeletal Implementation in Java - DZone Java

Got duplicate code? Learn how to get rid of it while making what's left work for you. A skeletal implementation gets your interface and Abstract class working together.

dzone.com

 

자판기 인터페이스가 있고 그것을 구현하는 음료수 자판기, 커피 자판기가 있다. 

public interface Vending {
    void start();
    void chooseProduct();
    void stop();
    void process();
}
public class BaverageVending implements Vending {
    @Override
    public void start() {
        System.out.println("vending start");
    }

    @Override
    public void chooseProduct() {
        System.out.println("choose menu");
        System.out.println("coke");
        System.out.println("energy drink");
    }

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

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

public class CoffeeVending implements Vending {
    @Override
    public void start() {
        System.out.println("vending start");
    }

    @Override
    public void chooseProduct() {
        System.out.println("choose menu");
        System.out.println("americano");
        System.out.println("cafe latte");
    }

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

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

두 구현체 모두 Vending 인터페이스를 구현한다. 그런에 상품을 선택하는 chooseProduct 메소드를 제외하고 전부 다 같은 동작을 한다. 중복 코드를 제거하기 위해 인터페이스를 추상 클래스로 대체하지 않고 추상 골격 구현을 이용해 중복 코드를 제거 해보자.

public abstract class AbstractVending implements Vending {
    @Override
    public void start() {
        System.out.println("vending start");
    }

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

    @Override
    public void process() {
        start();
        chooseProduct();
        stop();
    }
}
public class BaverageVending extends AbstractVending implements Vending {
    @Override
    public void chooseProduct() {
        System.out.println("choose menu");
        System.out.println("coke");
        System.out.println("energy drink");
    }
}

public class CoffeeVending extends AbstractVending implements Vending {
    @Override
    public void chooseProduct() {
        System.out.println("choose menu");
        System.out.println("americano");
        System.out.println("cafe latte");
    }
}

두 객체의 process 함수 호출 결과

위와 같은 간단한 예제로 인터페이스의 디폴트 메소드를 사용하지 않고 추상 골격 구현 클래스를 만들어 중복을 제거해 보았다. 
그런데, 만약 Vending을 구현하는 구현 클래스가 VendingManuFacturer 라는 제조사 클래스를 상속받아야해서 추상 골격 구현을 확장하지 못하는 상황일 땐 어떻게 해야할까?

public class VendingManufacturer {
    public void printManufacturerName() {
        System.out.println("Made By JavaBom");
    }
}

public class SnackVending extends VendingManufacturer implements Vending {
    InnerAbstractVending innerAbstractVending = new InnerAbstractVending();

    @Override
    public void start() {
        innerAbstractVending.start();
    }

    @Override
    public void chooseProduct() {
        innerAbstractVending.chooseProduct();
    }

    @Override
    public void stop() {
        innerAbstractVending.stop();
    }

    @Override
    public void process() {
        printManufacturerName();
        innerAbstractVending.process();
    }

    private class InnerAbstractVending extends AbstractVending {

        @Override
        public void chooseProduct() {
            System.out.println("choose product");
            System.out.println("chocolate");
            System.out.println("cracker");
        }
    }
}

process 메소드 실행 결과

위와 같이 인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고 각 메소드 호출을 내부 클래스의 인스턴스에 전달하여 골격 구현 클래스를 우회적으로 이용하는 방식을 시뮬레이트한 다중 상속(simulated multiple inheritance) 이라고 한다.

마지막으로 단순 구현(simple implementation)을 알아보자. 단순 구현이란 골격 구현의 작은 변종으로 골격 구현과 같이 상속을 위해 인터페이스를 구현한 것이지만, 추상 클래스가 아니라는 점에서 차이점을 가지고 있다. 
단순 구현은 추상 클래스와 다르게 그대로 써도 되거나 필요에 맞게 확장해도 된다. 단순 구현의 좋은 예로는 AbstractMap.SimpleEntry 가 있다.