Skip to content

Latest commit

 

History

History
157 lines (110 loc) · 7.33 KB

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

File metadata and controls

157 lines (110 loc) · 7.33 KB

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

추상 클래스와 인터페이스는 Java에서 다중 구현을 제공하기 위한 수단이다. 특히 Java 8부터는 default 메서드를 통해서 인터페이스에도 구현을 제공할 수 있다. 또한 Java 9에서는 인터페이스에 private 메서드까지 정의할 수 있다.

public interface Greeting {
    default String hello(String name) {
        return defaultGreeting(name);
    }

    private String defaultGreeting(String name) {
        return String.format("hello %s", name);
    }
}

인터페이스에 구현을 정의할 수 있는 시점에서 추상 클래스와 인터페이스의 큰 차이는 단일 상속과 다중 구현일 것이다. 즉, 다중 상속을 지원하지 않는 Java의 특성상 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야한다는 점이다. 즉, 새로운 타입 정의에 제약이 생긴다는 의미이다.

반면 인터페이스는 어떤 클래스를 상속하든 같은 타입으로 취급한다.

즉, 구현클래스가 인터페이스의 타입을 갖게된다는 의미로 보인다.

public abstract class Walker {
	public abstract String walk();
}

public class Person extends Walker {
	public int walk() {
		return 1;
	}
}

만약 Walker이라는 추상 클래스가 있고 구현 클래스 PersonWalker을 상속한다고 가정한다. 이때 만약 Walker와 함께 다음 Runner 추상 클래스를 끼워넣고 싶다.

public abstract class Runner {
    public abstract String run();
}

이 경우 Java는 다중 상속을 지원하지 않기 때문에 두 추상 클래스를 상속할 수 없다. 둘 중 하나는 포기해야한다는 의미이다. 아니면 WalkerRunner를 상속하거나 RunnerWalker를 상속하거나 하는 방식으로 또는 RunAndWalker 같은 타입으로 타입을 확장해야하는데 이는 불필요한 타입계층을 만들어낼뿐만 아니라 변화에 유연하지 못하게 만들 수 있다.

이를 인터페이스로 구현하면 다음과 PersonWalkerRunner 타입을 얻을 수 있다.

public interface Walker {
    default int walk() { return 1; }
}

public interface Runner {
    default int run() { return 2; }
}

public class Person implements Walker, Runner {
}

이렇게 Person에 편리하게 WalkerRunner 타입을 손쉽게 확장할 수 있다.

mixin

인터페이스는 믹스인 구현에 활용할 수 있다. 상속과 비슷해 보이지만 상속이 클래스와 클래스 사이의 관계를 고정시키는 반면 믹스인은 유연하게 관계를 재구성할 수 있도록 만들어준다. 믹스인은 합성처럼 유연하면서도 상속처럼 쉽게 코드를 재사용하도록 도와준다.

즉, 상속은 자식 클래스를 부모 클래스와 동일한 개념 범주로 묶어 is-a 관계를 만드는 반면 믹스인은 코드를 다른 코드안에 넣기 위한 조합 방식이다.

믹스인은 상속 계층 안에서 확장한 클래스보다 더 하위에 위치한다. 다시 말해서 믹스인은 대상 클래스의 자식 클래스처럼 사용될 용도로 만들어지는 것이다. 따라서 믹스인은 추상 서브클래스라고 부르기도 한다. 믹스인은 결론적으로는 슈퍼클래스로부터 상속될 클래스를 명시하는 메커니즘을 표현한다. 따라서 하나의 믹스인은 매우 다양한 클래스를 도출하면서 서로 다른 서브클래스를 이용해 인스턴스화 할 수 있다.

위 믹스인 정의는 오브젝트(저: 조영호)의 11장 합성과 유연한 설계 부분에서 발췌

Java 표준 API인 Comparable처럼 순서를 정할 수 있다고 선언하는 믹스인 인터페이스로 특정 클래스에 선택적인 기능을 혼합하도록 인터페이스를 사용할 수 있다.

다만, 인터페이스가 mixin을 위한 기능은 아니기 때문에 메서드 override를 강제로 막지 못한다는 점은 아쉬운점인거 같다. 인터페이스 메서드에 final 사용 불가

추상 클래스는 언제 사용하는게 좋을까?

추상 클래스와 인터페이스의 차이는 단일 상속 / 다중 구현 뿐일까?

  1. 추상 클래스는 인터페이스와 달리 프로퍼티를 정의할 수 있다. 참고로 인터페이스도 프로퍼티를 정의할 수 있지만 기본적으로 public static 형태로 정의된다. 상수취급

    즉, 추상 클래스의 구현 클래스는 추상 클래스가 가진 프로퍼티에 접근할 수 있다.

  2. protected 추상 메서드를 정의할 수 있다.

    인터페이스에서는 protected 접근자를 사용할 수 없다.

위 두 특징으로 가장 잘 활용할 수 있는 방법이 바로 템플릿 메서드 패턴이다.

템플릿 메서드 패턴

계산기를 구현한다고 가정한다. 이때 계산기는 덧셈, 나눗셈 등 여러 연산을 다루는 구현체가 있을 수 있다고 가정한다. 또한 0 ~ 100까지의 숫자만 연산에 사용하는 것이 요구사항이라고 가정한다.

public interface Calculator {
    int calculate(int x, int y);
}

public abstract class AbstractCalculator implements Calculator {
    private final CalculateValidator calculateValidator;

    public AbstractCalculator(CalculateValidator calculateValidator) {
        this.calculateValidator = calculateValidator;
    }

    @Override
    public int calculate(int x, int y) {
        calculateValidator.validate(x, y);
        return operate(x, y);
    }

    public abstract boolean isSupport(CalculateType type);
    protected abstract int operate(int x, int y);
}

위 요구사항에 따라서 다음과 같은 템플릿을 만들 수 있을 것이다. 이때 0 ~ 100의 숫자에 대한 유효성 검사는 모든 연산에서 필요한 부분이므로 calculateValidator를 두어 처리할 수 있을것이다.

그외 연산은 각 연산의 구현 클래스마다 달라질 수 있는 부분이다. 이 부분에 대해서는 operateprotected로 열어두어 구현클래스에서 정의하도록 만들고 이를 calculate에서 사용하도록 만들 수 있다.

public class PlusCalculator extends AbstractCalculator {
    public PlusCalculator(CalculateValidator calculateValidator) {
        super(calculateValidator);
    }

    @Override
    public boolean isSupport(CalculateType type) {
        return type == PLUS;
    }

    @Override
    protected int operate(int x, int y) {
        return x + y;
    }
}

public class MinusCalculator extends AbstractCalculator {
    public MinusCalculator(CalculateValidator calculateValidator) {
        super(calculateValidator);
    }

    @Override
    public boolean isSupport(CalculateType type) {
        return type == MINUS;
    }

    @Override
    protected int operate(int x, int y) {
        return x - y;
    }
}

이런 템플릿을 토대로 덧셈을 구현한 PlusCalculator나 뺄셈을 구현한 MinusCalculator를 위와 같이 정의할 수 있을 것이다.

protected가 Java에서는 자식 클래스 뿐만 아니라 같은 패키지 안에 클래스에서도 접근가능한 부분은 아쉬운 부분이다. 최근에 등장한 JVM 언어인 Kotlin에서는 protected가 자식 클래스에서만 접근가능하도록 허용한다.