개발은 재밌어야 한다
article thumbnail
반응형

추상 클래스, 인터페이스 비교

  • 공통점:
    • 타입을 정의하기 위한 다중 구현 메커니즘
    • 인스턴스 메소드(디폴트 메서드)를 구현 형태로 제공
  • 차이점:
    • 추상 클래스:
      • 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스 > 새로운 타입 정의에 제약
    • 인터 페이스:
      • 어떤 클래스 타입에 있건 인터페이스를 정의한 클래스는 해당 인터페이스 타입 가능 취급 > 다중 상속 가능

인터페이스 장점 : 기존 클래스도 인터페이스 타입이 될 수 있다.

기존 클래스에도, 새로운 인터페이스를 구현하도록 하면 되기 때문에 기존 클래스도 인터페이스 타입이 될 수 있다.

  • Comparable, Iterable, AutoCloeable 인터페이스가 새로 추가되었을 때, 자바의 수많은 기존 클래스가 해당 인터페이스들을 구현한 채 릴리즈 되었다.

반면 기존 클래스가 새로운 추상 클래스가 되도록 하는 것은 매우 어렵다.

그래서 인터페이스는 mixin 정의에 안성맞춤이다 (선택적인 기능 추가)

mixin 타입이란?

  • 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래 ‘주된 타입’외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.

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

타입을 계층적으로 정의하면 수많은 개념을 구조적으로 표현할 수 있지만 현실에는 계층을 엄격히 구분하기 어려운 개념이 있다.

예를 들어 가수와 작곡가를 생각해보자.

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

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

작곡도 하는 가수라면?

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

인터페이스로 정의하면 Singer 와 Songwirter 모두를 구현해도 문제가 되지 않고 추가로 메서드를 정의해도 문제가 되지 않음.

같은 구조를 클래스로 만들려면 가능한 조합 전부를 각각의 클래스로 정의한 고도비만 계층구조가 만들어질 수 있음

(2의n제곱(n은 속성의 개수))(조합폭발이라부름(combinatorial explosion))

추상 골격 구현 클래스

인터페이스로는 타입을 정의하고, 골격 구현 클래스는 나머지 메서드들까지 구현 => 골격 구현을 확장하는 것만으로 인터페이스를 구현하는데 필요한 일이 거의 완료 => "템플릿 메서드 패턴"

관례상 인터페이스 이름이 Interface라면 골격 구현 클래스 이름은 AbstractInterface로 짓는다. (예. AbstractCollection)

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

        return new AbstractList<>() {
            @Override public Integer get(int i) {
                return a[i];  // 오토박싱(아이템 6)
            }

            @Override public Integer set(int i, Integer val) {
                int oldVal = a[i];
                a[i] = val;     // 오토언박싱
                return oldVal;  // 오토박싱
            }

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

    public static void main(String[] args) {
        int[] a = new int[10];
        for (int i = 0; i < a.length; i++)
            a[i] = i;

        List<Integer> list = intArrayAsList(a);
        Collections.shuffle(list);
        System.out.println(list);
    }
}
  • 추상클래스처럼 구현을 도와줌
  • 추상 클래스로 타입을 정의할 때 따라오는 심각한 제약에서는 자유로움

구조상 골격 구현을 확장하지 못한다면 인터페이스를 직접 구현해야함

인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고 각 메서드 호출을 내부 클래스의 인스턴스에 전달하면 된다.

골격 구현 작성 방법

  1. 인터페이스에서 다른 메서드들의 구현에 사용되는 기반 메서드들 선정
  2. 기반 메서드를 사용해 직접 구현할 수 있는 메서드를 모두 디폴트 메서드로 제공
    • Object의 메서드는 제공 X
  3. 기반 메서드나, 디폴트 메서드로 만들지 못한 메서드가 남아 있다면 골격 구현 클래스를 하나 만들어 남은 메서드들을 작성
    • 만약 남은 메서드가 없다면 골격 구현 클래스를 만들 필요 X
public abstract class AbstractMapEntry<K,V>implements Map.Entry<K,V> {
    // 변경 가능한 엔트리는 이 메서드를 반드시 재정의해야 한다.
    @Override public V setValue(V value) {
        throw new UnsupportedOperationException();
    }

    // Map.Entry.equals의 일반 규약을 구현한다.
    @Override public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry) o;
        return Objects.equals(e.getKey(),   getKey())
                && Objects.equals(e.getValue(), getValue());
    }

    // Map.Entry.hashCode의 일반 규약을 구현한다.
    @Override public int hashCode() {
        return Objects.hashCode(getKey())
                ^ Objects.hashCode(getValue());
    }

    @Override public String toString() {
        return getKey() + "=" + getValue();
    }
}

조금 더 간단한 예제를 보자. 아래는 골격화를 사용하기 전이다.

반응형
public interface Student {

    void eat();

    void study();

    void goHome();

    void dailyRoutine();
}

public class Gil implements Student {

    @Override
    public void study() {
        System.out.println("클래스와 인터페이스 공부를 해요.");
    }

    @Override
    public void eat() {
        System.out.println("학식을 먹어요");
    }

    @Override
    public void goHome() {
        System.out.println("집으로 돌아가요");
    }

    @Override
    public void dailyRoutine() {
        eat();
        study();
        goHome();
    }
}

public class Jang implements Student {

    @Override
    public void study() {
        System.out.println("객체의 생성과 파괴를 공부해요");
    }
    @Override
    public void eat() {
        System.out.println("학식을 먹어요");
    }

    @Override
    public void goHome() {
        System.out.println("집으로 돌아가요");
    }

    @Override
    public void dailyRoutine() {
        eat();
        study();
        goHome();
    }
}

그럼 골격화를 사용하면 어떻게 될까?

public interface Student {

    void eat();

    void study();

    void goHome();

    void dailyRoutine();
}

public abstract class DguStudent implements Student {

    @Override
    public void eat() {
        System.out.println("학식을 먹어요");
    }

    @Override
    public void goHome() {
        System.out.println("집으로 돌아가요");
    }

    @Override
    public void dailyRoutine() {
        eat();
        study();
        goHome();
    }
}

public class Jang extends DguStudent implements Student {
    @Override
    public void study() {
        System.out.println("객체의 생성과 파괴를 공부해요");
    }
}

public class Gil extends DguStudent implements Student {

    @Override
    public void study() {
        System.out.println("클래스와 인터페이스 공부를 해요.");
    }
}

public class Main {

    public static void main(String[] args) {

        List<Student> students = new ArrayList<>();
        Student jang = new Jang();
        Student gil = new Gil();
        students.add(jang);
        students.add(gil);

        for (Student student : students) {
            student.dailyRoutine();
        }
    }
}
학식을 먹어요
객체의 생성과 파괴를 공부해요
집으로 돌아가요
학식을 먹어요
클래스와 인터페이스 공부를 해요.
집으로 돌아가요

중복되는 코드도 줄일 수 있고, 한층 더 유연성을 갖게 되었다.


정리

  • 일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다.
  • 복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하자.
  • 골격 구현은 인터페이스의 디폴트 메서드로 제공할 수 있고, 추상 골격 구현 클래스로 제공할 수 있다.



반응형
profile

개발은 재밌어야 한다

@ghyeong

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!