무지를 아는 것이 곧 앎의 시작

Java

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

Alex96 2022. 3. 3. 23:26

이펙티브 자바의 아이템20. '추상 클래스보다는 인터페이스를 우선하라' 부분을 읽고 정리하는 포스팅.✍️

 

클래스에 특정 역할을 부여하는 데에는 인터페이스가 우선이다.

자바 8부터 인터페이스도 default 메서드를 지원하여 인스턴스 구현 메서드를 구현 형태로 제공할 수 있다.

그리고 추상 클래스는 정의한 타입을 구현하는 클래스가 반드시 추상 클래스의 하위 클래스가 되어야한다는 제약 조건이 따른다.

반면에, 인터페이스는 구현한 모든 클래스를 어떤 클래스를 상속했든 같은 타입으로 취급한다.그래서 인터페이스는 기존 클래스에도 새로운 인터페이스를 구현하도록 하는데 제약이 없다.

 

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

믹스인이란, 클래스가 구현할 수 있는 타입으로, '주된 타입'외에 특정 선택적 행위를 제공하는 효과를 주는 것을 말한다.예를 들면, Comparable 인터페이스는 자신을 구현한 인스턴스끼리는 순서를 정할 수 있는 행위를 제공하는 인터페이스다.

추상 클래스에선 기존 클래스에 덧씌울 수 없기 때문에 믹스인을 정의할 수 없다.

인터페이스는 먼저 클래스를 정의하고 인터페이스를 입히는 데에 제약이 없지만, 추상 클래스는 두 부모를 섬길 수 없기 때문에 믹스인을 삽입하기 적합하지 않다.

 

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

예시코드 - 출처

public abstract class Singer {
   abstract AudioClip sing(Song song);
}

public abstract class Songwriter {
    abstract Song compose(int chartPosition);
}

public abstract class SingerSongwriter {
    // Singer의 함수
    abstract AudioClip sing(Song song);
    // Songwriter 함수
    abstract Song compose(int chartPosition);
    
    abstract AudioClip strum();
    abstract void actSensitive();
}

위 코드처럼 가수와 작곡가를 모두 포함하는 SingerSongwriter라는 새로운 역할을 만드려고 했을 때 추상 클래스로 만드려면, 구현하려는 클래스의 메서드를 그대로 뽑아와서 구현해야 한다. 이런 현상을 조합 폭발(combinatorial explosion)이라 한다.

 

public interface Singer {
    AudioClip sing(Song s);
}

public interface Songwriter {
    Song compose(int chartPosition);
}

public interface SingerSongwriter extends Singer, Songwriter {
    AudioClip strum();
    void actSensitive();
}

반면에, 인터페이스를 사용하면 '작곡을 하면서 노래도 부르는 가수'를 두 인터페이스를 구현하도록 하여 쉽게 표현할 수 있다.

심지어 두 인터페이스를 확장한 새로운 인터페이스를 만들 수도 있다.

추상 클래스처럼 계층에 얽메이지 않기 때문에 유연하게 역할을 부여할 수 있다.


그렇다면, 추상 클래스는 어디에 쓰는거지..?🤔

추상 클래스는 인터페이스를 구현하여 추상 골격 구현(skeletal implementation)을 통해 '템플릿 메서드 패턴'을 구현할 때 사용한다.

템플릿 메서드 패턴

  1. 인터페이스로 타입을 정의하고, 필요하면 디폴트 메서드까지 제공한다.
  2. 골격 구현 추상 클래스로 인터페이스를 구현하여 나머지 메서드들을 구현한다.
  3. 실제 사용할 클래스에서 이 골격 구현 추상 클래스를 확장하여 이미 대부분 구현된 인터페이스를 누린다.

골격 구현 클래스의 네이밍은 인터페이스 이름이 Interface라면 AbstractInterface라고 이름 짓는다.

컬렉션 프레임워크는 이러한 템플릿 메서드 패턴을 사용한 좋은 예시다.

AbstractCollection, AbstractSet, AbstractList, AbstractMap 각각이 모두 컬렉션 인터페이스의 골격 구현이다.

 

예시) AbstractList 골격 구현 클래스를 사용해서 구체 클래스 완성하기

public class IntArrays {
    static List<Integer> intArrayAsList(int[] a) {
        Objects.requireNonNull(a);

        return new AbstractList<>() {
            @Override
            public Integer get(int i) {
                return a[i];
            }

            @Override
            public Integer set(int i, Integer val) {
                int oldVal = a[i];
                a[i] = val;
                return oldVal;
            }

            @Override
            public int size() {
                return a.length;
            }
        };
    }
}

골격 구현 클래스를 사용해서 단 3개의 메서드만 오버라이드 하고 List를 구현한 구현체를 만들었다.

골격 구현 클래스를 사용하면 구현을 도와줌과 동시에 추상 클래스로 타입을 정의할 때 따라오는 심각한 제약에는 꽤 자유로워진다.

 

시뮬레이트한 다중 상속(simulated multiple inheritance)

골격 구현 클래스를 우회적으로 이용하여, 다중 상속의 많은 장점을 제공하는 동시에 단점은 피하게 해주는 방법이다.

예시코드- 출처

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");
        }
    }
}

위의 코드에선 AbsractVending이라는 추상 골격 클래스를 확장한 우회용 클래스를 구현해야할 클래스의 private 이너 클래스로 구현했다. 그리고 클래스 내부에서 그 우회용 클래스의 메서드를 호출해서 제공되는 추상 골격 클래스의 장점을 누리고 있다.

 

핵심

일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다.

복잡한 인터페이스라면 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 고려할 것.

골격 구현은 가능한 한 디폴트 메서드로 제공하고, 필요하면 골격 구현 추상 클래스를 만들어서 제공한다.

다른 의견.

인터페이스의 디폴트 메서드는 유지보수를 위해 나온 것이므로 골격 구현에 사용하는 것은 바람직하지 못하다.

또한, 인터페이스는 상태를 가지지 못하기 때문에 상태를 건드리는 메서드를 물려줘야한다면 제약이 있을 수 있다.

반대로 장점은 추상클래스에 비해 인터페이스가 가지는 유연함이 있음.

여러 개를 implements할 수도 있고, 기능의 명세만 정의하니까 내부 구조, 생성자 등등 구현 클래스에서 자유롭게 구현할 수 있다.

 

끗.